Skip to main content

max / makenotwork

Compose a converted screen's body class from its measure and its own tokens
Author: Max Johnson <me@maxj.phd> · 2026-08-31 12:21 UTC
Signed with PGP, not checked
Commit: ef0c345099cace4896537cb0bfdcffd68fbbd6ac
Parent: ac8541a
2 files changed, +77 insertions, -8 deletions
@@ -203,6 +203,27 @@
203 203 }
204 204 }
205 205
206 + /// The `<body>` class a screen that owns its document carries: its measure
207 + /// first, then whatever else its template said beside it.
208 + ///
209 + /// [`quasi_router::Document::classed`] replaces rather than appends, so a
210 + /// screen with a grouping or identity token of its own hands over one string.
211 + /// Composing it here is what stops a screen that meant to add `feed-page` from
212 + /// dropping `padded-page` on the way.
213 + ///
214 + /// The global half is not here. `Shell` carries what is true of every document
215 + /// and the renderer writes the two beside each other, the same composition
216 + /// `base.html` does with [`body_attrs`].
217 + #[must_use]
218 + pub fn body_class(page: Measure, own: &[&str]) -> String {
219 + let mut class = String::from(measure(page));
220 + for token in own {
221 + class.push(' ');
222 + class.push_str(token);
223 + }
224 + class
225 + }
226 +
206 227 #[cfg(test)]
207 228 mod tests {
208 229 use super::*;
@@ -268,6 +289,25 @@
268 289 assert_eq!(measure(Measure::Reading), "article-page");
269 290 }
270 291
292 + #[test]
293 + fn a_screen_with_nothing_of_its_own_carries_only_its_measure() {
294 + assert_eq!(body_class(Measure::Contained, &[]), "centered-page");
295 + }
296 +
297 + #[test]
298 + fn a_screens_own_token_lands_beside_its_measure_rather_than_instead_of_it() {
299 + // The exact string `templates/pages/feed.html` renders today, which is
300 + // the parity the next conversion has to hold.
301 + assert_eq!(
302 + body_class(Measure::Wide, &["feed-page"]),
303 + "padded-page feed-page"
304 + );
305 + assert_eq!(
306 + body_class(Measure::Wide, &["dashboard-page", "dashboard-user-page"]),
307 + "padded-page dashboard-page dashboard-user-page"
308 + );
309 + }
310 +
271 311 #[test]
272 312 fn no_template_still_writes_a_layout_class_by_hand() {
273 313 // The done-condition, checked rather than remembered: the layout axis
@@ -277,7 +317,7 @@
277 317 // The four standalone tokens are screen identity rather than measure
278 318 // and are deliberately left alone, so they are not looked for.
279 319 let mut offenders = Vec::new();
280 - for entry in walk("templates") {
320 + for entry in walk("templates", "html") {
281 321 let source = std::fs::read_to_string(&entry).expect("a template reads");
282 322 for (at, line) in source.lines().enumerate() {
283 323 if !line.contains("block body_attrs") {
@@ -297,8 +337,35 @@
297 337 assert!(offenders.is_empty(), "{offenders:?}");
298 338 }
299 339
300 - /// Every `.html` under a directory.
301 - fn walk(root: &str) -> Vec<std::path::PathBuf> {
340 + #[test]
341 + fn no_described_screen_writes_a_layout_class_by_hand() {
342 + // The screen-side twin of the template check above: a converted screen
343 + // states its measure and reads the class off it, so a literal reaching
344 + // `Document::classed` is a screen that will not follow `style.css` when
345 + // the mapping moves. The four standalone identity tokens and the embed
346 + // classes are screen identity rather than measure and are not looked
347 + // for.
348 + let mut offenders = Vec::new();
349 + for entry in walk("src", "rs") {
350 + // This file states the three strings once, which is the point of it.
351 + if entry.ends_with("shell.rs") {
352 + continue;
353 + }
354 + let source = std::fs::read_to_string(&entry).expect("a source file reads");
355 + for (at, line) in source.lines().enumerate() {
356 + let literal = ["padded-page", "centered-page", "article-page"]
357 + .iter()
358 + .any(|name| line.contains(name));
359 + if line.contains("classed(") && literal {
360 + offenders.push(format!("{}:{}", entry.display(), at + 1));
361 + }
362 + }
363 + }
364 + assert!(offenders.is_empty(), "{offenders:?}");
365 + }
366 +
367 + /// Every file with the given extension under a directory.
368 + fn walk(root: &str, wanted: &str) -> Vec<std::path::PathBuf> {
302 369 let mut found = Vec::new();
303 370 let mut stack = vec![std::path::PathBuf::from(root)];
304 371 while let Some(at) = stack.pop() {
@@ -309,7 +376,7 @@
309 376 let path = entry.path();
310 377 if path.is_dir() {
311 378 stack.push(path);
312 - } else if path.extension().is_some_and(|ext| ext == "html") {
379 + } else if path.extension().is_some_and(|ext| ext == wanted) {
313 380 found.push(path);
314 381 }
315 382 }
@@ -345,7 +412,7 @@
345 412 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
346 413 let mut linked = 0;
347 414 let mut sources = vec![head().to_string()];
348 - for entry in walk("templates") {
415 + for entry in walk("templates", "html") {
349 416 sources.push(std::fs::read_to_string(&entry).expect("a template reads"));
350 417 }
351 418 for source in &sources {
@@ -369,8 +369,10 @@
369 369 // shell is built once and `Arc`'d at adapter construction, so a class
370 370 // set there is a constant for every screen that adapter ever serves --
371 371 // which is right for one page and silently wrong for the second, and
372 - // the second is what the conversion is producing.
373 - .documented(Document::default().classed(crate::shell::measure(MEASURE)))
372 + // the second is what the conversion is producing. The empty slice is
373 + // `pricing.html` carrying nothing beside the measure, which is why this
374 + // screen needed no mapping at all.
375 + .documented(Document::default().classed(crate::shell::body_class(MEASURE, &[])))
374 376 // The whole of what `base.html` put in `<meta name="description">`,
375 377 // including the fee sentence, because this one string is now all three
376 378 // tags: the social pair and the plain one. It carried only the first
@@ -870,7 +872,7 @@
870 872
871 873 assert_eq!(
872 874 screen.document.body_class.as_deref(),
873 - Some(crate::shell::measure(MEASURE))
875 + Some(crate::shell::body_class(MEASURE, &[]).as_str())
874 876 );
875 877 assert_eq!(screen.document.body_class.as_deref(), Some("centered-page"));
876 878 }