//! 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 this is what checks the claim. A name //! missing from that set is worse than a name missing from the stylesheet: an //! app checking its own stylesheet against the set concludes that its rules for //! the name are dead and deletes live styling. //! //! 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, Contrast, Facet, FacetValue, Field, FieldKind, Figure, Meter, Priority, Readiness, Selecting, Sort, Standing, ThemeChoice, ThemeVariant, 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 { emitted_with(&Emit::default()) } /// [`emitted`], with the emit options a host would set. fn emitted_with(opts: &Emit) -> BTreeSet { 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 { 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("")), 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")]; // Two variants and two tiers, so the corpus carries a group boundary and a // badge that is not the same badge twice. One theme per variant would emit // one `` and prove nothing about where the next one opens. const THEMES: &[ThemeChoice<'_>] = &[ ThemeChoice::new("goingson", "GoingsOn", ThemeVariant::Light, Contrast::High), ThemeChoice::new("ayu-light", "Ayu Light", ThemeVariant::Light, Contrast::Low), ThemeChoice::new("carbonfox", "Carbonfox", ThemeVariant::Dark, Contrast::High), ThemeChoice::new("nord", "Nord", ThemeVariant::Dark, Contrast::Standard), ]; 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::Theme, FieldKind::Hidden, ] { let plain = Field { options: OPTIONS, themes: THEMES, follows: Some(Choice::new("system", "Follow System")), 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> = columns .iter() .map(|column| Cell { column: column.name, part, content: Markup("x"), }) .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::>() .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 = 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::>() .join("\n") ); } }