//! 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_into;
use makeover_webview::form::{Filling, Markup, Value, escape_into, field_html_into};
// `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_into, part_class, push_column_classes,
};
use makeover_webview::meter::meter_html_into;
use makeover_webview::placeholder::placeholder_html_into;
use quasi_router::screen::{Act, Cell, Cells, Destination, Field, Node, Row, Slot, Tag};
use quasi_router::{Action, Method, Params};
/// Write one prefixed class name onto a buffer the caller already has.
///
/// The two halves are escaped separately rather than joined and escaped once.
/// That is the same bytes -- escaping is per character and has no context to
/// carry across the seam -- for none of the allocations. Every class of every
/// element went through a `format!` and then a second `String` before this
/// existed, which measured as most of the emitter's allocation count.
///
/// Escaped at all because a prefix is host configuration reaching an attribute
/// value. It is a `&'static str` and every real one is identity under this, so
/// the cost is a scan; what it buys is that the one string here that did not
/// come from this crate cannot end the attribute.
pub(crate) fn class_into(name: &str, opts: &Emit, out: &mut String) {
escape_into(opts.class_prefix, out);
escape_into(name, out);
}
/// 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(' ');
}
class_into(name, opts, out);
}
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('"');
}
/// How a picture sits in its box, where it is not the default.
///
/// `tone_attr`'s shape and for its reason: `Natural` is what an `` does
/// with no rule at all, so saying it would be a stylesheet hook that changes
/// nothing. The two that need a rule get one.
fn fit_attr(fit: layout::Fit, out: &mut String) {
let value = match fit {
layout::Fit::Natural => return,
layout::Fit::Cover => "cover",
layout::Fit::Contain => "contain",
// `Fit` is `#[non_exhaustive]`, so a member added upstream lands here
// rather than failing the build. Drawing it as natural is the safe
// read: the picture is whole and its own shape, which is wrong about
// the box and never wrong about the content.
_ => return,
};
out.push_str(" data-fit=\"");
out.push_str(value);
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("