domtree/error.rs
1//! Non-fatal parse diagnostics.
2//!
3//! Parsing never fails: [`crate::parse`] always returns a best-effort tree.
4//! Anything the parser had to recover from (a stray end tag, an unterminated
5//! comment, an unknown entity, …) is recorded as a [`ParseError`] and exposed
6//! via [`crate::Dom::errors`]. Linters and validators can inspect them; the
7//! common "just give me the tree" path can ignore them entirely.
8
9use core::fmt;
10
11/// A single recoverable problem encountered while parsing, with the byte
12/// offset into the source where it occurred.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ParseError {
15 /// Byte offset into the original source string.
16 pub position: usize,
17 /// What kind of problem this is.
18 pub kind: ErrorKind,
19}
20
21impl ParseError {
22 pub(crate) fn new(position: usize, kind: ErrorKind) -> Self {
23 ParseError { position, kind }
24 }
25
26 /// A short, human-readable description of the problem.
27 pub fn message(&self) -> &'static str {
28 self.kind.message()
29 }
30}
31
32/// The category of a [`ParseError`].
33///
34/// Marked `#[non_exhaustive]`: new recovery cases may be added without it being
35/// a breaking change, so always include a `_ =>` arm when matching.
36#[non_exhaustive]
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum ErrorKind {
39 /// Input ended in the middle of a construct (tag, attribute, …).
40 UnexpectedEof,
41 /// A `<!-- comment` was never closed with `-->`.
42 UnterminatedComment,
43 /// A `<![CDATA[` section was never closed with `]]>`.
44 UnterminatedCdata,
45 /// A tag was opened with `<` but never closed with `>`.
46 UnterminatedTag,
47 /// An end tag (`</x>`) had no matching open element in reach and was
48 /// ignored. An end tag reaches the 256 innermost open elements and the 8
49 /// outermost (so `</body>` still closes a deep pile of unclosed tags);
50 /// one whose element is open only in between is stray too, and that
51 /// element stays open — closed by a later end tag in reach or at the end
52 /// of the input. This keeps every end tag O(1) on any input.
53 StrayEndTag,
54 /// An end tag closed elements other than the current one (mis-nesting).
55 /// Closing elements whose end tag is optional (`</ul>` over an open
56 /// `<li>`) is reported as [`ImplicitlyClosed`](Self::ImplicitlyClosed)
57 /// instead.
58 MismatchedEndTag,
59 /// An element was closed implicitly by another tag or end-of-input.
60 ImplicitlyClosed,
61 /// A reference-shaped `&…` was not recognised and was left verbatim: an
62 /// unknown `&name;`, a malformed `&#…`, or one of WHATWG's legacy names
63 /// (`&`, `©`, …) missing its `;`. A bare `&` (as in `a & b`,
64 /// `?a=1&b=2` or `x&order=1`) is valid HTML and not reported. The position
65 /// is that of the `&` (the first such one in a text run or attribute
66 /// value).
67 UnknownEntity,
68 /// A `<! ... >` or `<? ... >` construct was treated as a bogus comment.
69 BogusComment,
70}
71
72impl ErrorKind {
73 pub(crate) fn message(&self) -> &'static str {
74 match self {
75 ErrorKind::UnexpectedEof => "unexpected end of input",
76 ErrorKind::UnterminatedComment => "unterminated comment",
77 ErrorKind::UnterminatedCdata => "unterminated CDATA section",
78 ErrorKind::UnterminatedTag => "unterminated tag",
79 ErrorKind::StrayEndTag => "stray end tag with no matching open element",
80 ErrorKind::MismatchedEndTag => "end tag closed mis-nested elements",
81 ErrorKind::ImplicitlyClosed => "element implicitly closed",
82 ErrorKind::UnknownEntity => "unknown character reference left verbatim",
83 ErrorKind::BogusComment => "bogus comment",
84 }
85 }
86}
87
88impl fmt::Display for ParseError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{} at byte {}", self.kind.message(), self.position)
91 }
92}
93
94#[cfg(feature = "std")]
95impl std::error::Error for ParseError {}