Skip to main content

max / makenotwork

10.8 KB · 272 lines History Blame Raw
1 //! The content policy at `/policy`, described.
2 //!
3 //! The third public document. It replaces `templates/pages/policy.html`,
4 //! `PolicyTemplate` and `landing::policy_page`.
5 //!
6 //! # Prose is prose, and rows are for data
7 //!
8 //! `/use-cases` set the rule that a card holding a structure is a region and a
9 //! card holding a sentence is a row. This page is the case that rule does not
10 //! reach: six sections of prose, with bullets that are sentences rather than
11 //! records, carrying inline links and one emphasised address.
12 //!
13 //! A `Row` cannot hold either. `Row::new("Report suspicious downloads to
14 //! reports@makenot.work")` loses the emphasis, and nothing in a row can carry
15 //! the link inside "part of our [creator guarantees]". Forcing prose through
16 //! rows would silently flatten both, and neither loss is visible in a test that
17 //! checks the text is present.
18 //!
19 //! So each section's body is one [`super::own_prose`], which is markdown and
20 //! renders through docengine -- the same path `/docs/*` takes, so the policy
21 //! prose and the documents it links to are formatted by one renderer. **What
22 //! stays described is the structure**: [`Node::section`] per heading, so the
23 //! section hierarchy is a fact of the screen rather than an `<h2>` inside a
24 //! blob.
25 //!
26 //! `own_prose` and not `Node::rich`, which is the difference between a page
27 //! that points at its own documents and a page that tells crawlers not to
28 //! follow them (quasi 0.94, quasicoherent `24a3b1df`).
29 //!
30 //! The escape hatch is not swallowing the page. It is carrying the one thing
31 //! this page is made of, which is sentences.
32 //!
33 //! # The exception, and it is the one list that is data
34 //!
35 //! "Other Policies" is seven links, each a title and a sentence saying what the
36 //! document covers. That is a record per row and a route per row, so it is a
37 //! [`Node::list`] of rows with acts, not markdown. The test is what it would
38 //! cost to add an eighth: a row, versus a line of prose somebody has to match
39 //! against six others by hand.
40 //!
41 //! # The words are in `content/policy.toml`
42 //!
43 //! Both lists were `const` arrays here and are now copy, read at build time by
44 //! `copy "content/policy.toml" as sections` and `as others`. What is left in
45 //! this file is the structure: which regions there are, that a section's body
46 //! is prose and an other-policy is a row with an act. Somebody changing what
47 //! the policy says edits TOML and reads no Rust, which is the whole point
48 //! (quasicoherent `98fbee62`).
49 //!
50 //! It is not only a place to put words. A `const` cannot be evaluated by a
51 //! proc macro, so `for part in SECTIONS` survived into whatever the macro
52 //! emitted, loop and all. Copy the macro can read is copy the macro can write
53 //! out, which is what leaves the page foldable to a literal later.
54 //!
55 //! The closing paragraph went too, and it was a `const` here until this pass.
56 //! Keeping it in Rust would have cost the fold: a `const` is a path and not a
57 //! literal, so an `include` of it opens a staged scope and the sentence gets
58 //! built and rendered per request. The spelling device it existed for is worth
59 //! less than that.
60
61 use makeover_layout as layout;
62 use quasi_declare::declare;
63 use quasi_router::{Document, Request, Response, RouteError};
64 use quasi_webview::Webview;
65
66 /// The address, registered whole. See [`super::public_document_mount`].
67 pub const PATH: &str = "/policy";
68
69 /// The page's own region, and what the skip link points at.
70 pub const PAGE_REGION: &str = "policy";
71
72 const MEASURE: layout::Measure = layout::Measure::Wide;
73
74 /// The page. Reads nothing.
75 ///
76 /// Kept beside the residual mount rather than replaced by it, and it is what
77 /// the residual is checked against: `quasi::residuals` asserts that filling the
78 /// compiled one gives back exactly what building and rendering this gives. A
79 /// screen with no second way to produce its markup has nothing to check the
80 /// first one with.
81 pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
82 Ok(page_screen().into())
83 }
84
85 declare! {
86 /// The whole document: the title, the measure, the body.
87 pub(crate) shape page_screen() -> Screen;
88
89 screen single "Content Policy - Makenotwork" {
90 measured MEASURE;
91 documented Document::default().classed(crate::shell::body_class(MEASURE, &["policy-page"]));
92 summarised "What belongs on Makenotwork, what doesn't, and how problems are handled.";
93
94 include page_region();
95 }
96 }
97
98 declare! {
99 /// The page's one region, split out so it can be staged.
100 ///
101 /// `#[staged]` wants a shape whose residual is derived by rendering it, and
102 /// the document around it is not derivable: the shell carries the site
103 /// header, which says whether anybody is signed in. So the split is exactly
104 /// where the request stops mattering. Everything below this line is the
105 /// same for every reader on every request, and everything above it is not.
106 #[staged]
107 pub(crate) shape page_region() -> Slot;
108
109 region PAGE_REGION as Pane {
110 page "Content Policy";
111 text "Makenotwork exists so creators can sell their work on fair terms. This policy \
112 describes what belongs here, what doesn't, and how we handle problems.";
113
114 for part in copy "content/policy.toml" as sections {
115 section part.heading;
116 include super::own_prose(part.body);
117 }
118
119 section "Other Policies";
120 list {
121 for other in copy "content/policy.toml" as others {
122 row other.title {
123 secondary other.covers;
124 act "Read" to get other.route navigating;
125 }
126 }
127 }
128
129 section "Questions";
130 for closing in copy "content/policy.toml" as closing {
131 include super::own_prose(closing.body);
132 }
133 }
134 }
135
136 /// The document this screen is drawn in. Same shape as the other public
137 /// documents: the skip link and the site header, whose user is optional here.
138 #[must_use]
139 pub fn renderer(viewer: &super::Viewer) -> Webview {
140 Webview::new().with_shell(viewer.document_shell().with_body_first(format!(
141 "{}{}",
142 crate::shell::skip_link(PAGE_REGION),
143 crate::shell::site_header(viewer.user.as_ref()),
144 )))
145 }
146
147 #[cfg(test)]
148 mod tests {
149 use super::*;
150
151 fn html() -> String {
152 use quasi_axum::Serves as _;
153
154 Webview::new().screen(&page_screen())
155 }
156
157 /// `2790e5c4`. This template carried both classes on the body already, so
158 /// this one is a copy rather than the merge `/team` and `/use-cases` were.
159 #[test]
160 fn the_document_carries_the_classes_the_template_carried() {
161 let screen = page_screen();
162
163 assert_eq!(
164 screen.document.body_class.as_deref(),
165 Some("padded-page policy-page")
166 );
167 assert!(
168 html().contains("class=\"padded-page policy-page\""),
169 "{}",
170 html()
171 );
172 }
173
174 /// The two things rows would have flattened, which is the whole argument
175 /// for prose being prose: an inline link inside a sentence, and an
176 /// emphasised address inside a bullet.
177 #[test]
178 fn the_prose_keeps_its_inline_link_and_its_emphasis() {
179 let html = html();
180
181 assert!(
182 html.contains(r#"href="/docs/guarantees""#),
183 "the creator-guarantees link did not survive: {html}"
184 );
185 // This page exists to point at the other policy documents, so a
186 // `nofollow` on the way there would be the page working against itself.
187 // The seal on `super::own_prose`: an untrusted source is hardened.
188 assert!(
189 !html.contains("nofollow"),
190 "the policy page nofollowed its own documents: {html}"
191 );
192 assert!(
193 html.contains("<strong>reports@makenot.work</strong>")
194 || html.contains("<b>reports@makenot.work</b>"),
195 "the reports address lost its emphasis: {html}"
196 );
197 }
198
199 /// The copy the page is built from, read the way the macro reads it.
200 ///
201 /// The test asks the content file what the page should say rather than
202 /// holding a second copy of it. A row that stops being rendered fails here;
203 /// a row somebody deletes from the file is a deletion and not a failure,
204 /// which is the right answer for copy.
205 fn copy() -> toml::Table {
206 include_str!("../../content/policy.toml")
207 .parse()
208 .expect("the policy copy is TOML")
209 }
210
211 /// Every policy document the file names is on the page.
212 ///
213 /// The titles are compared escaped, because that is what lands in the
214 /// markup: two of the seven carry an ampersand, and the description layer
215 /// escapes it exactly as the template's `&amp;` did.
216 #[test]
217 fn every_other_policy_keeps_its_row_and_its_route() {
218 let html = html();
219
220 let copy = copy();
221 let others = copy["others"].as_array().expect("a list of documents");
222 assert_eq!(others.len(), 7, "the page pointed at seven documents");
223 for other in others {
224 let title = other["title"].as_str().expect("a title");
225 let route = other["route"].as_str().expect("a route");
226 let escaped = crate::helpers::escape_html(title);
227 assert!(html.contains(&escaped), "{title} missing");
228 assert!(html.contains(route), "{route} missing");
229 }
230 }
231
232 /// Every prose section the file names is a heading on the page.
233 ///
234 /// The counterpart for the other list, and the reason both are here: the
235 /// copy moved out of Rust in `98fbee62`, so what proves it is still on the
236 /// page has to read the file it moved into.
237 #[test]
238 fn every_section_keeps_its_heading() {
239 let html = html();
240
241 let copy = copy();
242 let sections = copy["sections"].as_array().expect("a list of sections");
243 assert_eq!(sections.len(), 5, "the page had five prose sections");
244 for section in sections {
245 let heading = section["heading"].as_str().expect("a heading");
246 let escaped = crate::helpers::escape_html(heading);
247 assert!(html.contains(&escaped), "{heading} missing");
248 }
249 }
250
251 /// Both addresses a reader is told to write to are still on the page. They
252 /// are different mailboxes on purpose (`feedback_mnw_email_routing`), so a
253 /// conversion that collapsed them would be a real loss.
254 #[test]
255 fn both_contact_addresses_survive_and_stay_distinct() {
256 let html = html();
257
258 assert!(html.contains("reports@makenot.work"), "{html}");
259 assert!(html.contains("policy@makenot.work"), "{html}");
260 }
261
262 /// `736f45a5`: this screen's markup carries none of the four spellings.
263 #[test]
264 fn the_page_spells_no_spinner() {
265 let html = html();
266
267 for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
268 assert!(!html.contains(spelling), "{spelling} survives in {html}");
269 }
270 }
271 }
272