//! The document head, emitted by quasi's renderer rather than by `base.html`.
//!
//! Wiki note `mnw-server-conversion-plan`, step S1. The dashboard is being
//! converted to described screens one at a time, and a converted screen renders
//! through [`quasi_webview::Shell`] while everything around it still renders
//! through Askama. Two heads written by two hands drift, and the drift is
//! silent: a layer statement that moves below a stylesheet, an htmx that loads
//! without the morph extension, a viewport that says one thing on half the
//! site. So the head is the renderer's on both paths from here, before any
//! screen is described.
//!
//! [`Shell::parts`] is what a host whose templating writes into the head takes:
//! Askama renders the per-page `{% block title %}` and `{% block head %}` in
//! place, inside `base.html`, and no caller ever holds them as a string.
//! `base.html` writes the title, ``, the `
` tag and the close;
//! everything above is here.
//!
//! One shell for the process, not one per request. Nothing in it varies by
//! viewer: the creator's theme block is per-request and unlayered on purpose,
//! and it stays where it is, injected by the three templates that show a
//! creator's work through `{% block head %}` so it lands after every sheet and
//! outranks every named layer.
use std::sync::OnceLock;
use quasi_webview::Shell;
/// The cache-busting suffix on the site's own assets.
///
/// A content hash of every watched static file, computed in `build.rs` and
/// handed over as an env var. It used to reach the head through a generated
/// `_head_assets.html`; the head is the renderer's now, so the version is all
/// that crosses. `_sheet.html` and `_island.html` are still generated with the
/// same hash, for the per-page sheets and islands this module does not see.
const V: &str = env!("STATIC_VERSION");
fn parts() -> &'static quasi_webview::Parts {
static PARTS: OnceLock = OnceLock::new();
PARTS.get_or_init(|| {
Shell::under("/static")
// Earlier in the list = lower priority, and `makeover` is prepended
// by the renderer. A layer's position is fixed where its name is
// FIRST seen, so without the statement the generated sheets would
// establish `makeover` simply by loading first and reordering two
// links would silently reorder the cascade. `components` is where
// the site's own sheets live, including the per-page wizard.css and
// media-player.css that arrive later through `{% block head %}` and
// that nothing here can order. `base` and `responsive` are still
// empty; declaring an empty layer costs nothing and fixes its
// position.
.layered(["base", "components", "responsive"])
// Whole value is being early: a preload discovered after the sheets
// it races bought nothing.
.with_head_first(
"\
",
)
.styled(format!("/static/geometry.css?v={V}"))
.styled(format!("/static/layout.css?v={V}"))
.styled(format!("/static/style.css?v={V}"))
// Last in the head, after htmx: the favicon has no order to keep,
// and neither script reads htmx at load. `upload.js` only defines
// `S3Uploader`, and the core module is deferred by being a module,
// so it still runs after the deferred htmx above it.
.with_head(format!(
"\
\
"
))
.parts()
})
}
/// `` through the head's contents, without ``.
///
/// Called from `base.html`, which appends the title and the per-page head and
/// closes the element.
pub fn head() -> &'static str {
&parts().head
}
/// The attributes the shell owns on ``, each one space-prefixed.
///
/// `base.html` writes ``, so a
/// page's own class attribute composes with these instead of replacing them.
pub fn body_attrs() -> &'static str {
&parts().body_attrs
}
pub use makeover_layout::Measure;
/// The body class for a screen's measure.
///
/// `0eccff0d`. 69 of 72 templates carried one of these three strings as a
/// literal, which made how wide a page runs a fact about the template rather
/// than about the screen. The strings are unchanged and the rules in
/// `style.css` are untouched: what moved is where the choice is written down,
/// from a class name in markup to a described property with a name in every
/// renderer's vocabulary.
///
/// The old names stay on the left-hand side of the rules because they are what
/// `style.css` matches, and renaming them is a separate change with no
/// description in it. `padded-page` is [`Measure::Wide`] because a padded page
/// is the full width with gutters, which is what the class always meant.
///
/// The four standalone tokens -- `health-page`, `purchase-page`, `buy-page`,
/// `stripe-disclaimer-page` -- are screen identity rather than measure, and are
/// deliberately not here.
#[must_use]
pub const fn measure(measure: Measure) -> &'static str {
match measure {
Measure::Contained => "centered-page",
Measure::Reading => "article-page",
// The default, and the arm a member added upstream lands in. A measure
// this server has not learned yet should render at the width every
// other page does rather than unstyled.
_ => "padded-page",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_layer_statement_precedes_every_stylesheet() {
// The property the hand-written `` existed to
// hold, now held by the renderer. It is the one that fails silently:
// the CSS stays valid and buttons and badges look subtly wrong.
let head = head();
let stmt = head
.find("@layer makeover, base, components, responsive;")
.expect("the order is stated");
for sheet in ["geometry.css", "layout.css", "style.css"] {
assert!(stmt < head.find(sheet).expect("the sheet is linked"));
}
}
#[test]
fn the_head_is_not_closed_and_carries_no_title() {
// Both are `base.html`'s, and emitting either here would produce a
// second one rather than an error.
assert!(!head().contains(""));
assert!(!head().contains(""));
assert!(head().starts_with(""));
}
#[test]
fn the_fonts_are_preloaded_before_the_sheets_that_race_them() {
let head = head();
assert!(head.find("Lato-Regular.woff2") < head.find("style.css"));
}
#[test]
fn the_body_attributes_start_with_a_space_so_a_page_can_add_its_own() {
// `base.html` writes them straight against ` Vec {
let mut found = Vec::new();
let mut stack = vec![std::path::PathBuf::from(root)];
while let Some(at) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&at) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "html") {
found.push(path);
}
}
}
found
}
#[test]
fn morph_is_loaded_wherever_a_swap_can_ask_for_it() {
// The shell writes `hx-ext="morph"` onto every page's body, so the
// extension has to be served. It is: `static/idiomorph-ext.min.js`.
assert!(head().contains("idiomorph-ext.min.js"));
assert!(
std::path::Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static/idiomorph-ext.min.js"
))
.exists()
);
}
}