facet_dom/event.rs
1//! DOM event types for tree-based parsing.
2
3use std::borrow::Cow;
4
5/// Events emitted by a DOM parser.
6///
7/// These events represent the structure of a tree-based document (HTML/XML).
8/// The event stream follows this pattern for each element:
9///
10/// ```text
11/// NodeStart { tag: "div" }
12/// Attribute { name: "class", value: "container" }
13/// Attribute { name: "id", value: "main" }
14/// ChildrenStart
15/// Text("Hello ")
16/// NodeStart { tag: "strong" }
17/// ChildrenStart
18/// Text("world")
19/// ChildrenEnd
20/// NodeEnd
21/// Text("!")
22/// ChildrenEnd
23/// NodeEnd
24/// ```
25#[derive(Debug, Clone, PartialEq)]
26pub enum DomEvent<'a> {
27 /// Start of an element node.
28 ///
29 /// Followed by zero or more `Attribute` events, then `ChildrenStart`.
30 NodeStart {
31 /// The tag name (e.g., "div", "p", "my-component").
32 tag: Cow<'a, str>,
33 /// Optional namespace URI.
34 namespace: Option<Cow<'a, str>>,
35 },
36
37 /// An attribute on the current element.
38 ///
39 /// Only valid between `NodeStart` and `ChildrenStart`.
40 Attribute {
41 /// Attribute name.
42 name: Cow<'a, str>,
43 /// Attribute value.
44 value: Cow<'a, str>,
45 /// Optional namespace URI.
46 namespace: Option<Cow<'a, str>>,
47 },
48
49 /// Start of the children section.
50 ///
51 /// After this, expect `Text`, `NodeStart`, or `ChildrenEnd`.
52 ChildrenStart,
53
54 /// End of the children section.
55 ChildrenEnd,
56
57 /// End of an element node.
58 ///
59 /// Must be preceded by `ChildrenEnd` (or directly by attributes for void elements).
60 NodeEnd,
61
62 /// Text content.
63 ///
64 /// Only valid between `ChildrenStart` and `ChildrenEnd`.
65 Text(Cow<'a, str>),
66
67 /// A comment (usually ignored during deserialization).
68 Comment(Cow<'a, str>),
69
70 /// A processing instruction (XML) or DOCTYPE (HTML).
71 ProcessingInstruction {
72 /// Target (e.g., "xml" for `<?xml ...?>`).
73 target: Cow<'a, str>,
74 /// Data content.
75 data: Cow<'a, str>,
76 },
77
78 /// DOCTYPE declaration (HTML5).
79 Doctype(Cow<'a, str>),
80}
81
82impl<'a> DomEvent<'a> {
83 /// Returns true if this is a `NodeStart` event.
84 pub fn is_node_start(&self) -> bool {
85 matches!(self, DomEvent::NodeStart { .. })
86 }
87
88 /// Returns true if this is a `NodeEnd` event.
89 pub fn is_node_end(&self) -> bool {
90 matches!(self, DomEvent::NodeEnd)
91 }
92
93 /// Returns true if this is an `Attribute` event.
94 pub fn is_attribute(&self) -> bool {
95 matches!(self, DomEvent::Attribute { .. })
96 }
97
98 /// Returns true if this is a `Text` event.
99 pub fn is_text(&self) -> bool {
100 matches!(self, DomEvent::Text(_))
101 }
102
103 /// Returns true if this is `ChildrenStart`.
104 pub fn is_children_start(&self) -> bool {
105 matches!(self, DomEvent::ChildrenStart)
106 }
107
108 /// Returns true if this is `ChildrenEnd`.
109 pub fn is_children_end(&self) -> bool {
110 matches!(self, DomEvent::ChildrenEnd)
111 }
112
113 /// Wrap this event for XML-like trace formatting.
114 pub fn trace(&self) -> TraceFmt<'_, 'a> {
115 TraceFmt(self)
116 }
117}
118
119/// Newtype for XML-like trace formatting of DOM events.
120pub struct TraceFmt<'r, 'a>(pub &'r DomEvent<'a>);
121
122impl std::fmt::Display for TraceFmt<'_, '_> {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 use owo_colors::OwoColorize;
125
126 match self.0 {
127 DomEvent::NodeStart { tag, namespace } => {
128 if let Some(ns) = namespace {
129 write!(f, "NodeStart {}<{}:{}>", "<".cyan(), ns, tag.cyan())
130 } else {
131 write!(f, "NodeStart {}{}{}", "<".cyan(), tag.cyan(), ">".cyan())
132 }
133 }
134 DomEvent::NodeEnd => write!(f, "NodeEnd {}", "</>".cyan()),
135 DomEvent::Attribute {
136 name,
137 value,
138 namespace,
139 } => {
140 if let Some(ns) = namespace {
141 write!(
142 f,
143 "Attribute {}{}:{}={}{}{}",
144 "@".yellow(),
145 ns,
146 name.yellow(),
147 "\"".yellow(),
148 value,
149 "\"".yellow()
150 )
151 } else {
152 write!(
153 f,
154 "Attribute {}{}={}{}{}",
155 "@".yellow(),
156 name.yellow(),
157 "\"".yellow(),
158 value,
159 "\"".yellow()
160 )
161 }
162 }
163 DomEvent::ChildrenStart => write!(f, "{}", "ChildrenStart".dimmed()),
164 DomEvent::ChildrenEnd => write!(f, "{}", "ChildrenEnd".dimmed()),
165 DomEvent::Text(t) => {
166 let preview: String = t.chars().take(40).collect();
167 if t.len() > 40 {
168 write!(
169 f,
170 "Text {}{}{}",
171 "\"".green(),
172 preview.green(),
173 "...\"".green()
174 )
175 } else {
176 write!(
177 f,
178 "Text {}{}{}",
179 "\"".green(),
180 preview.green(),
181 "\"".green()
182 )
183 }
184 }
185 DomEvent::Comment(c) => {
186 let preview: String = c.chars().take(20).collect();
187 write!(
188 f,
189 "Comment {}{}{}",
190 "<!--".dimmed(),
191 preview.dimmed(),
192 "-->".dimmed()
193 )
194 }
195 DomEvent::ProcessingInstruction { target, data } => {
196 write!(f, "ProcessingInstruction <?{target} {data}?>")
197 }
198 DomEvent::Doctype(d) => write!(f, "Doctype <!DOCTYPE {d}>"),
199 }
200 }
201}