//! Nodes to markup. //! //! Every function here takes a piece of [`quasi_router`]'s screen tree and //! pushes markup onto a buffer. Nothing returns a `Result`: a description that //! exists is renderable by construction, which is the property the owned mirror //! in `quasi-router` was built to have. //! //! # Where the htmx goes in //! //! In exactly one function, [`action_attrs`]. An [`Action`] is a method, a path //! and some params, and turning that into `hx-get` / `hx-post` / `hx-vals` is //! the whole of what "htmx is the transport" means in code. Nothing else in //! this file knows the word htmx, so decision 13's claim that the transport is //! replaceable is a claim about one function rather than about the crate. //! //! `hx-target` is emitted for one case and only one: [`Action::replaces`], set //! when a control calls a route the description layer does not serve. Decision 7 //! puts the target on the response, where `quasi-http` sets `HX-Retarget` from //! [`Response::Fragment`](quasi_router::Response::Fragment), because the router //! is the only party that knows what it just changed, and a control that also //! named a target would be a second party deciding one thing. That reasoning //! assumes the responder is described. A plain API route is not, cannot name a //! region, and leaves the answer to land wherever the transport defaults, which //! for htmx is inside the pressed button. See `Action::replaces` for the whole //! of it. use std::collections::HashMap; use std::fmt::Write as _; use makeover_layout as layout; // `Tone::token` is the trait method, and `data-tone` is spelled from it rather // than from a match here, so a tone added upstream cannot be named two ways. use makeover_layout::Intent as _; use makeover_webview::figure::figure_html; use makeover_webview::form::{Filling, Markup, Value, escape, field_html}; // `class`, `option_class` and the two part-class mappings below are makeover's, // not copies of it. They were copies until makeover-webview 0.27.0 made them // public: the prefix helper was byte-identical, and the row and cell part names // were a second spelling of a list whose own doc comment carries an obligation // to be grepped on upgrade. A second spelling is a second place to forget, and // the selector names had already drifted. use makeover_webview::{Emit, class, option_class}; // `Cell` is a name both crates use: makeover's is the emitted table cell, ours // is the described one. Aliased rather than qualified at the call site, so the // two never read as the same type. use makeover_webview::list::{ Cell as Emitted, cell_part_class, cells_html, column_classes, part_class, }; use makeover_webview::meter::meter_html; use makeover_webview::placeholder::placeholder_html; use quasi_router::screen::{Act, Cell, Cells, Destination, Field, Node, Row, Slot, Tag}; use quasi_router::{Action, Method, Params}; /// Write a `class="..."` attribute, prefixed. fn class_attr(names: &[&str], opts: &Emit, out: &mut String) { out.push_str(" class=\""); for (i, name) in names.iter().enumerate() { if i > 0 { out.push(' '); } out.push_str(&escape(&class(name, opts))); } out.push('"'); } /// Write the attribute naming a tone, if the tone is worth naming. /// /// [`Tone::Neutral`] writes nothing: ordinary content is the default, and an /// attribute meaning "nothing unusual" is an attribute on every element in the /// document. /// /// An attribute and not a class, which is the correction. This emitted /// `tone-info`, `tone-success`, `tone-warning` and `tone-danger` as classes, and /// makeover has never defined one of them: its whole vocabulary keys tone off /// `data-tone`, from `.badge[data-tone="danger"]` to the progress fill to a /// figure's value. So every toned thing a description produced arrived with a /// class no stylesheet in the tree had heard of, which is why the SSH-keys tab's /// Remove button came out the same colour as everything else. fn tone_attr(tone: layout::Tone, out: &mut String) { if matches!(tone, layout::Tone::Neutral) { return; } out.push_str(" data-tone=\""); out.push_str(tone.token()); out.push('"'); } /// What goes back in the box, for a form being offered again after a refusal. /// /// `1c4a66a4`. The description carries the value as a string, because that is /// what came off the wire; the kind is what says how to read it. A checkbox is /// carried by presence the way HTML submits one, so any value means ticked and /// nothing means not. /// /// A [`layout::FieldKind::Secret`] is emitted empty whatever it holds. That is /// the third refusal of the same thing and none of the three is redundant: /// `Field::value` will not store one, `makeover_webview::form` will not write /// one into an ``, and this one stands between them /// because `Field::value` is a public field that a struct literal reaches past. fn refill(field: &Field) -> Value<'_> { if field.kind == layout::FieldKind::Secret { return Value::Absent; } match field.value.as_deref() { None => Value::Absent, Some(_) if field.kind == layout::FieldKind::Checkbox => Value::On(true), Some(value) => Value::Text(value), } } /// One field, with whatever `1c4a66a4` and `14612ed8` added around it. /// /// The field's own markup is makeover-webview's, unchanged. A second field /// emitter here is the divergence phase A existed to end, and it would be the /// same anatomy with a different escaping story. /// /// A [`Field::changes`] is a wrapper rather than attributes on the control, /// because the control is emitted by makeover-webview and there is no seam to /// put them through. That turns out to be the better shape anyway: the `change` /// event bubbles, so one element around the group catches it whichever of the /// input, select or textarea forms the field took, and `hx-include` finds the /// control back without this having to know which it was. fn field_group_html(field: &Field, morphs: bool, opts: &Emit, out: &mut String) { let filling = Filling::of(refill(field)); let writes = field.changes.as_ref(); if let Some(action) = writes { out.push_str("