//! The content policy at `/policy`, described.
//!
//! The third public document. It replaces `templates/pages/policy.html`,
//! `PolicyTemplate` and `landing::policy_page`.
//!
//! # Prose is prose, and rows are for data
//!
//! `/use-cases` set the rule that a card holding a structure is a region and a
//! card holding a sentence is a row. This page is the case that rule does not
//! reach: six sections of prose, with bullets that are sentences rather than
//! records, carrying inline links and one emphasised address.
//!
//! A `Row` cannot hold either. `Row::new("Report suspicious downloads to
//! reports@makenot.work")` loses the emphasis, and nothing in a row can carry
//! the link inside "part of our [creator guarantees]". Forcing prose through
//! rows would silently flatten both, and neither loss is visible in a test that
//! checks the text is present.
//!
//! So each section's body is one [`super::own_prose`], which is markdown and
//! renders through docengine -- the same path `/docs/*` takes, so the policy
//! prose and the documents it links to are formatted by one renderer. **What
//! stays described is the structure**: [`Node::section`] per heading, so the
//! section hierarchy is a fact of the screen rather than an `
` inside a
//! blob.
//!
//! `own_prose` and not `Node::rich`, which is the difference between a page
//! that points at its own documents and a page that tells crawlers not to
//! follow them (quasi 0.94, quasicoherent `24a3b1df`).
//!
//! The escape hatch is not swallowing the page. It is carrying the one thing
//! this page is made of, which is sentences.
//!
//! # The exception, and it is the one list that is data
//!
//! "Other Policies" is seven links, each a title and a sentence saying what the
//! document covers. That is a record per row and a route per row, so it is a
//! [`Node::list`] of rows with acts, not markdown. The test is what it would
//! cost to add an eighth: a row, versus a line of prose somebody has to match
//! against six others by hand.
//!
//! # The words are in `content/policy.toml`
//!
//! Both lists were `const` arrays here and are now copy, read at build time by
//! `copy "content/policy.toml" as sections` and `as others`. What is left in
//! this file is the structure: which regions there are, that a section's body
//! is prose and an other-policy is a row with an act. Somebody changing what
//! the policy says edits TOML and reads no Rust, which is the whole point
//! (quasicoherent `98fbee62`).
//!
//! It is not only a place to put words. A `const` cannot be evaluated by a
//! proc macro, so `for part in SECTIONS` survived into whatever the macro
//! emitted, loop and all. Copy the macro can read is copy the macro can write
//! out, which is what leaves the page foldable to a literal later.
//!
//! The closing paragraph went too, and it was a `const` here until this pass.
//! Keeping it in Rust would have cost the fold: a `const` is a path and not a
//! literal, so an `include` of it opens a staged scope and the sentence gets
//! built and rendered per request. The spelling device it existed for is worth
//! less than that.
use makeover_layout as layout;
use quasi_declare::declare;
use quasi_router::{Document, Request, Response, RouteError};
use quasi_webview::Webview;
/// The address, registered whole. See [`super::public_document_mount`].
pub const PATH: &str = "/policy";
/// The page's own region, and what the skip link points at.
pub const PAGE_REGION: &str = "policy";
const MEASURE: layout::Measure = layout::Measure::Wide;
/// The page. Reads nothing.
///
/// Kept beside the residual mount rather than replaced by it, and it is what
/// the residual is checked against: `quasi::residuals` asserts that filling the
/// compiled one gives back exactly what building and rendering this gives. A
/// screen with no second way to produce its markup has nothing to check the
/// first one with.
pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result {
Ok(page_screen().into())
}
declare! {
/// The whole document: the title, the measure, the body.
pub(crate) shape page_screen() -> Screen;
screen single "Content Policy - Makenotwork" {
measured MEASURE;
documented Document::default().classed(crate::shell::body_class(MEASURE, &["policy-page"]));
summarised "What belongs on Makenotwork, what doesn't, and how problems are handled.";
include page_region();
}
}
declare! {
/// The page's one region, split out so it can be staged.
///
/// `#[staged]` wants a shape whose residual is derived by rendering it, and
/// the document around it is not derivable: the shell carries the site
/// header, which says whether anybody is signed in. So the split is exactly
/// where the request stops mattering. Everything below this line is the
/// same for every reader on every request, and everything above it is not.
#[staged]
pub(crate) shape page_region() -> Slot;
region PAGE_REGION as Pane {
page "Content Policy";
text "Makenotwork exists so creators can sell their work on fair terms. This policy \
describes what belongs here, what doesn't, and how we handle problems.";
for part in copy "content/policy.toml" as sections {
section part.heading;
include super::own_prose(part.body);
}
section "Other Policies";
list {
for other in copy "content/policy.toml" as others {
row other.title {
secondary other.covers;
act "Read" to get other.route navigating;
}
}
}
section "Questions";
for closing in copy "content/policy.toml" as closing {
include super::own_prose(closing.body);
}
}
}
/// The document this screen is drawn in. Same shape as the other public
/// documents: the skip link and the site header, whose user is optional here.
#[must_use]
pub fn renderer(viewer: &super::Viewer) -> Webview {
Webview::new().with_shell(viewer.document_shell().with_body_first(format!(
"{}{}",
crate::shell::skip_link(PAGE_REGION),
crate::shell::site_header(viewer.user.as_ref()),
)))
}
#[cfg(test)]
mod tests {
use super::*;
fn html() -> String {
use quasi_axum::Serves as _;
Webview::new().screen(&page_screen())
}
/// `2790e5c4`. This template carried both classes on the body already, so
/// this one is a copy rather than the merge `/team` and `/use-cases` were.
#[test]
fn the_document_carries_the_classes_the_template_carried() {
let screen = page_screen();
assert_eq!(
screen.document.body_class.as_deref(),
Some("padded-page policy-page")
);
assert!(
html().contains("class=\"padded-page policy-page\""),
"{}",
html()
);
}
/// The two things rows would have flattened, which is the whole argument
/// for prose being prose: an inline link inside a sentence, and an
/// emphasised address inside a bullet.
#[test]
fn the_prose_keeps_its_inline_link_and_its_emphasis() {
let html = html();
assert!(
html.contains(r#"href="/docs/guarantees""#),
"the creator-guarantees link did not survive: {html}"
);
// This page exists to point at the other policy documents, so a
// `nofollow` on the way there would be the page working against itself.
// The seal on `super::own_prose`: an untrusted source is hardened.
assert!(
!html.contains("nofollow"),
"the policy page nofollowed its own documents: {html}"
);
assert!(
html.contains("reports@makenot.work")
|| html.contains("reports@makenot.work"),
"the reports address lost its emphasis: {html}"
);
}
/// The copy the page is built from, read the way the macro reads it.
///
/// The test asks the content file what the page should say rather than
/// holding a second copy of it. A row that stops being rendered fails here;
/// a row somebody deletes from the file is a deletion and not a failure,
/// which is the right answer for copy.
fn copy() -> toml::Table {
include_str!("../../content/policy.toml")
.parse()
.expect("the policy copy is TOML")
}
/// Every policy document the file names is on the page.
///
/// The titles are compared escaped, because that is what lands in the
/// markup: two of the seven carry an ampersand, and the description layer
/// escapes it exactly as the template's `&` did.
#[test]
fn every_other_policy_keeps_its_row_and_its_route() {
let html = html();
let copy = copy();
let others = copy["others"].as_array().expect("a list of documents");
assert_eq!(others.len(), 7, "the page pointed at seven documents");
for other in others {
let title = other["title"].as_str().expect("a title");
let route = other["route"].as_str().expect("a route");
let escaped = crate::helpers::escape_html(title);
assert!(html.contains(&escaped), "{title} missing");
assert!(html.contains(route), "{route} missing");
}
}
/// Every prose section the file names is a heading on the page.
///
/// The counterpart for the other list, and the reason both are here: the
/// copy moved out of Rust in `98fbee62`, so what proves it is still on the
/// page has to read the file it moved into.
#[test]
fn every_section_keeps_its_heading() {
let html = html();
let copy = copy();
let sections = copy["sections"].as_array().expect("a list of sections");
assert_eq!(sections.len(), 5, "the page had five prose sections");
for section in sections {
let heading = section["heading"].as_str().expect("a heading");
let escaped = crate::helpers::escape_html(heading);
assert!(html.contains(&escaped), "{heading} missing");
}
}
/// Both addresses a reader is told to write to are still on the page. They
/// are different mailboxes on purpose (`feedback_mnw_email_routing`), so a
/// conversion that collapsed them would be a real loss.
#[test]
fn both_contact_addresses_survive_and_stay_distinct() {
let html = html();
assert!(html.contains("reports@makenot.work"), "{html}");
assert!(html.contains("policy@makenot.work"), "{html}");
}
/// `736f45a5`: this screen's markup carries none of the four spellings.
#[test]
fn the_page_spells_no_spinner() {
let html = html();
for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
assert!(!html.contains(spelling), "{spelling} survives in {html}");
}
}
}