//! The document around a screen. //! //! The renderer owns the document and the host injects what it knows. The //! alternative was each adapter supplying its own `` and the renderer //! filling the inside, which is honest about axum and Tauri resolving assets //! differently and costs two heads to keep in step — the divergence the stack //! exists to end. A served page and a custom-protocol page differ in where //! their assets live and in nothing else, so where assets live is the //! parameter and the rest is emitted once. //! //! What the host owes is small enough to list: four asset URLs, a language, //! and whatever else belongs in its own head. Everything with an opinion in it //! — the viewport, where the body's classes come from — is here, because a host that //! could get those wrong is a host that can diverge. //! //! The transport is htmx 4 (see [`quasi_http::htmx`]), which is what took two //! things out of this file: there is no extension to register for morphing, //! since `outerMorph` is a swap style 4 ships, and there is no //! `responseHandling` meta tag to emit, since 4 swaps every status but 204 and //! 304 on its own. use makeover_webview::form::escape_into; use quasi_router::{Chrome, Discovery, Document}; /// The parts of a document only the host knows. /// /// A [`Default`] shell is a valid one: the asset paths are what a server /// mounting its static directory at `/static` already serves, which is what /// both MNW and multithreaded do today, and the Tauri adapter overrides them /// with its own scheme. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Shell { /// The document language, for ``. pub lang: String, /// Where htmx is served from. /// /// `None` drops the script, for a document that calls no route. The five /// MNW embeds are the measured case: an embed is an iframe on a third /// party's page, and it contains not one `hx-` attribute. Shipping htmx to /// it is a script the reader downloads and runs to do nothing. /// /// There is nothing to get quietly wrong here. Dropping htmx from a /// document that never asked it for anything changes nothing a reader can /// see, and a document that does carry `hx-` attributes and drops this /// stops working visibly, on the first control pressed, which is the /// failure that reports itself. /// /// What is served has to be htmx 4. The emitted markup is 4's -- the /// `outerMorph` swap, `hx-disable` for the lock, `hx-headers:inherited` on /// the body -- and 2.x reads none of it, so pointing this at a 2.x bundle /// is a page whose controls fetch and whose swaps destroy state. pub htmx_src: Option, /// Where this crate's selection script is served from. /// /// [`SELECTION_JS`](crate::SELECTION_JS) is its source, shipped by this /// crate rather than written per app: what it does is renderer behaviour, /// the same way the terminal's keys are, and every app hand-rolling it is /// the plumbing this stack exists to delete. /// /// `None` drops it, and the fallback is safe and visible rather than /// silent: a commit control stays enabled and says no count: there is /// nothing here to get quietly wrong. /// /// # What it is for /// /// The two things a bulk bar does that no description can say. The ticks on /// a screen are the host's until something submits them, so a route /// answering from the store cannot know how many are set, and a commit /// control over an empty selection is otherwise offered, pressed, and /// answers "0 tasks completed". Both are the browser's to know, and this is /// the browser saying them. The terminal and egui renderers hold the set in /// their own `View` and say the same two things in Rust. pub selection_src: Option, /// Where _hyperscript is served from. /// /// The runtime for the programs this crate emits. A region that shows one /// child at a time carries the programs that move between its frames, and /// without the runtime they are unknown attributes a browser ignores. /// /// `None` drops it. What is lost is the local chrome and nothing else: the /// frames are all in the document, the first one is showing, and a strip /// button does what it did before this crate emitted a program, which is /// whatever the host binds to `data-shows`. Visible and safe, the same /// shape as the other two scripts' absence. /// /// It has to be the real hyperscript rather than a build with the `js` /// feature stripped or added: nothing emitted here uses `js`, which is the /// adoption's one rule, and it is also the only path in the interpreter /// that would need `unsafe-eval` from a CSP. pub hyperscript_src: Option, /// Where this crate's clock script is served from. /// /// [`CLOCK_JS`](crate::CLOCK_JS) is its source, shipped by this crate for /// [`selection_src`](Self::selection_src)'s reason: keeping a readout /// current is renderer behaviour, and every app writing it again is the /// plumbing this stack exists to delete. goingson's copy is 564 lines. /// /// It also takes a toast off the page once its time is up, which is the /// other thing on a page that happens because time passed. /// /// `None` drops it, and what is lost is the ticking rather than the /// readout: the words the server emitted stay on the page and stop moving, /// which is what every server-rendered timestamp in the tree already does. /// A toast stays up instead of dismissing itself, which is the same shape /// of loss: what the server said is there and nothing moves it. Visible and /// safe, like the selection script's absence. /// /// Kept by [`without_htmx`](Self::without_htmx), which drops the selection /// script. The difference is what each one needs: a bulk bar is a set of /// ticks submitted to a route, so a document that calls none has no use for /// one, while a page with no routes at all can still say how long ago /// something happened. pub clock_src: Option, /// Where this crate's download script is served from. /// /// [`DOWNLOAD_JS`](crate::DOWNLOAD_JS) is its source. `67881a88`: a route /// can answer [`Outcome::File`](quasi_router::Outcome::File), and on this /// host the http adapter writes `Content-Disposition`, which is the whole story /// for a plain link and is not enough for a control that reaches its /// route through htmx. htmx makes an XHR, an XHR is not a navigation, and a /// browser saves nothing: the file is swapped into the page as text. This /// script cancels that swap and hands the body to the browser itself. /// /// `None` drops it, and this is the one script whose absence is neither /// safe nor quiet, which is why it says so here rather than claiming the /// shape of the other four: a page that drops it and then presses an /// export control gets the file's contents swapped into a region. Drop it /// only on a document with no route that answers a file: a page whose /// downloads are plain links has no use for it, and that is the case it is /// [`None`] for. pub download_src: Option, /// Where this crate's fill script is served from. /// /// [`FILL_JS`](crate::FILL_JS) is its source. `f35aafee`: an /// [`Act::fills`](quasi_router::Act::fills) names a field on the same /// screen that receives the act's value, and where inside that field the /// value lands is the renderer's. Here it is the selection, which is what /// the three MNW surfaces this was measured on need — a server-side append /// costs up to thirty seconds of typing on one of them and the whole /// unsaved draft on the other two. /// /// `None` drops it, and the loss reports itself on the first press: the /// control runs its own action and writes nothing, which is what a picker /// did before the member existed. Drop it on a document with no act that /// fills. /// /// Independent of htmx, so [`without_htmx`](Self::without_htmx) keeps it: /// what it reads is two attributes on a control, and a document that calls /// no route can still deposit a value in a box. pub fill_src: Option, /// Where this crate's reveal script is served from. /// /// [`REVEAL_JS`](crate::REVEAL_JS) is its source. `079a011e`: a region can /// say which control and which value bring it out, and evaluating that is /// local work with no request in it. This file is what does it here. /// /// `None` drops it, and the loss is visible on the first screen that has a /// conditional region: every one of them is out, whatever its control /// holds. Drop it on a document with no region that names a condition. /// /// Independent of htmx, so [`without_htmx`](Self::without_htmx) keeps it: /// what it reads is three attributes on a region and the value of a control /// beside it, and a document that calls no route can still have a form with /// a section that only sometimes applies. pub reveal_src: Option, /// Where this crate's clipboard script is served from. /// /// [`COPY_JS`](crate::COPY_JS) is its source. `c3e145e0`: an /// [`Act::copies`](quasi_router::Act::copies) says a press puts a value on /// the clipboard, which is local work with no request in it, and this file /// is what does it here. /// /// `None` drops it, and the loss is total rather than partial: a copy /// control does nothing at all when pressed. There is no markup fallback to /// degrade to, because no browser copies without a script. Drop it on a /// document with no act that copies. /// /// Independent of htmx, so [`without_htmx`](Self::without_htmx) keeps it: /// what it reads is one attribute on a control, and a document that calls /// no route can still hand a value to the clipboard. pub copy_src: Option, /// Where this crate's repeat script is served from. /// /// [`REPEAT_JS`](crate::REPEAT_JS) is its source. `60d1753c`: a question /// answered N times lets the reader add and remove the slots, and doing /// that is local work with a `template` already in the document. /// /// `None` drops it, and the loss is visible on the first form with a /// repeating question: the slots the description offered are there and the /// add and remove controls do nothing. Drop it on a document with no /// question that repeats. /// /// Independent of htmx, so [`without_htmx`](Self::without_htmx) keeps it: /// what it reads is four attributes on a fieldset, and adding a slot is a /// clone of markup that is already on the page. pub repeat_src: Option, /// Where this crate's awaiting script is served from. /// /// [`AWAITING_JS`](crate::AWAITING_JS) is its source. `d43ea1c5`: the node /// emitter says which calls wait and `makeover-webview` draws what a wait /// looks like, and neither of them knows when one is running. This file /// puts `aria-busy` on the element making the request, which is what the /// design system keys the drawing off, and sets `--awaiting-share` from /// bytes the browser reports delivered. /// /// `None` drops it, and the loss is a page where nothing marks a wait. /// Every control still locks itself through `hx-disable`: less decoration, /// never less behaviour. /// /// **Not** independent of htmx, unlike [`fill_src`](Self::fill_src), /// [`reveal_src`](Self::reveal_src) and [`repeat_src`](Self::repeat_src). /// What it listens to is the request lifecycle, so /// [`without_htmx`](Self::without_htmx) drops it with the rest. pub awaiting_src: Option, /// Where this crate's instant script is served from. /// /// [`INSTANT_JS`](crate::INSTANT_JS) is its source. /// [`makeover_layout::Field::as_instant`] is a description saying a /// wall-clock value is submitted as the moment it names and declining to /// say how; the browser is the only party that knows what "your computer's /// time zone" means, so how is this file. `makeover-webview` marks the /// input and this rewrites the value on the way out. /// /// `None` drops it, and the loss is a form submitting a local wall-clock /// string to a route expecting an instant. That is a wrong value rather /// than a missing affordance, so a host with a described datetime field /// that asks for one wants this served. /// /// **Not** independent of htmx, for [`awaiting_src`](Self::awaiting_src)'s /// reason: what it listens to is the request lifecycle, so /// [`without_htmx`](Self::without_htmx) drops it with the rest. pub instant_src: Option, /// Where the focus script is served from, or `None` to leave it out. /// /// htmx restores focus around the *main* swap target and only that one, so /// a focused control inside an out-of-band region is destroyed outside /// that window and focus falls to `body`. This renderer emits out-of-band /// swaps, so it owns the gap. /// /// **Not** independent of htmx, for [`awaiting_src`](Self::awaiting_src)'s /// reason: it reads the request lifecycle. /// /// `None` drops it, and the loss falls entirely on readers navigating by /// keyboard: after a response that updates a second region, the caret is /// gone and the next Tab starts from the top of the document. Silent to /// everyone else, which is the reason it is on by default. pub focus_src: Option, /// Where this crate's menu script is served from. /// /// [`MENU_JS`](crate::MENU_JS) is its source. /// [`makeover_layout::Fallback::Menu`] asks for the members that no longer /// fit to move into one overflow control, which needs a measurement CSS /// cannot make; this is the measuring. /// /// Independent of htmx, like [`fill_src`](Self::fill_src): it measures on /// resize and on load, and takes a settle as one more occasion when there /// is one. [`without_htmx`](Self::without_htmx) keeps it. /// /// `None` drops it, and the loss is a menu run that wraps instead of /// folding. Every member stays reachable, so this is less rearrangement and /// never less behaviour, which is the direction the rest of this renderer /// degrades in. pub menu_src: Option, /// Where [`crate::OUTLINE_JS`] is served from, or `None` not to ship it. /// /// A described hierarchy is a flat list of rows carrying their depth; the /// emitter hides what a shut branch covers, and this is the press that /// opens it again. /// /// Independent of htmx, like [`menu_src`](Self::menu_src): one delegated /// listener on the document, so [`without_htmx`](Self::without_htmx) keeps /// it and a list arriving in a swap is covered. /// /// `None` drops it, and the loss is an outline stuck in the shape the /// description gave it, with every row still reachable. Less rearrangement /// and never less content, which is the direction the rest of this renderer /// degrades in. pub outline_src: Option, /// Stylesheets, in link order. pub stylesheets: Vec, /// The app's own cascade layer names, in priority order, lowest first. /// /// The renderer always emits the layer statement, with `makeover` first and /// these after it, before any stylesheet link: /// /// ```css /// @layer makeover, base, components, responsive; /// ``` /// /// It is emitted even when this is empty, because the point is not the /// app's names but `makeover`'s position. A layer's place in the cascade is /// fixed where its name is FIRST seen, so with no statement the generated /// stylesheets establish `makeover` simply by loading first, and reordering /// two link tags silently reorders the cascade. /// /// This is a field rather than something the host writes into /// [`head_first`](Self::head_first) because it is exactly the class of /// thing the module header says belongs here: a host that could get it /// wrong is a host that can diverge, and getting it wrong is silent. The /// CSS stays valid, the minifier stays happy, and buttons and badges look /// subtly wrong. Only the app's own names are the host's to supply, since /// the renderer cannot know them. /// /// Names are filtered to CSS identifier characters. A name is markup inside /// a `"); } /// The screen's own attributes on the root element. /// /// A free function rather than a method, because it is the screen's statement /// rather than the shell's: nothing on a [`Shell`] takes part in it, and a /// method would read as though the two composed the way the body classes do. /// /// A name [`writable_root_attr`](quasi_router::writable_root_attr) refuses is /// dropped and the rest of the document is still emitted, which is the member's /// own rule. So is a repeat: the first statement about a name wins, and a /// second is a description arguing with itself rather than a reason to emit two /// attributes a parser would halve. fn push_root_attrs(document: Option<&Document>, out: &mut String) { let Some(document) = document else { return; }; let mut written: Vec<&str> = Vec::new(); for (name, value) in &document.root { if !quasi_router::writable_root_attr(name) || written.contains(&name.as_str()) { continue; } written.push(name); out.push(' '); // The name is not escaped and does not need to be: `writable_root_attr` // has already refused everything escaping would have had to catch, // which is why the gate is a gate rather than an escape. out.push_str(name); out.push_str("=\""); escape_into(value, out); out.push('"'); } } impl Default for Shell { fn default() -> Self { Self { lang: "en".into(), htmx_src: Some("/static/htmx.min.js".into()), selection_src: Some("/static/quasi-selection.js".into()), hyperscript_src: Some("/static/_hyperscript.min.js".into()), clock_src: Some("/static/quasi-clock.js".into()), download_src: Some("/static/quasi-download.js".into()), fill_src: Some("/static/quasi-fill.js".into()), reveal_src: Some("/static/quasi-reveal.js".into()), copy_src: Some("/static/quasi-copy.js".into()), repeat_src: Some("/static/quasi-repeat.js".into()), awaiting_src: Some("/static/quasi-awaiting.js".into()), instant_src: Some("/static/quasi-instant.js".into()), focus_src: Some("/static/quasi-focus.js".into()), menu_src: Some("/static/quasi-menu.js".into()), outline_src: Some("/static/quasi-outline.js".into()), stylesheets: Vec::new(), app_layers: Vec::new(), head_first: None, head: None, sends: Vec::new(), body_first: None, body_last: None, body_class: None, chrome: Chrome::new(), } } } impl Shell { /// A shell serving its assets from under this prefix. /// /// The common case said once: `Shell::under("/assets")` rather than a path /// per script written out, each of which could disagree with the others. #[must_use] pub fn under(prefix: &str) -> Self { let prefix = prefix.trim_end_matches('/'); Self { htmx_src: Some(format!("{prefix}/htmx.min.js")), selection_src: Some(format!("{prefix}/quasi-selection.js")), hyperscript_src: Some(format!("{prefix}/_hyperscript.min.js")), clock_src: Some(format!("{prefix}/quasi-clock.js")), download_src: Some(format!("{prefix}/quasi-download.js")), fill_src: Some(format!("{prefix}/quasi-fill.js")), reveal_src: Some(format!("{prefix}/quasi-reveal.js")), copy_src: Some(format!("{prefix}/quasi-copy.js")), repeat_src: Some(format!("{prefix}/quasi-repeat.js")), awaiting_src: Some(format!("{prefix}/quasi-awaiting.js")), instant_src: Some(format!("{prefix}/quasi-instant.js")), focus_src: Some(format!("{prefix}/quasi-focus.js")), menu_src: Some(format!("{prefix}/quasi-menu.js")), outline_src: Some(format!("{prefix}/quasi-outline.js")), ..Self::default() } } /// Declare what the app offers from every screen. #[must_use] pub fn with_chrome(mut self, chrome: Chrome) -> Self { self.chrome = chrome; self } /// Add a stylesheet, chaining. #[must_use] pub fn styled(mut self, href: impl Into) -> Self { self.stylesheets.push(href.into()); self } /// Name the app's cascade layers, in priority order, lowest first. /// /// `makeover` is not named here; the renderer puts it first on its own. #[must_use] pub fn layered(mut self, names: I) -> Self where I: IntoIterator, S: Into, { self.app_layers = names.into_iter().map(Into::into).collect(); self } /// Declare a header every request from this document carries, chaining. /// /// Repeated calls add headers rather than replacing them, and a name given /// twice is emitted twice: this writes what it is told, and deduplicating /// would be the renderer deciding which of two values the host meant. #[must_use] pub fn sending(mut self, name: impl Into, value: impl Into) -> Self { self.sends.push((name.into(), value.into())); self } /// Prepend markup to the head, chaining. Not escaped. /// /// Repeated calls append to each other, so the emitted order matches the /// call order, the way [`with_head`](Self::with_head) already does. #[must_use] pub fn with_head_first(mut self, markup: impl Into) -> Self { self.head_first = Some(match self.head_first.take() { Some(existing) => format!("{existing}{}", markup.into()), None => markup.into(), }); self } /// Append markup to the head, chaining. Not escaped. #[must_use] pub fn with_head(mut self, markup: impl Into) -> Self { self.head = Some(match self.head.take() { Some(existing) => format!("{existing}{}", markup.into()), None => markup.into(), }); self } /// Markup to append to the end of the body, verbatim. /// /// See [`body_last`](Self::body_last). The mirror of /// [`with_head`](Self::with_head), for the tail of a template a host has /// not described yet. #[must_use] pub fn with_body_last(mut self, markup: impl Into) -> Self { self.body_last = Some(markup.into()); self } /// Markup to open the body with, verbatim. /// /// See [`body_first`](Self::body_first). The mirror of /// [`with_head_first`](Self::with_head_first), for what has to precede /// everything the screen draws. #[must_use] pub fn with_body_first(mut self, markup: impl Into) -> Self { self.body_first = Some(markup.into()); self } /// Run with no script of any kind, chaining. /// /// For a document served under a CSP that permits none: MNW's custom pages /// are creator-authored markup on a cookieless host under `default-src /// 'none'`, so a `"); } // After htmx, because it listens for htmx's own settle event and a // deferred script that loads first would bind before the event exists // to be dispatched. if let Some(src) = &self.selection_src { out.push_str(""); } if let Some(src) = &self.clock_src { out.push_str(""); } // After htmx, for the selection script's reason: it binds an htmx // event, and a deferred script that ran first would bind before htmx // existed to dispatch one. if let Some(src) = &self.download_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: it listens on the // document for a press on a control the node emitter marked, so markup // arriving in a swap is covered without this file knowing htmx exists. if let Some(src) = &self.fill_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: it reads three // attributes on a region and the control they name, and re-reads them // after a swap, so markup arriving mid-page is covered. if let Some(src) = &self.reveal_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: it listens on the // document for a press on a control carrying the value, so a control // arriving in a swap is covered without this file knowing htmx exists. if let Some(src) = &self.copy_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: it listens on the // document for a press on the add or remove control of a group the node // emitter marked, and the blank slot it clones is markup already in the // page, so a group arriving in a swap is covered. if let Some(src) = &self.repeat_src { out.push_str(""); } // Deferred like the rest, and delegated on the document like the rest, // so a control arriving in a swap is covered. Unlike the rest it is not // independent of htmx: what it listens to is the request lifecycle. if let Some(src) = &self.awaiting_src { out.push_str(""); } // Deferred like the rest, delegated on the document like the awaiting // script, and not independent of htmx for the same reason: it reads the // request lifecycle. Nothing about it is per-region, so one listener // pair covers every out-of-band swap the renderer will ever emit. if let Some(src) = &self.focus_src { out.push_str(""); } // Deferred like the rest, delegated on the body like the awaiting // script, and not independent of htmx for the same reason: it rewrites // the request htmx is about to make. if let Some(src) = &self.instant_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: what it reads is the // room a run has, which a document with no routes has just as much of. if let Some(src) = &self.menu_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: one delegated // listener on the document for a press on a branch's chevron, so a list // arriving in a swap is covered without this file knowing htmx exists. if let Some(src) = &self.outline_src { out.push_str(""); } // Deferred like the rest, and independent of htmx: what it reads is the // programs the node emitter wrote into the markup, and a document that // calls no route can still carry a carousel. It initialises on // `DOMContentLoaded` and watches the document after that, so markup // arriving in a swap is processed without this file knowing htmx exists. if let Some(src) = &self.hyperscript_src { out.push_str(""); } if let Some(head) = &self.head { out.push_str(head); } // Last in the head, and outside the layer statement above: an unlayered // rule beats a layered one whatever the specificity, which is what makes // the screen's own sheet win over everything the shell shares without it // having to name a layer it cannot know. `48a6e9e5`. if let Some(css) = document.and_then(|document| document.style.as_deref()) { push_style(css, out); } } /// What a link preview and a crawler read, from the screen itself. /// /// Every value is escaped. These are user-authored strings — an item /// description, a bio — going into attribute values, and this is the one /// place in the head where that is true. Same `escape` the node emitter /// uses; a second one here would be a second thing to get wrong. /// /// A `None` emits nothing at all. An empty `og:description` is worse than /// no tag: a preview showing a blank line reads as a broken page rather /// than as a page that said nothing. /// /// The Twitter tags mirror the OG ones, which is what the server's 28 /// templates do by hand today. fn discovery_head(title: Option<&str>, discovery: &Discovery, out: &mut String) { let mut meta = |property: &str, content: &str| { out.push_str(""); }; if let Some(title) = title { meta("og:title", title); } if let Some(summary) = &discovery.summary { meta("og:description", summary); } if let Some(image) = &discovery.image { meta("og:image", image); } meta("og:type", discovery.kind.as_str()); if let Some(url) = &discovery.canonical { meta("og:url", url); } // `name`, not `property`: the Twitter tags were never part of RDFa, and // a card written with `property` is a card the crawler skips. let mut named = |name: &str, content: &str| { out.push_str(""); }; named( "twitter:card", if discovery.image.is_some() { "summary_large_image" } else { "summary" }, ); if let Some(title) = title { named("twitter:title", title); } if let Some(summary) = &discovery.summary { named("twitter:description", summary); } if let Some(image) = &discovery.image { named("twitter:image", image); } // The plain one, which is what a search engine reads. The pair above is // what a share sheet reads, and `base.html` emitted all three off one // string; a described document emitted two and left the host to append // the third through `Shell::head`, which is an escape hatch spending // itself on something the description already carries. // // Under `indexable`, unlike the social pair, and the asymmetry is the // point: a screen that has told crawlers not to index it has nothing to // gain from describing itself to them, while a noindex page can still // be linked and still owes a share sheet a preview. // // Measured 2026-08-29 before choosing the gate: MNW's five embeds -- an // iframe widget is the case where a search description would be wrong, // since it would compete with the page framing it -- call `summarised` // nowhere, so they emit nothing either way. The gate is what keeps that // true when one of them starts. if discovery.indexable && let Some(summary) = &discovery.summary { named("description", summary); } if !discovery.indexable { named("robots", "noindex"); } // The canonical link, beside `og:url` rather than instead of it: one is // what a crawler dedupes on and the other is what a share sheet shows, // and the six purchased-content screens need both to agree. if let Some(url) = &discovery.canonical { out.push_str(""); } // Autodiscovery. This is what a browser and every reader look for, and // it is the one tag here a host wrote by hand at each of its three // sites; the media type comes off `FeedKind` rather than out of a // template, so `application/rss` cannot be written once and skipped // everywhere. // // The title is escaped like everything else here: it is the app's own // string going into an attribute value, and the rule in this function // is that none of them is trusted for being ours. if let Some(feed) = &discovery.feed { out.push_str(""); } } /// The attributes the shell owns on ``, each one space-prefixed so /// they compose with whatever else the host puts on the tag. /// /// [`Parts`] wants these as a value and the emitted document does not, so /// the buffer-writing form is the one with the code in it. On the described /// path this was a `String` per render for two attributes, one of which is /// a constant. fn push_body_attrs(&self, document: Option<&Document>, out: &mut String) { if !self.sends.is_empty() { // `:inherited` and not a bare `hx-headers`: htmx 4 inherits nothing // unless the attribute says so, and the point of putting these on // the body is that a control cannot be emitted without them. The // value is JSON inside an attribute, escaped by the same function // `hx-vals` uses: a token is opaque bytes and this is the one place // a shell puts host-supplied text into markup. out.push_str(" hx-headers:inherited=\""); crate::node::json_pairs_attr( self.sends.iter().map(|(n, v)| (n.as_str(), v.as_str())), out, ); out.push('"'); } // One attribute holding both halves, because two `class` attributes on // one tag is markup a parser drops half of. The shell's part is what is // true of every page and the screen's is what is true of this one, so // the shell's goes first: a stylesheet reading them in order reads the // general before the specific. let screen_class = document.and_then(|document| document.body_class.as_deref()); if self.body_class.is_some() || screen_class.is_some() { out.push_str(" class=\""); if let Some(class) = &self.body_class { escape_into(class, out); if screen_class.is_some() { out.push(' '); } } if let Some(class) = screen_class { escape_into(class, out); } out.push('"'); } } /// The same attributes as a value, for a host writing the `` tag. /// /// No screen reaches here: [`parts`](Self::parts) is for a host assembling /// its own document, and that host writes the `` tag itself. fn body_attrs(&self) -> String { let mut out = String::new(); self.push_body_attrs(None, &mut out); out } /// The close of what [`open`](Self::open) opened. pub(crate) fn close(&self, out: &mut String) { // Before the close and after everything else, which is the whole of // what `body_last` promises. if let Some(last) = &self.body_last { out.push_str(last); } out.push_str(""); } } /// The shell's half of a host-assembled document. See [`Shell::parts`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Parts { /// `` through the head's contents, without ``. The host /// appends its own head markup and closes the element. pub head: String, /// The attributes the shell owns on ``, space-prefixed, for a host /// writing the tag itself: ``. pub body_attrs: String, /// The chrome's markup, for the end of the body the host is writing. /// /// Empty when the app declares no chrome, which is why this does not make /// the chrome unconditional: a host assembling its own document appends /// this before closing the body, and appending an empty string is what a /// document with no chrome already does. pub body_chrome: String, }