//! 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}"); } } }