Skip to main content

sup_xml_core/html/
events.rs

1#![forbid(unsafe_code)]
2
3//! Event types emitted by [`HtmlReader`](super::stream::HtmlReader)
4//! and dispatched to [`HtmlSaxHandler`](super::stream::HtmlSaxHandler).
5//!
6//! These are tree-construction events (post-html5ever insertion-mode
7//! processing), not raw tokens — implicit `<html>`/`<head>`/`<body>`
8//! insertion, void-element handling, and tag-soup recovery have all
9//! happened by the time you see an event.
10//!
11//! The events borrow from the reader's internal owned storage, so they
12//! are valid until the next [`next()`](super::stream::HtmlReader::next)
13//! call.  Materialise anything you need to keep into owned types
14//! before advancing.
15//!
16//! # Naming
17//!
18//! Struct-variant style mirrors the XML side
19//! ([`Event`](crate::reader::Event)).  Separate payload types are
20//! avoided to keep the public type surface small and to prevent name
21//! clashes (e.g. with [`HtmlDoctype`](sup_xml_tree::HtmlDoctype) on
22//! `Document::html_metadata`).
23
24/// A tree-construction event emitted by the streaming HTML parser.
25#[derive(Debug)]
26pub enum HtmlEvent<'a> {
27    /// An element start tag.  Self-closing source forms (`<br/>`)
28    /// and void elements (`<br>`) both emit a single `StartElement`;
29    /// for void elements no matching `EndElement` follows.  For
30    /// non-void elements the matching `EndElement` follows after
31    /// the element's content events.
32    StartElement {
33        /// The lower-cased element name, e.g. `"div"`.
34        name: &'a str,
35        /// Iterable view over the element's attributes.
36        attributes: HtmlAttrs<'a>,
37    },
38    /// An element end tag.  Emitted for every non-void element that
39    /// was opened, including ones implicitly closed by html5ever's
40    /// recovery.
41    EndElement {
42        /// The lower-cased element name being closed.
43        name: &'a str,
44    },
45    /// A character data run.  Adjacent text events from html5ever
46    /// are coalesced into a single event by the streaming sink, so
47    /// consumers don't see arbitrarily-fragmented text.
48    Text(&'a str),
49    /// An HTML comment, with the surrounding `<!--` / `-->`
50    /// delimiters stripped.
51    Comment(&'a str),
52    /// The document `<!DOCTYPE>` declaration.  Emitted at most once
53    /// per document, before any element events.
54    Doctype {
55        name: &'a str,
56        public_id: &'a str,
57        system_id: &'a str,
58    },
59    /// End of input.  Subsequent calls to
60    /// [`HtmlReader::next`](super::stream::HtmlReader::next) keep
61    /// returning `Eof`.
62    Eof,
63}
64
65/// Iterable view over an element's attributes.  Cheap to clone
66/// (it's `Copy`).
67#[derive(Debug, Clone, Copy)]
68pub struct HtmlAttrs<'a> {
69    pub(crate) inner: &'a [OwnedAttr],
70}
71
72impl<'a> HtmlAttrs<'a> {
73    /// Iterate attributes in source order.
74    pub fn iter(&self) -> HtmlAttrsIter<'a> {
75        HtmlAttrsIter {
76            slice: self.inner,
77            pos: 0,
78        }
79    }
80
81    /// Number of attributes on this element.
82    pub fn len(&self) -> usize {
83        self.inner.len()
84    }
85
86    /// True if no attributes.
87    pub fn is_empty(&self) -> bool {
88        self.inner.is_empty()
89    }
90
91    /// Look up an attribute value by lower-case name.  O(n) over
92    /// the attribute list — for small attribute counts this is
93    /// faster than constructing a `HashMap`.
94    pub fn get(&self, name: &str) -> Option<&'a str> {
95        self.inner
96            .iter()
97            .find(|a| a.name == name)
98            .map(|a| a.value.as_str())
99    }
100}
101
102/// Iterator returned by [`HtmlAttrs::iter`].
103pub struct HtmlAttrsIter<'a> {
104    slice: &'a [OwnedAttr],
105    pos: usize,
106}
107
108impl<'a> Iterator for HtmlAttrsIter<'a> {
109    type Item = HtmlAttribute<'a>;
110    fn next(&mut self) -> Option<HtmlAttribute<'a>> {
111        let a = self.slice.get(self.pos)?;
112        self.pos += 1;
113        Some(HtmlAttribute {
114            name: &a.name,
115            value: &a.value,
116        })
117    }
118    fn size_hint(&self) -> (usize, Option<usize>) {
119        let rem = self.slice.len() - self.pos;
120        (rem, Some(rem))
121    }
122}
123
124impl<'a> ExactSizeIterator for HtmlAttrsIter<'a> {}
125
126/// A single attribute on a start tag.
127#[derive(Debug, Clone, Copy)]
128pub struct HtmlAttribute<'a> {
129    pub name: &'a str,
130    pub value: &'a str,
131}
132
133// ── internal owned storage ───────────────────────────────────────────────────
134
135use crate::error::XmlError;
136
137/// Owned event variant stored in the streaming sink's queue.  Not
138/// public; the public surface is [`HtmlEvent`] which borrows from
139/// these.
140#[derive(Debug)]
141pub(crate) enum OwnedEvent {
142    StartElement {
143        name: String,
144        attrs: Vec<OwnedAttr>,
145    },
146    EndElement {
147        name: String,
148    },
149    Text(String),
150    Comment(String),
151    Doctype {
152        name: String,
153        public_id: String,
154        system_id: String,
155    },
156    /// A parse error reported by html5ever.  Filtered out before
157    /// reaching the consumer — surfaces as `Result::Err` in strict
158    /// mode or accumulates in `recovered_errors()` in lenient mode.
159    ParseError(XmlError),
160    Eof,
161}
162
163#[derive(Debug)]
164pub(crate) struct OwnedAttr {
165    pub name: String,
166    pub value: String,
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    fn sample_attrs() -> Vec<OwnedAttr> {
174        vec![
175            OwnedAttr { name: "id".into(),    value: "main".into() },
176            OwnedAttr { name: "class".into(), value: "x y".into() },
177        ]
178    }
179
180    #[test]
181    fn html_attrs_len_and_is_empty() {
182        let attrs = sample_attrs();
183        let v = HtmlAttrs { inner: &attrs };
184        assert_eq!(v.len(), 2);
185        assert!(!v.is_empty());
186
187        let empty: Vec<OwnedAttr> = Vec::new();
188        let v2 = HtmlAttrs { inner: &empty };
189        assert_eq!(v2.len(), 0);
190        assert!(v2.is_empty());
191    }
192
193    #[test]
194    fn html_attrs_iter_yields_in_source_order() {
195        let attrs = sample_attrs();
196        let v = HtmlAttrs { inner: &attrs };
197        let names: Vec<&str> = v.iter().map(|a| a.name).collect();
198        assert_eq!(names, vec!["id", "class"]);
199        let values: Vec<&str> = v.iter().map(|a| a.value).collect();
200        assert_eq!(values, vec!["main", "x y"]);
201    }
202
203    #[test]
204    fn html_attrs_iter_size_hint_exact() {
205        let attrs = sample_attrs();
206        let v = HtmlAttrs { inner: &attrs };
207        let mut it = v.iter();
208        assert_eq!(it.size_hint(), (2, Some(2)));
209        let _ = it.next();
210        assert_eq!(it.size_hint(), (1, Some(1)));
211        // ExactSizeIterator is implemented — len() should mirror size_hint.
212        assert_eq!(it.len(), 1);
213        let _ = it.next();
214        assert_eq!(it.size_hint(), (0, Some(0)));
215        assert!(it.next().is_none());
216    }
217
218    #[test]
219    fn html_attrs_get_finds_by_lowercase_name() {
220        let attrs = sample_attrs();
221        let v = HtmlAttrs { inner: &attrs };
222        assert_eq!(v.get("id"),    Some("main"));
223        assert_eq!(v.get("class"), Some("x y"));
224        assert_eq!(v.get("missing"), None);
225    }
226
227    #[test]
228    fn html_attrs_is_copy() {
229        // Smoke test: HtmlAttrs derives Copy, so passing by value
230        // doesn't move.
231        let attrs = sample_attrs();
232        let v = HtmlAttrs { inner: &attrs };
233        let v2 = v; // copy
234        let v3 = v; // copy again — would fail to compile if not Copy
235        assert_eq!(v2.len(), v3.len());
236    }
237}