makeover-webview 0.59.1

The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser.
Documentation
//! A render of everything this crate can emit, scraped for class names.
//!
//! [`vocabulary::names`](crate::vocabulary::names) claims to hold every class
//! this crate can put in markup, and until 0.59.0 nothing checked the claim. It
//! was wrong by a whole family: `quasi-webview`'s own corpus guard found
//! `cell-fill`, `form-group` and `form-label` coming out in rendered documents
//! and had to carry them in a `MAKEOVER_UNLISTED` constant of its own, because
//! an app checking its stylesheet against our set alone concludes that its
//! rules for them are dead and deletes live styling.
//!
//! What that guard recorded is also why this is a render and not a scan of the
//! emitters: a careful read of `quasi-webview`'s emitters produced 35 names and
//! its corpus found 14 more. A class assembled at runtime -- a width class, a
//! drop class, a state appended to an attribute already open -- is a literal
//! nowhere in this source, and that is the shape of every name that was
//! missing here.
//!
//! # What it is not
//!
//! Not a rendering test. Nothing here asserts what an emitter produced, only
//! which classes came out, so it stays quiet when markup changes and speaks
//! when the vocabulary does.

use crate::form::{Filling, Markup, Value, field_html};
use crate::list::{Cell, cells_html};
use crate::{Emit, facet::facet_html, figure::figures_html};
use crate::{meter::meter_html, placeholder::placeholder_html};
use makeover_layout::{
    Accepted, CellPart, Choice, Column, Facet, FacetValue, Field, FieldKind, Figure, Meter,
    Priority, Readiness, Selecting, Sort, Standing, Tone, Width,
};
use std::collections::BTreeSet;

/// Every class the corpus puts in a document, unprefixed.
///
/// Scraped from `class="..."` rather than predicted, which is the point.
pub(crate) fn emitted() -> BTreeSet<String> {
    emitted_with(&Emit::default())
}

/// [`emitted`], with the emit options a host would set.
fn emitted_with(opts: &Emit) -> BTreeSet<String> {
    let mut found = BTreeSet::new();
    for html in documents(opts) {
        let mut rest = html.as_str();
        while let Some(at) = rest.find("class=\"") {
            rest = &rest[at + "class=\"".len()..];
            let end = rest.find('"').expect("an attribute closes");
            for name in rest[..end].split_whitespace() {
                found.insert(name.to_owned());
            }
            rest = &rest[end..];
        }
    }
    found
}

/// One document per emitter, over every input that changes what it writes.
///
/// Every markup emitter this crate has is called here. A new one that is not
/// added is the one hole this guard has, which is why the list is short enough
/// to read: `placeholder`, `form`, `figure`, `facet`, `meter` and `list` are
/// the whole of what emits markup, and `lib.rs` writes rules rather than
/// documents.
fn documents(opts: &Emit) -> Vec<String> {
    let mut out = vec![placeholders(opts), fields(opts), rows(opts)];
    out.push(figures_html(
        &[
            Figure::new("42", "Tasks"),
            Figure::new("12.5%", "Growth")
                .change("+3")
                .tone(Tone::Success),
        ],
        opts,
    ));
    for mode in [
        Selecting::OneOf,
        Selecting::AnyOf,
        Selecting::Range,
        Selecting::Text,
        Selecting::Subtree,
    ] {
        let values = [
            FacetValue::new("music", "Music")
                .standing(Standing::Taken)
                .counted(128)
                .at(0, true),
            FacetValue::new("music/synths", "Synths")
                .standing(Standing::Inherited)
                .at(1, false),
            FacetValue::new("music/drums", "Drums")
                .standing(Standing::Pruned)
                .at(1, false),
            FacetValue::new("talk", "Talk").at(0, false),
        ];
        out.push(facet_html(&Facet::new("Tag", mode, &values), opts));
    }
    // Toned, untoned, and over its total, which is the one state that adds an
    // attribute of its own.
    for meter in [
        Meter::new(3, 7),
        Meter::new(3, 7).tone(Tone::Success).label("subtasks"),
        Meter::new(9, 7).tone(Tone::Danger),
    ] {
        out.push(meter_html(&meter, opts));
    }
    out
}

/// Every readiness, with and without the action a stand-in can carry.
fn placeholders(opts: &Emit) -> String {
    let mut html = String::new();
    for state in [
        Readiness::Ready,
        Readiness::Pending,
        Readiness::Empty,
        Readiness::Failed,
    ] {
        html.push_str(&placeholder_html(state, "Nothing here", None, opts));
        html.push_str(&placeholder_html(
            state,
            "Nothing here",
            Some(Markup("<button>Add one</button>")),
            opts,
        ));
    }
    html
}

