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