1#![allow(dead_code)]
2use alloc::collections::BTreeMap;
5use alloc::string::String;
6use alloc::string::ToString;
7use alloc::vec::Vec;
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum NodeType {
11 Element {
12 tag_name: String,
13 attributes: BTreeMap<String, String>,
14 classes: Vec<String>,
15 id: Option<String>,
16 },
17 Text(String),
18}
19
20#[derive(Debug, Clone)]
21pub struct Node {
22 pub node_type: NodeType,
23 pub children: Vec<Node>,
24}
25
26impl Node {
27 pub fn new_text(text: String) -> Self {
28 Node {
29 node_type: NodeType::Text(text),
30 children: Vec::new(),
31 }
32 }
33
34 pub fn new_element(name: String, attrs: BTreeMap<String, String>) -> Self {
35 let id = attrs.get("id").cloned();
36 let classes = match attrs.get("class") {
37 Some(class_str) => class_str.split_whitespace().map(String::from).collect(),
38 None => Vec::new(),
39 };
40
41 Node {
42 node_type: NodeType::Element {
43 tag_name: name.to_lowercase(),
44 attributes: attrs,
45 classes,
46 id,
47 },
48 children: Vec::new(),
49 }
50 }
51
52 pub fn new_element_direct(
53 name: String,
54 attrs: BTreeMap<String, String>,
55 id: Option<String>,
56 classes: Vec<String>,
57 ) -> Self {
58 Node {
59 node_type: NodeType::Element {
60 tag_name: name.to_lowercase(),
61 attributes: attrs,
62 classes,
63 id,
64 },
65 children: Vec::new(),
66 }
67 }
68
69 pub fn empty_root() -> Self {
73 Node::new_element(String::from("html"), BTreeMap::new())
74 }
75
76 pub fn get_attribute(&self, name: &str) -> Option<&str> {
78 if let NodeType::Element { attributes, id, .. } = &self.node_type {
79 if name == "id" {
80 return id.as_deref();
81 }
82 attributes.get(name).map(|s| s.as_str())
83 } else {
84 None
85 }
86 }
87
88 pub fn dataset(&self) -> BTreeMap<String, String> {
90 if let NodeType::Element { attributes, .. } = &self.node_type {
91 crate::os_lib::dom_dataset::extract_dataset(attributes)
92 } else {
93 BTreeMap::new()
94 }
95 }
96
97 pub fn set_dataset_property(
99 &mut self,
100 prop: &str,
101 value: &str,
102 ) -> Result<String, crate::os_lib::dom_dataset::DatasetError> {
103 if let NodeType::Element { attributes, .. } = &mut self.node_type {
104 crate::os_lib::dom_dataset::set_dataset_property(attributes, prop, value)
105 } else {
106 Ok(String::new())
107 }
108 }
109
110 pub fn remove_dataset_property(
112 &mut self,
113 prop: &str,
114 ) -> Result<Option<String>, crate::os_lib::dom_dataset::DatasetError> {
115 if let NodeType::Element { attributes, .. } = &mut self.node_type {
116 crate::os_lib::dom_dataset::remove_dataset_property(attributes, prop)
117 } else {
118 Ok(None)
119 }
120 }
121
122 const MAX_SEARCH_DEPTH: usize = 256;
123
124 pub fn get_element_by_id(&self, target_id: &str) -> Option<&Node> {
126 self.get_element_by_id_depth(target_id, 0)
127 }
128
129 fn get_element_by_id_depth(&self, target_id: &str, depth: usize) -> Option<&Node> {
130 if depth >= Self::MAX_SEARCH_DEPTH {
131 return None;
132 }
133 if let NodeType::Element { id: Some(id), .. } = &self.node_type {
134 if id == target_id {
135 return Some(self);
136 }
137 }
138 for child in &self.children {
139 if let Some(found) = child.get_element_by_id_depth(target_id, depth + 1) {
140 return Some(found);
141 }
142 }
143 None
144 }
145
146 pub fn get_elements_by_class_name<'a>(&'a self, target_class: &str, out: &mut Vec<&'a Node>) {
148 self.get_elements_by_class_name_depth(target_class, out, 0);
149 }
150
151 fn get_elements_by_class_name_depth<'a>(
152 &'a self,
153 target_class: &str,
154 out: &mut Vec<&'a Node>,
155 depth: usize,
156 ) {
157 if depth >= Self::MAX_SEARCH_DEPTH {
158 return;
159 }
160 if let NodeType::Element { classes, .. } = &self.node_type {
161 if classes.iter().any(|c| c == target_class) {
162 out.push(self);
163 }
164 }
165 for child in &self.children {
166 child.get_elements_by_class_name_depth(target_class, out, depth + 1);
167 }
168 }
169
170 pub fn get_elements_by_tag_name<'a>(&'a self, target_tag: &str, out: &mut Vec<&'a Node>) {
172 self.get_elements_by_tag_name_depth(target_tag, out, 0);
173 }
174
175 fn get_elements_by_tag_name_depth<'a>(
176 &'a self,
177 target_tag: &str,
178 out: &mut Vec<&'a Node>,
179 depth: usize,
180 ) {
181 if depth >= Self::MAX_SEARCH_DEPTH {
182 return;
183 }
184 if let NodeType::Element { tag_name, .. } = &self.node_type {
185 if target_tag == "*" || tag_name.eq_ignore_ascii_case(target_tag) {
186 out.push(self);
187 }
188 }
189 for child in &self.children {
190 child.get_elements_by_tag_name_depth(target_tag, out, depth + 1);
191 }
192 }
193
194 pub fn text_content(&self) -> String {
196 let mut out = String::new();
197 self.collect_text(&mut out, 0);
198 out
199 }
200
201 fn collect_text(&self, out: &mut String, depth: usize) {
202 if depth >= Self::MAX_SEARCH_DEPTH {
203 return;
204 }
205 match &self.node_type {
206 NodeType::Text(t) => out.push_str(t),
207 NodeType::Element { .. } => {
208 for child in &self.children {
209 child.collect_text(out, depth + 1);
210 }
211 }
212 }
213 }
214}
215
216fn get_tag_name(node: &Node) -> Option<&str> {
218 match &node.node_type {
219 NodeType::Element { tag_name, .. } => Some(tag_name.as_str()),
220 _ => None,
221 }
222}
223
224pub fn decode_entities(s: &str) -> String {
226 if !s.contains('&') {
227 return s.to_string();
228 }
229
230 let mut result = String::with_capacity(s.len());
231 let bytes = s.as_bytes();
232 let len = bytes.len();
233 let mut i = 0;
234
235 let legacy_entities: &[(&str, char)] = &[
236 ("trade", '™'),
237 ("pound", '£'),
238 ("cent", '¢'),
239 ("euro", '€'),
240 ("quot", '"'),
241 ("apos", '\''),
242 ("nbsp", ' '),
243 ("copy", '©'),
244 ("amp", '&'),
245 ("yen", '¥'),
246 ("reg", '®'),
247 ("deg", '°'),
248 ("lt", '<'),
249 ("gt", '>'),
250 ];
251
252 while i < len {
253 if bytes[i] == b'&' {
254 let max_end = if i + 13 < len { i + 13 } else { len };
256 let mut semi = i + 1;
257 while semi < max_end && bytes[semi] != b';' {
258 semi += 1;
259 }
260 if semi < max_end && bytes[semi] == b';' {
261 if let Some(entity) = s.get(i + 1..semi) {
262 if let Some(decoded) = decode_named_or_numeric(entity) {
263 result.push(decoded);
264 i = semi + 1;
265 continue;
266 }
267 }
268 }
269
270 let mut decoded_char = None;
272 let mut consumed_len = 0;
273
274 let rest_bytes = &bytes[i + 1..];
275 for &(name, ch) in legacy_entities {
276 let nbytes = name.as_bytes();
277 if rest_bytes.starts_with(nbytes) {
278 let next_idx = nbytes.len();
279 let next_char_ok = if next_idx < rest_bytes.len() {
280 let next_b = rest_bytes[next_idx];
281 next_b != b'=' && !next_b.is_ascii_alphanumeric()
282 } else {
283 true
284 };
285 if next_char_ok {
286 decoded_char = Some(ch);
287 consumed_len = 1 + nbytes.len();
288 break;
289 }
290 }
291 }
292
293 if let Some(ch) = decoded_char {
294 result.push(ch);
295 i += consumed_len;
296 continue;
297 }
298
299 result.push('&');
301 i += 1;
302 } else if bytes[i] < 0x80 {
303 result.push(bytes[i] as char);
304 i += 1;
305 } else {
306 if let Some(c) = s.get(i..).and_then(|sub| sub.chars().next()) {
307 result.push(c);
308 i += c.len_utf8();
309 } else {
310 i += 1;
311 }
312 }
313 }
314
315 result
316}
317
318fn decode_named_or_numeric(entity: &str) -> Option<char> {
320 let ch = match entity {
322 "amp" => Some('&'),
324 "lt" => Some('<'),
325 "gt" => Some('>'),
326 "quot" => Some('"'),
327 "apos" => Some('\''),
328 "nbsp" => Some('\u{00A0}'), "ensp" => Some('\u{2002}'),
331 "emsp" => Some('\u{2003}'),
332 "thinsp" => Some('\u{2009}'),
333 "zwnj" => Some('\u{200C}'),
334 "zwj" => Some('\u{200D}'),
335 "lrm" => Some('\u{200E}'),
336 "rlm" => Some('\u{200F}'),
337 "shy" => Some('\u{00AD}'), "cent" => Some('\u{00A2}'),
340 "pound" => Some('\u{00A3}'),
341 "curren" => Some('\u{00A4}'),
342 "yen" => Some('\u{00A5}'),
343 "euro" => Some('\u{20AC}'),
344 "copy" => Some('\u{00A9}'),
345 "reg" => Some('\u{00AE}'),
346 "trade" => Some('\u{2122}'),
347 "sect" => Some('\u{00A7}'),
348 "para" => Some('\u{00B6}'),
349 "deg" => Some('\u{00B0}'),
351 "plusmn" => Some('\u{00B1}'),
352 "times" => Some('\u{00D7}'),
353 "divide" => Some('\u{00F7}'),
354 "frac12" => Some('\u{00BD}'),
355 "frac14" => Some('\u{00BC}'),
356 "frac34" => Some('\u{00BE}'),
357 "sup1" => Some('\u{00B9}'),
358 "sup2" => Some('\u{00B2}'),
359 "sup3" => Some('\u{00B3}'),
360 "micro" => Some('\u{00B5}'),
361 "infin" => Some('\u{221E}'),
362 "minus" => Some('\u{2212}'),
363 "sdot" => Some('\u{22C5}'),
364 "middot" => Some('\u{00B7}'),
365 "permil" => Some('\u{2030}'),
366 "prime" => Some('\u{2032}'),
367 "Prime" => Some('\u{2033}'),
368 "sum" => Some('\u{2211}'),
369 "prod" => Some('\u{220F}'),
370 "int" => Some('\u{222B}'),
371 "radic" => Some('\u{221A}'),
372 "prop" => Some('\u{221D}'),
373 "part" => Some('\u{2202}'),
374 "nabla" => Some('\u{2207}'),
375 "forall" => Some('\u{2200}'),
376 "exist" => Some('\u{2203}'),
377 "empty" => Some('\u{2205}'),
378 "isin" => Some('\u{2208}'),
379 "notin" => Some('\u{2209}'),
380 "ni" => Some('\u{220B}'),
381 "and" => Some('\u{2227}'),
382 "or" => Some('\u{2228}'),
383 "cap" => Some('\u{2229}'),
384 "cup" => Some('\u{222A}'),
385 "cong" => Some('\u{2245}'),
386 "asymp" => Some('\u{2248}'),
387 "ne" => Some('\u{2260}'),
388 "equiv" => Some('\u{2261}'),
389 "le" => Some('\u{2264}'),
390 "ge" => Some('\u{2265}'),
391 "sub" => Some('\u{2282}'),
392 "sup" => Some('\u{2283}'),
393 "sube" => Some('\u{2286}'),
394 "supe" => Some('\u{2287}'),
395 "oplus" => Some('\u{2295}'),
396 "otimes" => Some('\u{2297}'),
397 "perp" => Some('\u{22A5}'),
398 "sim" => Some('\u{223C}'),
399 "ang" => Some('\u{2220}'),
400 "not" => Some('\u{00AC}'),
401 "ordm" => Some('\u{00BA}'),
403 "ordf" => Some('\u{00AA}'),
404 "macr" => Some('\u{00AF}'),
405 "acute" => Some('\u{00B4}'),
406 "cedil" => Some('\u{00B8}'),
407 "uml" => Some('\u{00A8}'),
408 "iexcl" => Some('\u{00A1}'),
409 "iquest" => Some('\u{00BF}'),
410 "lsquo" => Some('\u{2018}'), "rsquo" => Some('\u{2019}'), "sbquo" => Some('\u{201A}'),
414 "ldquo" => Some('\u{201C}'), "rdquo" => Some('\u{201D}'), "bdquo" => Some('\u{201E}'),
417 "laquo" => Some('\u{00AB}'), "raquo" => Some('\u{00BB}'), "lsaquo" => Some('\u{2039}'),
420 "rsaquo" => Some('\u{203A}'),
421 "ndash" => Some('\u{2013}'), "mdash" => Some('\u{2014}'), "horbar" => Some('\u{2015}'),
424 "hellip" => Some('\u{2026}'), "bull" => Some('\u{2022}'), "dagger" => Some('\u{2020}'),
427 "Dagger" => Some('\u{2021}'),
428 "spades" => Some('\u{2660}'),
430 "clubs" => Some('\u{2663}'),
431 "hearts" => Some('\u{2665}'),
432 "diams" => Some('\u{2666}'),
433 "star" => Some('\u{22C6}'),
434 "larr" => Some('\u{2190}'),
435 "uarr" => Some('\u{2191}'),
436 "rarr" => Some('\u{2192}'),
437 "darr" => Some('\u{2193}'),
438 "harr" => Some('\u{2194}'),
439 "crarr" => Some('\u{21B5}'),
440 "lArr" => Some('\u{21D0}'),
441 "uArr" => Some('\u{21D1}'),
442 "rArr" => Some('\u{21D2}'),
443 "dArr" => Some('\u{21D3}'),
444 "hArr" => Some('\u{21D4}'),
445 "frasl" => Some('\u{2044}'),
446 "weierp" => Some('\u{2118}'),
447 "image" => Some('\u{2111}'),
448 "real" => Some('\u{211C}'),
449 "alefsym" => Some('\u{2135}'),
450 "oline" => Some('\u{203E}'),
451 "Agrave" => Some('\u{00C0}'), "Aacute" => Some('\u{00C1}'),
453 "Acirc" => Some('\u{00C2}'), "Atilde" => Some('\u{00C3}'),
454 "Auml" => Some('\u{00C4}'), "Aring" => Some('\u{00C5}'),
455 "AElig" => Some('\u{00C6}'), "Ccedil" => Some('\u{00C7}'),
456 "Egrave" => Some('\u{00C8}'), "Eacute" => Some('\u{00C9}'),
457 "Ecirc" => Some('\u{00CA}'), "Euml" => Some('\u{00CB}'),
458 "Igrave" => Some('\u{00CC}'), "Iacute" => Some('\u{00CD}'),
459 "Icirc" => Some('\u{00CE}'), "Iuml" => Some('\u{00CF}'),
460 "ETH" => Some('\u{00D0}'), "Ntilde" => Some('\u{00D1}'),
461 "Ograve" => Some('\u{00D2}'), "Oacute" => Some('\u{00D3}'),
462 "Ocirc" => Some('\u{00D4}'), "Otilde" => Some('\u{00D5}'),
463 "Ouml" => Some('\u{00D6}'), "Oslash" => Some('\u{00D8}'),
464 "Ugrave" => Some('\u{00D9}'), "Uacute" => Some('\u{00DA}'),
465 "Ucirc" => Some('\u{00DB}'), "Uuml" => Some('\u{00DC}'),
466 "Yacute" => Some('\u{00DD}'), "THORN" => Some('\u{00DE}'),
467 "szlig" => Some('\u{00DF}'),
468 "agrave" => Some('\u{00E0}'), "aacute" => Some('\u{00E1}'),
469 "acirc" => Some('\u{00E2}'), "atilde" => Some('\u{00E3}'),
470 "auml" => Some('\u{00E4}'), "aring" => Some('\u{00E5}'),
471 "aelig" => Some('\u{00E6}'), "ccedil" => Some('\u{00E7}'),
472 "egrave" => Some('\u{00E8}'), "eacute" => Some('\u{00E9}'),
473 "ecirc" => Some('\u{00EA}'), "euml" => Some('\u{00EB}'),
474 "igrave" => Some('\u{00EC}'), "iacute" => Some('\u{00ED}'),
475 "icirc" => Some('\u{00EE}'), "iuml" => Some('\u{00EF}'),
476 "eth" => Some('\u{00F0}'), "ntilde" => Some('\u{00F1}'),
477 "ograve" => Some('\u{00F2}'), "oacute" => Some('\u{00F3}'),
478 "ocirc" => Some('\u{00F4}'), "otilde" => Some('\u{00F5}'),
479 "ouml" => Some('\u{00F6}'), "oslash" => Some('\u{00F8}'),
480 "ugrave" => Some('\u{00F9}'), "uacute" => Some('\u{00FA}'),
481 "ucirc" => Some('\u{00FB}'), "uuml" => Some('\u{00FC}'),
482 "yacute" => Some('\u{00FD}'), "thorn" => Some('\u{00FE}'),
483 "yuml" => Some('\u{00FF}'),
484 "Alpha" => Some('\u{0391}'), "Beta" => Some('\u{0392}'),
486 "Gamma" => Some('\u{0393}'), "Delta" => Some('\u{0394}'),
487 "Epsilon" => Some('\u{0395}'), "Zeta" => Some('\u{0396}'),
488 "Eta" => Some('\u{0397}'), "Theta" => Some('\u{0398}'),
489 "Iota" => Some('\u{0399}'), "Kappa" => Some('\u{039A}'),
490 "Lambda" => Some('\u{039B}'), "Mu" => Some('\u{039C}'),
491 "Nu" => Some('\u{039D}'), "Xi" => Some('\u{039E}'),
492 "Omicron" => Some('\u{039F}'), "Pi" => Some('\u{03A0}'),
493 "Rho" => Some('\u{03A1}'), "Sigma" => Some('\u{03A3}'),
494 "Tau" => Some('\u{03A4}'), "Upsilon" => Some('\u{03A5}'),
495 "Phi" => Some('\u{03A6}'), "Chi" => Some('\u{03A7}'),
496 "Psi" => Some('\u{03A8}'), "Omega" => Some('\u{03A9}'),
497 "alpha" => Some('\u{03B1}'), "beta" => Some('\u{03B2}'),
498 "gamma" => Some('\u{03B3}'), "delta" => Some('\u{03B4}'),
499 "epsilon" => Some('\u{03B5}'), "zeta" => Some('\u{03B6}'),
500 "eta" => Some('\u{03B7}'), "theta" => Some('\u{03B8}'),
501 "iota" => Some('\u{03B9}'), "kappa" => Some('\u{03BA}'),
502 "lambda" => Some('\u{03BB}'), "mu" => Some('\u{03BC}'),
503 "nu" => Some('\u{03BD}'), "xi" => Some('\u{03BE}'),
504 "omicron" => Some('\u{03BF}'), "pi" => Some('\u{03C0}'),
505 "rho" => Some('\u{03C1}'), "sigmaf" => Some('\u{03C2}'),
506 "sigma" => Some('\u{03C3}'), "tau" => Some('\u{03C4}'),
507 "upsilon" => Some('\u{03C5}'), "phi" => Some('\u{03C6}'),
508 "chi" => Some('\u{03C7}'), "psi" => Some('\u{03C8}'),
509 "omega" => Some('\u{03C9}'), "thetasym"=> Some('\u{03D1}'),
510 "upsih" => Some('\u{03D2}'), "piv" => Some('\u{03D6}'),
511 _ => None,
512 };
513 if ch.is_some() {
514 return ch;
515 }
516
517 if let Some(num_str) = entity.strip_prefix('#') {
519 let code = if num_str.starts_with('x') || num_str.starts_with('X') {
520 u32::from_str_radix(num_str.get(1..).unwrap_or(""), 16).ok()
522 } else {
523 parse_u32(num_str)
525 };
526 if let Some(c) = code {
527 return char::from_u32(c);
528 }
529 }
530
531 None
532}
533
534fn parse_u32(s: &str) -> Option<u32> {
536 let mut result: u32 = 0;
537 if s.is_empty() {
538 return None;
539 }
540 for b in s.bytes() {
541 if !b.is_ascii_digit() {
542 return None;
543 }
544 result = result.checked_mul(10)?.checked_add((b - b'0') as u32)?;
545 }
546 Some(result)
547}
548
549fn find_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
552 let mut i = start;
553 let mut in_quote: Option<u8> = None;
554
555 while i < bytes.len() {
556 let b = bytes[i];
557 match in_quote {
558 Some(q) => {
559 if b == q {
560 in_quote = None;
561 }
562 }
563 None => {
564 if b == b'"' || b == b'\'' {
565 in_quote = Some(b);
566 } else if b == b'>' {
567 return Some(i);
568 }
569 }
570 }
571 i += 1;
572 }
573 None
574}
575
576fn parse_opening_tag_full(tag_trim: &str) -> (Node, bool) {
578 let bytes = tag_trim.as_bytes();
579 let mut i = 0usize;
580
581 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
583 i += 1;
584 }
585
586 let name_start = i;
588 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'/' && bytes[i] != b'>'
589 {
590 i += 1;
591 }
592 let name = if name_start < i {
593 String::from(tag_trim.get(name_start..i).unwrap_or(""))
594 } else {
595 String::from("div")
596 };
597
598 let mut attrs = BTreeMap::new();
599 let mut is_self_closing = false;
600
601 while i < bytes.len() {
602 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
604 i += 1;
605 }
606 if i >= bytes.len() {
607 break;
608 }
609
610 if bytes[i] == b'/' {
612 let rest = tag_trim.get(i + 1..).unwrap_or("").trim();
614 if rest.is_empty() {
615 is_self_closing = true;
616 break;
617 }
618 i += 1;
620 continue;
621 }
622
623 let key_start = i;
625 while i < bytes.len()
626 && !bytes[i].is_ascii_whitespace()
627 && bytes[i] != b'='
628 && bytes[i] != b'/'
629 && bytes[i] != b'>'
630 {
631 i += 1;
632 }
633 if key_start == i {
634 break;
635 }
636 let key = String::from(tag_trim.get(key_start..i).unwrap_or(""));
637
638 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
640 i += 1;
641 }
642
643 let mut value = String::new();
644 if i < bytes.len() && bytes[i] == b'=' {
645 i += 1;
646 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
648 i += 1;
649 }
650
651 if i < bytes.len() && (bytes[i] == b'"' || bytes[i] == b'\'') {
652 let quote = bytes[i];
653 i += 1;
654 let val_start = i;
655 while i < bytes.len() && bytes[i] != quote {
656 i += 1;
657 }
658 value = decode_entities(tag_trim.get(val_start..i).unwrap_or(""));
659 if i < bytes.len() && bytes[i] == quote {
660 i += 1;
661 }
662 } else {
663 let val_start = i;
665 while i < bytes.len()
666 && !bytes[i].is_ascii_whitespace()
667 && bytes[i] != b'/'
668 && bytes[i] != b'>'
669 {
670 i += 1;
671 }
672 value = decode_entities(tag_trim.get(val_start..i).unwrap_or(""));
673 }
674 }
675
676 if !key.is_empty() {
677 attrs.insert(key, value);
678 }
679 }
680
681 (Node::new_element(name, attrs), is_self_closing)
682}
683
684fn skip_raw_content(html: &str, start: usize, tag_name: &str) -> usize {
688 let bytes = html.as_bytes();
689 let name_bytes = tag_name.as_bytes();
690 let tlen = 3 + name_bytes.len(); let mut i = start;
693 while i + tlen <= bytes.len() {
694 if bytes[i] == b'<' && bytes[i + 1] == b'/' {
695 let mut matched = true;
696 for (k, &nb) in name_bytes.iter().enumerate() {
697 if bytes[i + 2 + k].to_ascii_lowercase() != nb.to_ascii_lowercase() {
698 matched = false;
699 break;
700 }
701 }
702 if matched && bytes[i + 2 + name_bytes.len()] == b'>' {
703 return i + tlen;
704 }
705 }
706 i += 1;
707 }
708 bytes.len()
709}
710
711pub fn parse_html(html: &str) -> Node {
713 let mut root = Node::new_element(String::from("html"), BTreeMap::new());
714 let mut stack: Vec<Node> = Vec::new();
715
716 let bytes = html.as_bytes();
717 let mut i = 0;
718
719 while i < bytes.len() {
720 if bytes[i] == b'<' {
721 if i + 3 < bytes.len()
723 && bytes[i + 1] == b'!'
724 && bytes[i + 2] == b'-'
725 && bytes[i + 3] == b'-'
726 {
727 let mut j = i + 4;
729 loop {
730 if j + 2 >= bytes.len() {
731 j = bytes.len();
732 break;
733 }
734 if bytes[j] == b'-' && bytes[j + 1] == b'-' && bytes[j + 2] == b'>' {
735 j += 3;
736 break;
737 }
738 j += 1;
739 }
740 i = j;
741 continue;
742 }
743
744 if i + 1 < bytes.len() && bytes[i + 1] == b'!' {
746 if let Some(j) = find_tag_end(bytes, i + 1) {
747 i = j + 1;
748 } else {
749 i += 1;
750 }
751 continue;
752 }
753
754 if let Some(j) = find_tag_end(bytes, i + 1) {
756 let tag_content = html.get(i + 1..j).unwrap_or("");
757 let tag_trim = tag_content.trim();
758
759 if tag_trim.is_empty() {
760 i = j + 1;
761 continue;
762 }
763
764 if tag_trim.starts_with('/') {
765 let close_name = tag_trim
767 .get(1..)
768 .unwrap_or("")
769 .split_whitespace()
770 .next()
771 .unwrap_or("")
772 .to_lowercase();
773
774 if is_void_tag_name(&close_name) {
775 i = j + 1;
776 continue;
777 }
778
779 let mut found_idx: Option<usize> = None;
781 for idx in (0..stack.len()).rev() {
782 if let Some(sname) = get_tag_name(&stack[idx]) {
783 if sname == close_name {
784 found_idx = Some(idx);
785 break;
786 }
787 }
788 }
789
790 if let Some(target_idx) = found_idx {
791 while stack.len() > target_idx + 1 {
793 if let Some(child) = stack.pop() {
794 if let Some(parent) = stack.last_mut() {
795 parent.children.push(child);
796 } else {
797 root.children.push(child);
798 }
799 }
800 }
801 if let Some(child) = stack.pop() {
803 if let Some(parent) = stack.last_mut() {
804 parent.children.push(child);
805 } else {
806 root.children.push(child);
807 }
808 }
809 }
810 } else {
812 let (node, is_self_closing) = parse_opening_tag_full(tag_trim);
814
815 let tag_name_lower = match &node.node_type {
816 NodeType::Element { tag_name, .. } => tag_name.clone(),
817 _ => String::new(),
818 };
819
820 let is_void = is_void_tag_name(&tag_name_lower);
821
822 if is_self_closing || is_void {
823 if let Some(parent) = stack.last_mut() {
825 parent.children.push(node);
826 } else {
827 root.children.push(node);
828 }
829 } else if matches!(tag_name_lower.as_str(), "script" | "style" | "textarea" | "noscript" | "title") {
830 let skip_to = skip_raw_content(html, j + 1, &tag_name_lower);
836 let close_tag_len = 3 + tag_name_lower.len(); let content_end = skip_to.saturating_sub(close_tag_len).max(j + 1);
838 let mut node = node;
839 if content_end > j + 1 {
840 let raw_text = html.get(j + 1..content_end).unwrap_or("");
841 node.children.push(Node::new_text(String::from(raw_text)));
842 }
843 if let Some(parent) = stack.last_mut() {
844 parent.children.push(node);
845 } else {
846 root.children.push(node);
847 }
848 i = skip_to;
849 continue;
850 } else {
851 fn find_implicitly_closeable(
854 stack: &[Node],
855 target: &str,
856 stop_at: &[&str],
857 ) -> Option<usize> {
858 for idx in (0..stack.len()).rev() {
859 if let Some(sname) = get_tag_name(&stack[idx]) {
860 if sname == target {
861 return Some(idx);
862 }
863 if stop_at.contains(&sname) {
864 return None;
865 }
866 }
867 }
868 None
869 }
870
871 fn pop_to(stack: &mut Vec<Node>, root: &mut Node, target_idx: usize) {
872 while stack.len() > target_idx {
873 if let Some(child) = stack.pop() {
874 if let Some(parent) = stack.last_mut() {
875 parent.children.push(child);
876 } else {
877 root.children.push(child);
878 }
879 }
880 }
881 }
882
883 if tag_name_lower == "li" {
884 if let Some(idx) =
885 find_implicitly_closeable(&stack, "li", &["ul", "ol"])
886 {
887 pop_to(&mut stack, &mut root, idx);
888 }
889 } else if tag_name_lower == "tr" {
890 if let Some(idx) = find_implicitly_closeable(
891 &stack,
892 "tr",
893 &["table", "thead", "tbody", "tfoot"],
894 ) {
895 pop_to(&mut stack, &mut root, idx);
896 }
897 } else if matches!(tag_name_lower.as_str(), "tbody" | "thead" | "tfoot" | "caption" | "colgroup") {
898 if let Some(idx) = find_implicitly_closeable(&stack, "tbody", &["table"])
900 .or_else(|| find_implicitly_closeable(&stack, "thead", &["table"]))
901 .or_else(|| find_implicitly_closeable(&stack, "tfoot", &["table"]))
902 .or_else(|| find_implicitly_closeable(&stack, "caption", &["table"]))
903 .or_else(|| find_implicitly_closeable(&stack, "colgroup", &["table"]))
904 {
905 pop_to(&mut stack, &mut root, idx);
906 }
907 } else if matches!(tag_name_lower.as_str(), "rt" | "rp") {
908 if let Some(idx) = find_implicitly_closeable(&stack, "rt", &["ruby"])
909 .or_else(|| find_implicitly_closeable(&stack, "rp", &["ruby"]))
910 {
911 pop_to(&mut stack, &mut root, idx);
912 }
913 } else if matches!(tag_name_lower.as_str(), "td" | "th") {
914 if let Some(idx) =
915 find_implicitly_closeable(&stack, &tag_name_lower, &["tr", "table"])
916 {
917 pop_to(&mut stack, &mut root, idx);
918 }
919 } else if tag_name_lower == "option" {
920 if let Some(idx) =
921 find_implicitly_closeable(&stack, "option", &["select", "datalist"])
922 {
923 pop_to(&mut stack, &mut root, idx);
924 }
925 } else if tag_name_lower == "dt" || tag_name_lower == "dd" {
926 if let Some(idx) = find_implicitly_closeable(&stack, "dt", &["dl"])
927 .or_else(|| find_implicitly_closeable(&stack, "dd", &["dl"]))
928 {
929 pop_to(&mut stack, &mut root, idx);
930 }
931 } else if matches!(
932 tag_name_lower.as_str(),
933 "p" | "div"
934 | "ul"
935 | "ol"
936 | "h1"
937 | "h2"
938 | "h3"
939 | "h4"
940 | "h5"
941 | "h6"
942 | "pre"
943 | "blockquote"
944 | "section"
945 | "article"
946 | "nav"
947 | "aside"
948 | "header"
949 | "footer"
950 | "main"
951 ) {
952 let mut p_idx = None;
954 for idx in (0..stack.len()).rev() {
955 if let Some(sname) = get_tag_name(&stack[idx]) {
956 if sname == "p" {
957 p_idx = Some(idx);
958 break;
959 }
960 if !matches!(
961 sname,
962 "a" | "span"
963 | "b"
964 | "strong"
965 | "i"
966 | "em"
967 | "code"
968 | "small"
969 | "sub"
970 | "sup"
971 | "abbr"
972 | "cite"
973 | "q"
974 | "s"
975 | "u"
976 ) {
977 break;
978 }
979 }
980 }
981 if let Some(target_idx) = p_idx {
982 pop_to(&mut stack, &mut root, target_idx);
983 }
984 }
985
986 if tag_name_lower == "tr" {
988 if let Some(parent) = stack.last() {
989 if let Some(pname) = get_tag_name(parent) {
990 if pname == "table" {
991 stack.push(Node::new_element(String::from("tbody"), alloc::collections::BTreeMap::new()));
992 }
993 }
994 }
995 } else if matches!(tag_name_lower.as_str(), "td" | "th") {
996 if let Some(parent) = stack.last() {
997 if let Some(pname) = get_tag_name(parent) {
998 if matches!(pname, "table" | "thead" | "tbody" | "tfoot") {
999 if pname == "table" {
1000 stack.push(Node::new_element(String::from("tbody"), alloc::collections::BTreeMap::new()));
1001 }
1002 stack.push(Node::new_element(String::from("tr"), alloc::collections::BTreeMap::new()));
1003 }
1004 }
1005 }
1006 } else if tag_name_lower == "col" {
1007 if let Some(parent) = stack.last() {
1008 if let Some(pname) = get_tag_name(parent) {
1009 if pname == "table" {
1010 stack.push(Node::new_element(String::from("colgroup"), alloc::collections::BTreeMap::new()));
1011 }
1012 }
1013 }
1014 }
1015
1016 stack.push(node);
1018 }
1019 }
1020 i = j + 1;
1021 continue;
1022 } else {
1023 i += 1;
1025 continue;
1026 }
1027 }
1028
1029 let mut j = i;
1031 while j < bytes.len() && bytes[j] != b'<' {
1032 j += 1;
1033 }
1034 let text_raw = html.get(i..j).unwrap_or("");
1035 let text_decoded = decode_entities(text_raw);
1036
1037 let mut is_inside_pre = false;
1039 for idx in (0..stack.len()).rev() {
1040 if let Some(sname) = get_tag_name(&stack[idx]) {
1041 if sname == "pre" {
1042 is_inside_pre = true;
1043 break;
1044 }
1045 }
1046 }
1047
1048 if is_inside_pre {
1049 let node = Node::new_text(text_decoded);
1050 if let Some(parent) = stack.last_mut() {
1051 parent.children.push(node);
1052 } else {
1053 root.children.push(node);
1054 }
1055 } else {
1056 let text_trim = text_decoded
1057 .trim_matches(|c: char| c == '\r' || c == '\n')
1058 .trim()
1059 .to_string();
1060 if !text_trim.is_empty() {
1061 let has_leading = text_decoded.chars().next().is_some_and(|c| c.is_whitespace());
1062 let has_trailing = text_decoded.chars().last().is_some_and(|c| c.is_whitespace());
1063 let mut text = String::new();
1064 if has_leading {
1065 text.push(' ');
1066 }
1067 text.push_str(&text_trim);
1068 if has_trailing {
1069 text.push(' ');
1070 }
1071 let node = Node::new_text(text);
1072 if let Some(parent) = stack.last_mut() {
1073 parent.children.push(node);
1074 } else {
1075 root.children.push(node);
1076 }
1077 } else if !text_decoded.is_empty() {
1078 if let Some(parent) = stack.last_mut() {
1083 let structural = get_tag_name(parent).is_some_and(|t| {
1084 matches!(
1085 t,
1086 "html" | "head" | "table" | "thead" | "tbody" | "tfoot" | "tr"
1087 | "colgroup" | "select" | "datalist" | "optgroup"
1088 )
1089 });
1090 if !structural {
1091 parent.children.push(Node::new_text(String::from(" ")));
1092 }
1093 }
1094 }
1095 }
1096 i = j;
1097 }
1098
1099 while let Some(node) = stack.pop() {
1101 if let Some(parent) = stack.last_mut() {
1102 parent.children.push(node);
1103 } else {
1104 root.children.push(node);
1105 }
1106 }
1107
1108 root
1109}
1110
1111fn is_void_tag_name(name: &str) -> bool {
1112 matches!(
1113 name,
1114 "br" | "hr"
1115 | "img"
1116 | "input"
1117 | "meta"
1118 | "link"
1119 | "source"
1120 | "area"
1121 | "base"
1122 | "col"
1123 | "embed"
1124 | "param"
1125 | "track"
1126 | "wbr"
1127 )
1128}