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}