/// Every field kind, twice: plain, and carrying everything a group can hold.
///
/// The second pass is where the vocabulary lives. A hint, a unit and an error
/// each add a class, and an error adds two -- one on the message and one on the
/// group, which is `Field::invalid`'s own reasoning about a renderer that
/// cannot find the group from the message.
fn fields(opts: &Emit) -> String {
    const OPTIONS: &[Choice<'_>] = &[
        Choice::new("a", "The first"),
        Choice::new("b", "The second").unless("Not while the first is running"),
    ];
    const ACCEPT: &[Accepted<'_>] = &[Accepted::Type("image/png"), Accepted::Suffix(".zip")];

    let mut html = String::new();
    for kind in [
        FieldKind::Text,
        FieldKind::Secret,
        FieldKind::Number,
        FieldKind::Range,
        FieldKind::Interval,
        FieldKind::Email,
        FieldKind::Url,
        FieldKind::Tel,
        FieldKind::Date,
        FieldKind::DateTime,
        FieldKind::Textarea,
        FieldKind::Rich,
        FieldKind::Select,
        FieldKind::Radio,
        FieldKind::Checkbox,
        FieldKind::File,
        FieldKind::Hidden,
    ] {
        let plain = Field {
            options: OPTIONS,
            accept: ACCEPT,
            upper_name: Some("upper"),
            min: Some("0"),
            max: Some("10"),
            ..Field::new(kind, "name", "Label")
        };
        let dressed = Field {
            hint: Some("What it is for"),
            error: Some("That will not do"),
            unit: Some("minutes"),
            required: true,
            ..plain
        };
        for field in [plain, dressed] {
            for value in [
                Value::Absent,
                Value::Text("a"),
                Value::On(true),
                Value::Between {
                    lower: "1",
                    upper: "9",
                },
            ] {
                html.push_str(&field_html(&field, &Filling::of(value), opts));
            }
        }
    }
    html
}

/// A cell of every width and every priority, and every part a cell can be.
///
/// The width and the drop are the classes no source literal carries: they are
/// chosen from the column and pushed, which is how `cell-fill` came to be
/// emitted by every table in the tree and named by nothing.
fn rows(opts: &Emit) -> String {
    let mut columns = Vec::new();
    for (index, width) in [Width::Content, Width::Fixed, Width::Fill]
        .into_iter()
        .enumerate()
    {
        for (rank, priority) in [Priority::Optional, Priority::Secondary, Priority::Essential]
            .into_iter()
            .enumerate()
        {
            columns.push(Column {
                width,
                priority,
                sortable: true,
                sorted: Some(if rank % 2 == 0 {
                    Sort::Ascending
                } else {
                    Sort::Descending
                }),
                ..Column::new(NAMES[index * 3 + rank])
            });
        }
    }

    let parts = [
        None,
        Some(CellPart::Value),
        Some(CellPart::Tokens),
        Some(CellPart::Actions),
        Some(CellPart::Link),
    ];
    let mut html = String::new();
    for part in parts {
        let cells: Vec<Cell<'_>> = columns
            .iter()
            .map(|column| Cell {
                column: column.name,
                part,
                content: Markup("<span>x</span>"),
            })
            .collect();
        html.push_str(&cells_html(&columns, &cells, opts));
    }
    html
}

/// A name per column, so the nine are nine columns rather than one repeated.
const NAMES: [&str; 9] = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_class_this_crate_emits_is_one_its_vocabulary_names() {
        // The guard `MAKEOVER_UNLISTED` in quasi-webview was standing in for.
        // A name emitted and not written down shrinks the set an app checks
        // against, so the app concludes its live rules are dead -- silently,
        // and in the direction that deletes styling rather than keeping too
        // much of it.
        let opts = Emit::default();
        let names = crate::vocabulary::names(&opts);
        let emitted = emitted();
        assert!(
            emitted.len() > 30,
            "the corpus rendered {} classes, which reads as the corpus having \
             stopped calling the emitters rather than the crate having shrunk",
            emitted.len()
        );

        let stranger: Vec<&String> = emitted
            .iter()
            // The one open family, and it is this crate's: a cell carries a
            // class built from its column's name, which is app data and which
            // no set can hold. See `list::push_column_class`.
            .filter(|class| !class.starts_with("col-"))
            .filter(|class| !names.contains(*class))
            .collect();
        assert!(
            stranger.is_empty(),
            "{} class(es) emitted that `vocabulary::names` does not hold. Give \
             them a rule, or add them to the unruled list `names` reads:\n{}",
            stranger.len(),
            stranger
                .iter()
                .map(|c| format!("  .{c}"))
                .collect::<Vec<_>>()
                .join("\n")
        );
    }

    #[test]
    fn a_class_prefix_reaches_every_class_but_the_states() {
        // The same guard with a prefix set, which is a different question: a
        // name that reached markup without going through `class` is prefixed
        // nowhere and would pass the check above, then fail in the one app that
        // sets a prefix. `names` answers for both, because it prefixes the
        // written-down half and leaves the states alone.
        let opts = Emit {
            class_prefix: "mk-",
            ..Emit::default()
        };
        let names = crate::vocabulary::names(&opts);
        let stranger: Vec<String> = emitted_with(&opts)
            .into_iter()
            .filter(|class| !class.starts_with("mk-col-"))
            .filter(|class| !names.contains(class))
            .collect();
        assert!(
            stranger.is_empty(),
            "{} class(es) a prefixed render emits that `vocabulary::names` does \
             not hold:\n{}",
            stranger.len(),
            stranger
                .iter()
                .map(|c| format!("  .{c}"))
                .collect::<Vec<_>>()
                .join("\n")
        );
    }
}