Skip to main content

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}