damascene_html/lints.rs
1//! Lint findings emitted while transforming HTML.
2//!
3//! The transformer's stated contract is: render the HTML subset that
4//! maps cleanly onto Damascene's widget vocabulary, and **surface
5//! everything else as a lint finding** so authors and downstream tools
6//! know what was dropped. A doc that emits twenty findings tells the
7//! author the renderer can't do their layout — without claiming
8//! success.
9//!
10//! Findings carry no severity; the embedder decides whether each kind
11//! is fatal, a warning, or informational. The transformer never
12//! produces duplicates for the same node + kind, but does emit one
13//! finding per occurrence across the document.
14//!
15//! Access via [`crate::html_with_lints`] or [`crate::html_blocks_with_lints`].
16//! The lint-free entry points ([`crate::html`], [`crate::html_with_options`])
17//! collect internally and discard, preserving the v1 signature.
18
19use std::cell::RefCell;
20
21/// What was dropped or coerced, in a single dimension. The transformer
22/// emits at most one finding per (node, kind) tuple, so a single
23/// element with three unsupported properties produces three findings.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum FindingKind {
26 /// A CSS declaration was recognised but its target has no Damascene
27 /// equivalent — `position: absolute`, `float: left`, `display:
28 /// grid`, `vh`/`vw`/`fr` units, etc. The author sees the
29 /// declaration was dropped, not silently honoured.
30 DroppedDeclaration,
31 /// A `<style>`-block selector form is outside the supported subset
32 /// (descendant / child / sibling combinators, pseudo-classes,
33 /// attribute selectors, namespace prefixes) and the whole rule was
34 /// ignored. Reported by the selector parser at stylesheet-collect
35 /// time.
36 UnsupportedSelector,
37 /// Author-set `margin-top` / `margin-bottom` on siblings disagreed
38 /// across pairs and was flattened to a single parent `gap`. The
39 /// detail string spells out the values that collided.
40 MarginAsymmetryFlattened,
41 /// A tag was dropped wholesale because it has no equivalent and is
42 /// not in the security-stripped set either — `<form>`, `<video>`,
43 /// `<audio>`, `<canvas>`, `<dialog>`, etc.
44 UnsupportedTag,
45}
46
47/// One lint occurrence. `detail` is a human-readable hint: the dropped
48/// property name + value, the collided margin pair, the unsupported
49/// selector text, and so on.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct Finding {
52 pub kind: FindingKind,
53 pub detail: String,
54}
55
56impl Finding {
57 pub(crate) fn new(kind: FindingKind, detail: impl Into<String>) -> Self {
58 Self {
59 kind,
60 detail: detail.into(),
61 }
62 }
63}
64
65/// Walker-internal collector. Threaded through `WalkCx` via shared
66/// borrow + interior mutability so the dispatch functions can `push`
67/// without a `&mut` plumb-through.
68#[derive(Default, Debug)]
69pub(crate) struct Lints {
70 findings: RefCell<Vec<Finding>>,
71}
72
73impl Lints {
74 pub(crate) fn push(&self, kind: FindingKind, detail: impl Into<String>) {
75 self.findings.borrow_mut().push(Finding::new(kind, detail));
76 }
77
78 pub(crate) fn into_vec(self) -> Vec<Finding> {
79 self.findings.into_inner()
80 }
81}