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 (or a whole foreign-namespace subtree like inline `<svg>`
42 /// / MathML `<math>`) was dropped because it has no equivalent and
43 /// is not in the security-stripped set either — `<video>`,
44 /// `<audio>`, `<canvas>`, `<dialog>`, `<colgroup>`, etc. Where the
45 /// element can carry text (e.g. `<video>` fallback content), the
46 /// contents are still flattened into the output; `<form>` /
47 /// `<fieldset>` / `<legend>` are *not* in this set — they render
48 /// as generic containers.
49 UnsupportedTag,
50 /// An attribute was recognised but can't be honoured — `colspan` /
51 /// `rowspan` on table cells (cells render unmerged). The element
52 /// itself still renders.
53 UnsupportedAttribute,
54 /// Block-level content appeared somewhere Damascene only renders
55 /// inline runs (table cells), so its block structure was flattened
56 /// — text survives, paragraph breaks and nested lists don't.
57 FlattenedContent,
58 /// A `<style>` block or inline `style="..."` attribute was dropped
59 /// because [`crate::HtmlOptions`]'s `sanitize_styles` is set. Lets
60 /// a sanitizing embedder see that the input tried to style itself.
61 SanitizedStyle,
62}
63
64/// One lint occurrence. `detail` is a human-readable hint: the dropped
65/// property name + value, the collided margin pair, the unsupported
66/// selector text, and so on.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Finding {
69 pub kind: FindingKind,
70 pub detail: String,
71}
72
73impl Finding {
74 pub(crate) fn new(kind: FindingKind, detail: impl Into<String>) -> Self {
75 Self {
76 kind,
77 detail: detail.into(),
78 }
79 }
80}
81
82/// Walker-internal collector. Threaded through `WalkCx` via shared
83/// borrow + interior mutability so the dispatch functions can `push`
84/// without a `&mut` plumb-through.
85#[derive(Default, Debug)]
86pub(crate) struct Lints {
87 findings: RefCell<Vec<Finding>>,
88}
89
90impl Lints {
91 pub(crate) fn push(&self, kind: FindingKind, detail: impl Into<String>) {
92 self.findings.borrow_mut().push(Finding::new(kind, detail));
93 }
94
95 pub(crate) fn into_vec(self) -> Vec<Finding> {
96 self.findings.into_inner()
97 }
98}