//! Every class this crate is responsible for, as a set rather than one name at //! a time. //! //! The naming functions ([`crate::class`], [`crate::option_class`], //! [`crate::list::part_class`], [`crate::list::cell_part_class`]) answer "what //! is this one thing called". That is half the agreement. The other half is //! the set: a checker cannot ask "is this app rule //! re-specifying something makeover already defines" without the list, and this //! crate is the only place that knows it, because this crate writes the sheet. //! //! # Two sets, because there are two questions //! //! [`vocabulary`] is the classes the generated stylesheet writes a rule for. //! That is the set a drift check wants: an app rule for one of these is a //! restatement of a rule the app already gets, and unlayered app CSS beats //! `@layer makeover`, so the restatement silently wins. //! //! [`names`] is every class this crate can put in markup, which is the first //! set plus the ones it deliberately leaves unruled. `row-actions`, //! `cell-actions`, `cell-tokens` and `cell-link` have no rule on purpose: only //! `.cell-value` takes a colour, because a token carries its own tone and an //! action is a control rather than text. A class that sets no properties is a //! class that means "I thought about this", and this crate does not emit those. //! So a screen renderer legitimately emits names that [`vocabulary`] does not //! contain, and a test asking "is every class this renderer emits one makeover //! knows about" has to read [`names`] or it fails on four correct ones. //! //! # Why the first set is scraped and not listed //! //! A hand-maintained copy of the sheet's contents is the defect being fixed, //! one level up: it can disagree with the sheet, and the day it does, the //! checker reads the list and the browser reads the sheet. So [`vocabulary`] //! parses the CSS this crate generates. There is no second source to drift //! from, and a class added to an emitter enters the vocabulary in the same //! commit that adds it. use crate::facet::FACET_CLASSES; use crate::figure::FIGURE_CLASSES; use crate::form::{FIELD_CLASSES, FIELD_STATE_CLASSES}; use crate::list::{ CELL_DROP_CLASSES, CELL_PART_CLASSES, CELL_WIDTH_CLASSES, FLOW_CLASSES, NESTING_CLASSES, ROW_PART_CLASSES, }; use crate::meter::METER_CLASSES; use crate::placeholder::PLACEHOLDER_CLASSES; use crate::{Emit, option_class}; use makeover_layout::Selector; use std::collections::{BTreeMap, BTreeSet}; /// Every class name the generated stylesheet defines a rule for, prefixed the /// way `opts` prefixes them. /// /// Includes the state classes a caller never spells alone (`chosen`, /// `latched`). Those are deliberately unprefixed: they qualify a prefixed /// component (`.mo-tab.chosen`) rather than standing on their own, so a prefix /// moves the thing and not its state. #[must_use] pub fn vocabulary(opts: &Emit) -> BTreeSet { classes_in_css(&crate::stylesheet(opts)) } /// Every class this crate can put in markup or in a rule. /// /// [`vocabulary`] plus every class an emitter here writes without the sheet /// ruling it. This is the set to check a renderer's emitted markup against: a /// class outside it is a name that renderer invented, which is how /// quasi-webview came to spell `tabs`, `segmented` and `option` and render /// every described selector flat. /// /// # The unruled half is written down, module by module /// /// One list per module that emits markup, each beside its emitters, and this /// is their union. Keeping the omissions beside the emitters is what stops the /// set drifting from what actually comes out in a document. A name this /// function omits is a name an app reads as dead and deletes live rules for. /// /// [`crate::corpus`] is what keeps the union honest, and it renders rather than /// reading the source: a width class, a drop class and a state appended to an /// open attribute are literals nowhere, which is what a reading of the /// emitters missed for eleven of the fifteen. #[must_use] pub fn names(opts: &Emit) -> BTreeSet { let mut all = vocabulary(opts); all.extend( ROW_PART_CLASSES .iter() .chain(CELL_PART_CLASSES) .chain(CELL_WIDTH_CLASSES) .chain(CELL_DROP_CLASSES) .chain(FLOW_CLASSES) .chain(NESTING_CLASSES) .chain(crate::RUN_CLASSES) .chain(FACET_CLASSES) .chain(FIELD_CLASSES) .chain(FIGURE_CLASSES) .chain(METER_CLASSES) .chain(PLACEHOLDER_CLASSES) .map(|name| crate::class(name, opts)), ); all.extend( [Selector::Tabs, Selector::Segmented, Selector::Toggle] .into_iter() .map(|s| crate::class(option_class(s), opts)), ); // Unprefixed, deliberately, exactly as the `chosen` and `latched` the // scraped half brings in: a state qualifies a prefixed component rather // than standing on its own. all.extend(FIELD_STATE_CLASSES.iter().map(|name| (*name).to_owned())); all } /// Which properties a stylesheet sets on each class it names. /// /// The grain a drift check actually wants. A class name in common is not by /// itself a divergence: goingson's `.badge` sets shape and the generated /// `.badge` sets fill and edge, and the app's own comment says "do not add /// background, border or box-shadow here". That arrangement is settled and /// correct, so a check that flagged the shared name would demand deleting it. /// A shared *property* is the thing that goes wrong, because app CSS is /// unlayered and takes the property from the design system silently. /// /// A property appearing under more than one selector arm collapses into one /// entry. That loses a real distinction -- the sort caret's reserved gap is /// `content` on the unsorted arm and the generated caret is `content` on the /// sorted one, which is a deliberate pairing rather than a clash -- so a /// consumer of this needs a way to say a pair was reviewed. Deciding that here /// would need a selector matcher, and a check that guesses wrong about /// specificity fails correct builds. /// /// A declaration whose value is exactly `revert-layer` is not one of them. It /// takes nothing by construction: it is a later layer handing the property back /// to the one below, which is the opposite of the thing this reader is looking /// for. Counting it made every handoff in a consumer's sheet look like an /// override, and the allowlist entry written to silence one went on permitting /// a real override on the same pair afterwards. [`deferrals_by_class`] is where /// those declarations go instead. #[must_use] pub fn declarations_by_class(css: &str) -> BTreeMap> { by_class(css, |value| !is_handoff(value)) } /// Which properties a stylesheet hands back to the layer below, per class. /// /// The other half of [`declarations_by_class`]. A `revert-layer` says "whatever /// the design system set here, keep it", so a checker reading a consumer's /// sheet wants it as evidence that a clash was already remedied rather than as /// a clash of its own. #[must_use] pub fn deferrals_by_class(css: &str) -> BTreeMap> { by_class(css, is_handoff) } /// [`declarations_by_class`] and [`deferrals_by_class`], which differ only in /// which declarations they keep. fn by_class(css: &str, keep: impl Fn(&str) -> bool) -> BTreeMap> { let mut out: BTreeMap> = BTreeMap::new(); for (selector, body) in rules(css) { let classes = classes_in_selector(&selector); if classes.is_empty() { continue; } let properties = properties_in_body(&body, &keep); if properties.is_empty() { continue; } for class in classes { out.entry(class).or_default().extend(properties.clone()); } } out } /// Which properties a stylesheet sets on each bare element it names. /// /// The blind spot [`declarations_by_class`] has by construction: it keys rules /// by the classes in their selectors, so a rule carrying no class at all is /// invisible to it. `button { color: var(--content) }` is exactly that, and it /// sets the same property the generated `.button` does on every described act /// in the app -- including the tone of a destructive one, so a delete comes to /// look like an ordinary button with the check reporting nothing. /// /// Only a selector arm that is one bare compound counts: `button`, /// `button:hover`, `input[type="text"]`. A scoped arm (`.page button`) reaches /// the elements inside one region rather than every one of them, so whether it /// lands on a described act depends on where that act is rendered, and a check /// that guessed would fail correct builds. The certain case is the one this /// reads. /// /// Pair the result against [`classes_for_element`] to ask the question a /// checker wants: does this element rule take a property the design system sets /// on a class that element can carry. /// /// The answer carries the strongest arm each property was set on, because the /// app's own remedy has to outrank the rule it remedies. `.field` does not beat /// `input[type="text"]`: both are the app's, both are in the same layer, and /// the attribute makes the element rule the more specific of the two. A check /// reading only "the app mentions this pair somewhere" waves that straight /// through, which is the shape of every handoff that looked written and was /// not. #[must_use] pub fn declarations_by_element(css: &str) -> BTreeMap> { let mut out: BTreeMap> = BTreeMap::new(); for (selector, body) in rules(css) { let properties = properties_in_body(&body, |value| !is_handoff(value)); if properties.is_empty() { continue; } for arm in selector.split(',') { let Some(element) = bare_element(arm) else { continue; }; let rank = specificity(arm); let entry = out.entry(element).or_default(); for property in &properties { let strongest = entry.entry(property.clone()).or_default(); *strongest = (*strongest).max(rank); } } } out } /// What a stylesheet says about each class, and how strongly. /// /// Every property the sheet names on a class, whether it takes it or hands it /// back, keyed by the strongest arm that names it. The question it answers is /// not "does this collide" -- [`declarations_by_class`] is that -- but "has the /// app spoken for this pair, in a rule that wins where it has to". #[must_use] pub fn mentions_by_class(css: &str) -> BTreeMap> { let mut out: BTreeMap> = BTreeMap::new(); for (selector, body) in rules(css) { let properties = properties_in_body(&body, |_| true); if properties.is_empty() { continue; } for arm in selector.split(',') { let classes = classes_in_selector(arm); if classes.is_empty() { continue; } let rank = specificity(arm); for class in classes { let entry = out.entry(class).or_default(); for property in &properties { let strongest = entry.entry(property.clone()).or_default(); *strongest = (*strongest).max(rank); } } } } out } /// How CSS ranks one selector: ids, then classes, then elements. /// /// Ordered the way the cascade orders it, so the tuple comparison is the /// cascade's comparison. It settles a contest between two rules in the same /// layer, which is the only contest it is used for here: a layer beats /// specificity outright, so nothing in the app's sheet has to be compared /// against the generated one this way. pub type Specificity = (usize, usize, usize); /// The specificity of one selector arm. /// /// A functional pseudo-class counts as one class and its argument is not read. /// CSS says `:not(.a.b)` takes the specificity of its strongest argument, so /// this undercounts a compound inside one -- which puts the error on the side /// of reporting a remedy as too weak rather than accepting one that is. #[must_use] pub fn specificity(selector: &str) -> Specificity { let chars: Vec = selector.chars().collect(); let (mut ids, mut classes, mut elements) = (0, 0, 0); let mut i = 0; while i < chars.len() { match chars[i] { '#' => { ids += 1; i = skip_name(&chars, i + 1); } '.' => { classes += 1; i = skip_name(&chars, i + 1); } ':' => { // `::before` is an element, `:hover` is a class. if chars.get(i + 1) == Some(&':') { elements += 1; i = skip_name(&chars, i + 2); } else { classes += 1; i = skip_name(&chars, i + 1); } if chars.get(i) == Some(&'(') { i = skip_group(&chars, i); } } '[' => { classes += 1; i = skip_group(&chars, i); } c if c.is_ascii_alphabetic() => { elements += 1; i = skip_name(&chars, i); } // A combinator, whitespace, or the universal selector, none of // which count for anything. _ => i += 1, } } (ids, classes, elements) } /// Past the identifier starting at `from`. fn skip_name(chars: &[char], from: usize) -> usize { let mut i = from; while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '-' || chars[i] == '_') { i += 1; } i } /// Past the bracketed or parenthesised group opening at `from`, nesting and /// all. fn skip_group(chars: &[char], from: usize) -> usize { let mut depth = 0usize; let mut i = from; while i < chars.len() { match chars[i] { '[' | '(' => depth += 1, ']' | ')' => { depth -= 1; if depth == 0 { return i + 1; } } _ => {} } i += 1; } i } /// A value that hands the property back rather than taking it. /// /// Bare only. `revert-layer !important` in a later layer inverts layer order /// and takes the property from every layer below, which is the opposite /// declaration wearing the same word. fn is_handoff(value: &str) -> bool { value.trim() == "revert-layer" } /// The property names a declaration block sets, keeping the ones `keep` admits. fn properties_in_body(body: &str, keep: impl Fn(&str) -> bool) -> BTreeSet { body.split(';') .filter_map(|decl| decl.split_once(':')) .filter(|(_, value)| keep(value)) .map(|(name, _)| name.trim().to_string()) .filter(|name| !name.is_empty() && !name.contains(['{', '}'])) .collect() } #[must_use] pub fn classes_in_css(css: &str) -> BTreeSet { rules(css) .into_iter() .flat_map(|(selector, _)| classes_in_selector(&selector)) .collect() } /// `(selector, declaration block)` for every rule in a stylesheet. /// /// One reader for both sides. Comparing what makeover defines against what an /// app defines is only meaningful if the two were read the same way, which is /// why this is the only place either question is answered from. /// /// A comment is skipped whole: the banner at the top of the generated sheet is /// prose about the cascade layer and would otherwise contribute words that look /// like selectors. A string is opaque, because `content: "\25B2"` is the sort /// caret rather than a selector and a brace inside one would desync the stack. /// An at-rule block (`@layer`, `@media`, `@supports`) holds rules rather than /// declarations, so a depth counter alone is not enough and the stack records /// what kind of block each brace opened. fn rules(css: &str) -> Vec<(String, String)> { let mut out = Vec::new(); // One entry per open brace: true when that block holds declarations rather // than nested rules. let mut blocks: Vec = Vec::new(); // Text since the last `{`, `}` or `;`. What precedes a `{` is that block's // prelude, and a prelude starting with `@` opens an at-rule. let mut prelude = String::new(); // The selector of each open declaration block, and the body so far. let mut open: Vec<(String, String)> = Vec::new(); let mut chars = css.chars().peekable(); while let Some(c) = chars.next() { match c { '/' if chars.peek() == Some(&'*') => { chars.next(); let mut star = false; for c in chars.by_ref() { if star && c == '/' { break; } star = c == '*'; } prelude.clear(); } '"' | '\'' => { let quote = c; let mut escaped = false; // Keep the quotes in the body: a value is not a property name, // and dropping them would join two declarations into one. if blocks.last().copied().unwrap_or(false) && let Some((_, body)) = open.last_mut() { body.push(quote); } for c in chars.by_ref() { if escaped { escaped = false; } else if c == '\\' { escaped = true; } else if c == quote { break; } } // The closing quote only. A value holding `;` or `:` would // otherwise read as two declarations, and `url("a;b:c")` is a // real thing an app writes. if blocks.last().copied().unwrap_or(false) && let Some((_, body)) = open.last_mut() { body.push(quote); } } '{' => { let declarations = !prelude.trim_start().starts_with('@'); if declarations { open.push((prelude.clone(), String::new())); } blocks.push(declarations); prelude.clear(); } '}' => { if blocks.pop().unwrap_or(false) && let Some(rule) = open.pop() { out.push(rule); } prelude.clear(); } _ => { if blocks.last().copied().unwrap_or(false) && let Some((_, body)) = open.last_mut() { body.push(c); } else if c == ';' { prelude.clear(); } else { prelude.push(c); } } } } out } /// Which generated classes each element can plausibly carry. /// /// The half of the element check that CSS cannot answer. A stylesheet says /// `button { color: ... }` and `.chip { color: ... }` and nothing in either /// text says a chip is rendered as a `