1use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt;
6use std::num::NonZeroUsize;
7use std::ops::Range;
8
9use crate::tokenizer::matches_normalized_name;
10use crate::{Attribute, Reader, Token};
11
12#[derive(Debug, Clone, Copy)]
18pub struct Limits {
19 pub max_input_bytes: usize,
21 pub max_nodes: usize,
23 pub max_depth: usize,
25}
26
27impl Default for Limits {
28 fn default() -> Self {
29 Self {
30 max_input_bytes: 128 * 1024 * 1024,
31 max_nodes: 4_000_000,
32 max_depth: 256,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Error {
40 InputLimit,
42 NodeLimit,
44 DepthLimit,
46}
47
48impl fmt::Display for Error {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 formatter.write_str(match self {
51 Self::InputLimit => "HTML input exceeds the byte limit",
52 Self::NodeLimit => "HTML document exceeds the node limit",
53 Self::DepthLimit => "HTML document exceeds the nesting limit",
54 })
55 }
56}
57
58impl std::error::Error for Error {}
59
60pub struct Document<'a> {
70 nodes: Vec<Node<'a>>,
71 attributes: Vec<Attribute<'a>>,
72}
73
74struct Node<'a> {
75 kind: Kind<'a>,
76 parent: Option<NonZeroUsize>,
78 end: usize,
80}
81
82enum Kind<'a> {
83 Element(ElementData<'a>),
84 Text(Cow<'a, str>),
85}
86
87struct ElementData<'a> {
88 name: Cow<'a, str>,
89 attributes: Range<usize>,
90}
91
92struct Open {
93 node: usize,
94 previous: Option<usize>,
96}
97
98impl<'a> Document<'a> {
99 pub fn parse(source: &'a str) -> Result<Self, Error> {
101 Self::parse_with_limits(source, Limits::default())
102 }
103
104 pub fn parse_with_limits(source: &'a str, limits: Limits) -> Result<Self, Error> {
109 if source.len() > limits.max_input_bytes {
110 return Err(Error::InputLimit);
111 }
112 let mut document = Self {
113 nodes: Vec::with_capacity(source.len().min(limits.max_nodes).min(16)),
114 attributes: Vec::new(),
115 };
116 let mut open: Vec<Open> = Vec::new();
117 let mut names: Option<HashMap<Cow<'a, str>, usize>> = None;
120 let mut reader = Reader::new(source);
121 let mut attribute_buffer = Vec::new();
122 while let Some(token) = reader.next_with_attribute_buffer(&mut attribute_buffer) {
123 let mut attributes = match token {
124 Token::StartTag(mut tag) => {
125 let is_void = is_void(&tag.name);
126 if !is_void && open.len() >= limits.max_depth {
127 return Err(Error::DepthLimit);
128 }
129 let index = document.nodes.len();
130 if index >= limits.max_nodes {
131 return Err(Error::NodeLimit);
132 }
133 let parent = open
134 .last()
135 .and_then(|entry| NonZeroUsize::new(entry.node + 1));
136 if !is_void {
137 let previous = names
138 .as_mut()
139 .and_then(|names| names.insert(tag.name.clone(), open.len()));
140 open.push(Open {
141 node: index,
142 previous,
143 });
144 }
145 let start = document.attributes.len();
146 if document.attributes.is_empty() {
147 std::mem::swap(&mut document.attributes, &mut tag.attributes);
148 } else {
149 document.attributes.append(&mut tag.attributes);
150 }
151 document.nodes.push(Node {
152 kind: Kind::Element(ElementData {
153 name: tag.name,
154 attributes: start..document.attributes.len(),
155 }),
156 parent,
157 end: index + 1,
158 });
159 tag.attributes
160 }
161 Token::EndTag(tag) => {
162 let matches_top = open.last().is_some_and(|entry| {
163 matches!(&document.nodes[entry.node].kind, Kind::Element(start) if start.name == tag.name)
164 });
165 if !matches_top && open.len() > 8 && names.is_none() {
166 let mut index = HashMap::new();
167 for (depth, entry) in open.iter_mut().enumerate() {
168 let Kind::Element(tag) = &document.nodes[entry.node].kind else {
169 unreachable!()
170 };
171 entry.previous = index.insert(tag.name.clone(), depth);
172 }
173 names = Some(index);
174 }
175 let depth = if matches_top {
176 Some(open.len() - 1)
177 } else if let Some(names) = &names {
178 names.get(tag.name.as_ref()).copied()
179 } else {
180 open.iter().rposition(|entry| {
181 matches!(&document.nodes[entry.node].kind, Kind::Element(start) if start.name == tag.name)
182 })
183 };
184 if let Some(depth) = depth {
185 document.close(&mut open, &mut names, depth);
186 }
187 tag.attributes
188 }
189 Token::Text(text) => {
190 if !text.is_empty() {
191 let index = document.nodes.len();
192 if index >= limits.max_nodes {
193 return Err(Error::NodeLimit);
194 }
195 document.nodes.push(Node {
196 kind: Kind::Text(text),
197 parent: open
198 .last()
199 .and_then(|entry| NonZeroUsize::new(entry.node + 1)),
200 end: index + 1,
201 });
202 }
203 continue;
204 }
205 Token::Comment(_) | Token::Doctype(_) | Token::ProcessingInstruction { .. } => {
206 continue;
207 }
208 };
209 attributes.clear();
210 if attributes.capacity() > attribute_buffer.capacity() {
211 attribute_buffer = attributes;
212 }
213 }
214 document.close(&mut open, &mut names, 0);
215 Ok(document)
216 }
217
218 fn close(
220 &mut self,
221 open: &mut Vec<Open>,
222 names: &mut Option<HashMap<Cow<'a, str>, usize>>,
223 depth: usize,
224 ) {
225 let end = self.nodes.len();
226 while open.len() > depth {
227 let entry = open.pop().expect("nonempty open stack");
228 let node = &mut self.nodes[entry.node];
229 node.end = end;
230 let Kind::Element(tag) = &node.kind else {
231 unreachable!()
232 };
233 if let Some(names) = names {
234 if let Some(previous) = entry.previous {
235 *names
236 .get_mut(tag.name.as_ref())
237 .expect("open name is indexed") = previous;
238 } else {
239 names.remove(tag.name.as_ref());
240 }
241 }
242 }
243 }
244
245 pub fn elements(&self) -> impl Iterator<Item = Element<'_, 'a>> {
247 self.elements_in(0, self.nodes.len())
248 }
249
250 fn elements_in(&self, start: usize, end: usize) -> impl Iterator<Item = Element<'_, 'a>> {
251 (start..end).filter_map(|index| {
252 matches!(self.nodes[index].kind, Kind::Element(_)).then_some(Element {
253 document: self,
254 index,
255 })
256 })
257 }
258}
259
260#[derive(Clone, Copy)]
262pub struct Element<'doc, 'src> {
263 document: &'doc Document<'src>,
264 index: usize,
265}
266
267impl<'doc, 'src> Element<'doc, 'src> {
268 fn data(self) -> &'doc ElementData<'src> {
269 let Kind::Element(tag) = &self.document.nodes[self.index].kind else {
270 unreachable!()
271 };
272 tag
273 }
274
275 pub fn name(self) -> &'doc str {
277 &self.data().name
278 }
279
280 pub fn is(self, name: &str) -> bool {
282 let actual = self.name();
283 actual == name || matches_normalized_name(actual.as_bytes(), name.as_bytes())
284 }
285
286 #[inline]
291 pub fn attribute(self, name: &str) -> Option<&'doc Attribute<'src>> {
292 self.attributes().find(|attribute| {
293 attribute.name == name
294 || matches_normalized_name(attribute.name.as_bytes(), name.as_bytes())
295 })
296 }
297
298 pub fn has_attribute(self, name: &str) -> bool {
300 self.attribute(name).is_some()
301 }
302
303 pub fn attributes(self) -> impl Iterator<Item = &'doc Attribute<'src>> {
305 self.document.attributes[self.data().attributes.clone()].iter()
306 }
307
308 pub fn parent(self) -> Option<Self> {
310 self.document.nodes[self.index].parent.map(|index| Self {
311 document: self.document,
312 index: index.get() - 1,
313 })
314 }
315
316 pub fn descendants(self) -> impl Iterator<Item = Self> {
318 self.document
319 .elements_in(self.index + 1, self.document.nodes[self.index].end)
320 }
321
322 pub fn children(self) -> impl Iterator<Item = Self> {
324 let mut cursor = self.index + 1;
325 let end = self.document.nodes[self.index].end;
326 std::iter::from_fn(move || {
327 while cursor < end {
328 let index = cursor;
329 let node = &self.document.nodes[index];
330 cursor = node.end;
331 if matches!(node.kind, Kind::Element(_)) {
332 return Some(Self {
333 document: self.document,
334 index,
335 });
336 }
337 }
338 None
339 })
340 }
341
342 #[inline]
349 pub fn text(self) -> Cow<'doc, str> {
350 let end = self.document.nodes[self.index].end;
351 let mut segments = self.document.nodes[self.index + 1..end]
352 .iter()
353 .filter_map(|node| {
354 if let Kind::Text(text) = &node.kind {
355 Some(text.as_ref())
356 } else {
357 None
358 }
359 });
360 let Some(first) = segments.next() else {
361 return Cow::Borrowed("");
362 };
363 let Some(second) = segments.next() else {
364 return Cow::Borrowed(first);
365 };
366 Cow::Owned(concatenate_text(first, second, segments))
367 }
368}
369
370#[inline(never)]
372fn concatenate_text<'a>(
373 first: &str,
374 second: &str,
375 remaining: impl Iterator<Item = &'a str>,
376) -> String {
377 let mut text = String::with_capacity(first.len() + second.len());
378 text.push_str(first);
379 text.push_str(second);
380 for segment in remaining {
381 text.push_str(segment);
382 }
383 text
384}
385
386impl fmt::Debug for Element<'_, '_> {
387 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
388 formatter
389 .debug_struct("Element")
390 .field("name", &self.name())
391 .field("index", &self.index)
392 .finish()
393 }
394}
395
396fn is_void(name: &str) -> bool {
397 matches!(
398 name,
399 "area"
400 | "base"
401 | "br"
402 | "col"
403 | "embed"
404 | "hr"
405 | "img"
406 | "input"
407 | "link"
408 | "meta"
409 | "param"
410 | "source"
411 | "track"
412 | "wbr"
413 )
414}