1use crate::types::DOMNodeInfo;
2
3fn decode_html_entities(s: &str) -> String {
5 let mut result = String::with_capacity(s.len());
6 let bytes = s.as_bytes();
7 let mut i = 0;
8
9 while i < bytes.len() {
10 if bytes[i] == b'&' {
11 if i + 2 < bytes.len() && bytes[i + 1] == b'#' {
13 let start = i + 2;
14 let mut end = start;
15 while end < bytes.len() && end < start + 10 {
16 if bytes[end] == b';' {
17 break;
18 }
19 end += 1;
20 }
21
22 if end < bytes.len() && bytes[end] == b';' {
23 let entity = &s[start..end];
24
25 let value = if entity.starts_with('x') || entity.starts_with('X') {
27 let hex = &entity[1..];
29 u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
30 } else {
31 entity.parse::<u32>().ok().and_then(char::from_u32)
33 };
34
35 if let Some(c) = value {
36 result.push(c);
37 i = end + 1;
38 continue;
39 }
40 }
41 } else {
42 let named_entities = [
44 ("nbsp", '\u{00A0}'),
45 ("amp", '&'),
46 ("lt", '<'),
47 ("gt", '>'),
48 ("quot", '"'),
49 ("apos", '\''),
50 ("copy", '©'),
51 ("reg", '®'),
52 ("trade", '™'),
53 ("ndash", '–'),
54 ("mdash", '—'),
55 ("hellip", '…'),
56 ("laquo", '«'),
57 ("raquo", '»'),
58 ("euro", '€'),
59 ("pound", '£'),
60 ("yen", '¥'),
61 ];
62
63 let mut found = false;
64 for (name, char) in named_entities {
65 let name_len = name.len();
66 if i + 1 + name_len < bytes.len() && bytes[i + 1..].starts_with(name.as_bytes())
68 && bytes[i + 1 + name_len] == b';'
69 {
70 result.push(char);
71 i += 1 + name_len + 1;
72 found = true;
73 break;
74 }
75 }
76
77 if found {
78 continue;
79 }
80 }
81 }
82
83 result.push(bytes[i] as char);
84 i += 1;
85 }
86
87 result
88}
89
90#[derive(Debug, Clone)]
91pub struct DomNode {
92 pub tag: String,
93 pub id: Option<String>,
94 pub classes: Vec<String>,
95 pub start: usize,
96 pub end: usize,
97 pub parent: Option<usize>,
98 pub children: Vec<usize>,
99}
100
101#[derive(Debug, Clone)]
102pub struct DomTree {
103 nodes: Vec<DomNode>,
104 roots: Vec<usize>,
105}
106
107#[derive(Debug, Clone)]
108pub struct StyleBlock {
109 pub content: String,
110 pub content_start: usize,
111}
112
113#[derive(Debug, Clone)]
114pub struct InlineStyle {
115 pub value: String,
116 pub value_start: usize,
117 pub attribute_start: usize,
118}
119
120#[derive(Debug, Clone)]
121pub struct HtmlParseResult {
122 pub dom_tree: DomTree,
123 pub style_blocks: Vec<StyleBlock>,
124 pub inline_styles: Vec<InlineStyle>,
125}
126
127impl DomTree {
128 pub fn parse(html: &str) -> HtmlParseResult {
129 let bytes = html.as_bytes();
130 let len = bytes.len();
131 let mut i = 0;
132 let mut comment_depth = 0usize;
133 let mut nodes: Vec<DomNode> = Vec::new();
134 let mut roots: Vec<usize> = Vec::new();
135 let mut stack: Vec<usize> = Vec::new();
136 let mut style_blocks = Vec::new();
137 let mut inline_styles = Vec::new();
138
139 while i < len {
140 if comment_depth > 0 {
141 if starts_with(bytes, i, b"<!--") {
142 comment_depth += 1;
143 i += 4;
144 continue;
145 }
146 if starts_with(bytes, i, b"-->") {
147 comment_depth -= 1;
148 i += 3;
149 continue;
150 }
151 i += 1;
152 continue;
153 }
154
155 if starts_with(bytes, i, b"<!--") {
156 comment_depth = 1;
157 i += 4;
158 continue;
159 }
160
161 if bytes[i] != b'<' {
162 i += 1;
163 continue;
164 }
165
166 if i + 1 >= len {
167 break;
168 }
169
170 if bytes[i + 1] == b'/' {
171 if let Some((tag_name, end_pos)) = parse_end_tag(html, i) {
173 let mut match_index = None;
174 for (pos, node_idx) in stack.iter().enumerate().rev() {
175 if nodes[*node_idx].tag == tag_name {
176 match_index = Some(pos);
177 break;
178 }
179 }
180 if let Some(pos) = match_index {
181 let node_idx = stack[pos];
182 nodes[node_idx].end = end_pos;
183 stack.truncate(pos);
184 }
185 i = end_pos;
186 continue;
187 }
188 }
189
190 if bytes[i + 1] == b'!' {
191 if let Some(end_pos) = find_char(bytes, i + 2, b'>') {
193 i = end_pos + 1;
194 continue;
195 }
196 }
197
198 let tag_start = i;
199 i += 1;
200 while i < len && bytes[i].is_ascii_whitespace() {
201 i += 1;
202 }
203 let name_start = i;
204 while i < len && is_tag_name_char(bytes[i]) {
205 i += 1;
206 }
207 if name_start == i {
208 i += 1;
209 continue;
210 }
211 let tag_name = html[name_start..i].to_lowercase();
212
213 let mut id: Option<String> = None;
214 let mut classes: Vec<String> = Vec::new();
215 let mut self_closing = false;
216
217 while i < len {
218 while i < len && bytes[i].is_ascii_whitespace() {
219 i += 1;
220 }
221 if i >= len {
222 break;
223 }
224 if bytes[i] == b'/' && i + 1 < len && bytes[i + 1] == b'>' {
225 self_closing = true;
226 i += 2;
227 break;
228 }
229 if bytes[i] == b'>' {
230 i += 1;
231 break;
232 }
233
234 let attr_name_start = i;
235 while i < len && is_attr_name_char(bytes[i]) {
236 i += 1;
237 }
238 if attr_name_start == i {
239 i += 1;
240 continue;
241 }
242 let attr_name = html[attr_name_start..i].to_lowercase();
243 while i < len && bytes[i].is_ascii_whitespace() {
244 i += 1;
245 }
246
247 let mut value: Option<String> = None;
248 let mut value_start = None;
249 if i < len && bytes[i] == b'=' {
250 i += 1;
251 while i < len && bytes[i].is_ascii_whitespace() {
252 i += 1;
253 }
254 if i < len && (bytes[i] == b'"' || bytes[i] == b'\'') {
255 let quote = bytes[i];
256 i += 1;
257 let start = i;
258 while i < len && bytes[i] != quote {
259 i += 1;
260 }
261 let end = i.min(len);
262 let raw_value = html[start..end].to_string();
263 value = Some(decode_html_entities(&raw_value));
264 value_start = Some(start);
265 if i < len {
266 i += 1;
267 }
268 } else {
269 let start = i;
270 while i < len && !bytes[i].is_ascii_whitespace() && bytes[i] != b'>' {
271 i += 1;
272 }
273 let end = i;
274 let raw_value = html[start..end].to_string();
275 value = Some(decode_html_entities(&raw_value));
276 value_start = Some(start);
277 }
278 }
279
280 match attr_name.as_str() {
281 "id" => {
282 if let Some(v) = &value {
283 if !v.is_empty() {
284 id = Some(v.to_string());
285 }
286 }
287 }
288 "class" | "classname" => {
289 if let Some(v) = &value {
290 classes.extend(v.split_whitespace().map(|c| c.to_string()));
291 }
292 }
293 "style" => {
294 if let (Some(v), Some(v_start)) = (value.clone(), value_start) {
295 inline_styles.push(InlineStyle {
296 value: v,
297 value_start: v_start,
298 attribute_start: attr_name_start,
299 });
300 }
301 }
302 _ => {}
303 }
304 }
305
306 let tag_end = i;
307 let node_idx = nodes.len();
308 let parent = stack.last().copied();
309 nodes.push(DomNode {
310 tag: tag_name.clone(),
311 id,
312 classes,
313 start: tag_start,
314 end: tag_end,
315 parent,
316 children: Vec::new(),
317 });
318
319 if let Some(parent_idx) = parent {
320 nodes[parent_idx].children.push(node_idx);
321 } else {
322 roots.push(node_idx);
323 }
324
325 if tag_name == "style" {
326 if let Some((content_start, content_end, close_end)) =
327 find_block_content(html, tag_end, "style")
328 {
329 let content = html[content_start..content_end].to_string();
330 style_blocks.push(StyleBlock {
331 content,
332 content_start,
333 });
334 nodes[node_idx].end = close_end;
335 i = close_end;
336 continue;
337 }
338 }
339
340 if tag_name == "script" {
341 if let Some((_, _, close_end)) = find_block_content(html, tag_end, "script") {
342 nodes[node_idx].end = close_end;
343 i = close_end;
344 continue;
345 }
346 }
347
348 if self_closing || is_void_tag(&tag_name) {
349 nodes[node_idx].end = tag_end;
350 } else {
351 stack.push(node_idx);
352 }
353 }
354
355 let final_end = html.len();
356 for idx in stack {
357 if nodes[idx].end < final_end {
358 nodes[idx].end = final_end;
359 }
360 }
361
362 HtmlParseResult {
363 dom_tree: DomTree { nodes, roots },
364 style_blocks,
365 inline_styles,
366 }
367 }
368
369 pub fn find_node_at_position(&self, position: usize) -> Option<DOMNodeInfo> {
370 for &root_idx in &self.roots {
371 if let Some(info) = self.find_node_recursive(root_idx, position) {
372 return Some(info);
373 }
374 }
375 None
376 }
377
378 fn find_node_recursive(&self, idx: usize, position: usize) -> Option<DOMNodeInfo> {
379 let node = &self.nodes[idx];
380 if position < node.start || position > node.end {
381 return None;
382 }
383 for &child in &node.children {
384 if let Some(found) = self.find_node_recursive(child, position) {
385 return Some(found);
386 }
387 }
388 Some(self.to_info(idx))
389 }
390
391 pub fn matches_selector(&self, node_index: usize, selector: &str) -> bool {
392 let selector = selector.trim();
393 if selector.is_empty() {
394 return false;
395 }
396 if selector == ":root" {
397 return true;
398 }
399 let selectors: Vec<&str> = selector.split(',').map(|s| s.trim()).collect();
400 for sel in selectors {
401 if sel.is_empty() {
402 continue;
403 }
404 let parts = parse_selector_parts(sel);
405 if matches_selector_parts(self, node_index, &parts) {
406 return true;
407 }
408 }
409 false
410 }
411
412 fn to_info(&self, idx: usize) -> DOMNodeInfo {
413 let node = &self.nodes[idx];
414 DOMNodeInfo {
415 tag: node.tag.clone(),
416 id: node.id.clone(),
417 classes: node.classes.clone(),
418 position: node.start,
419 node_index: Some(idx),
420 }
421 }
422}
423
424#[derive(Debug, Clone, Copy)]
425enum Combinator {
426 Descendant,
427 Child,
428}
429
430#[derive(Debug, Clone)]
431struct SimpleSelector {
432 tag: Option<String>,
433 id: Option<String>,
434 classes: Vec<String>,
435}
436
437#[derive(Debug, Clone)]
438struct SelectorPart {
439 combinator: Combinator,
440 selector: SimpleSelector,
441}
442
443fn parse_selector_parts(selector: &str) -> Vec<SelectorPart> {
444 let mut tokens: Vec<String> = Vec::new();
445 let mut current = String::new();
446 let mut in_attr = 0usize;
447 let mut in_paren = 0usize;
448 let mut last_was_space = false;
449
450 for ch in selector.chars() {
451 match ch {
452 '[' => {
453 in_attr += 1;
454 current.push(ch);
455 last_was_space = false;
456 }
457 ']' => {
458 in_attr = in_attr.saturating_sub(1);
459 current.push(ch);
460 last_was_space = false;
461 }
462 '(' => {
463 in_paren += 1;
464 current.push(ch);
465 last_was_space = false;
466 }
467 ')' => {
468 in_paren = in_paren.saturating_sub(1);
469 current.push(ch);
470 last_was_space = false;
471 }
472 '>' if in_attr == 0 && in_paren == 0 => {
473 if !current.trim().is_empty() {
474 tokens.push(current.trim().to_string());
475 }
476 tokens.push(">".to_string());
477 current.clear();
478 last_was_space = false;
479 }
480 ch if ch.is_whitespace() && in_attr == 0 && in_paren == 0 => {
481 if !current.trim().is_empty() {
482 tokens.push(current.trim().to_string());
483 current.clear();
484 }
485 if !last_was_space {
486 tokens.push(" ".to_string());
487 last_was_space = true;
488 }
489 }
490 _ => {
491 current.push(ch);
492 last_was_space = false;
493 }
494 }
495 }
496
497 if !current.trim().is_empty() {
498 tokens.push(current.trim().to_string());
499 }
500
501 let mut parts = Vec::new();
502 let mut next_combinator = Combinator::Descendant;
503
504 for token in tokens {
505 if token == ">" {
506 next_combinator = Combinator::Child;
507 continue;
508 }
509 if token == " " {
510 next_combinator = Combinator::Descendant;
511 continue;
512 }
513 let selector = parse_simple_selector(&token);
514 parts.push(SelectorPart {
515 combinator: next_combinator,
516 selector,
517 });
518 next_combinator = Combinator::Descendant;
519 }
520
521 parts
522}
523
524fn parse_simple_selector(token: &str) -> SimpleSelector {
525 let mut tag: Option<String> = None;
526 let mut id: Option<String> = None;
527 let mut classes: Vec<String> = Vec::new();
528
529 let mut slice = token;
530 if let Some(idx) = slice.find([':', '[']) {
531 slice = &slice[..idx];
532 }
533
534 let mut current = String::new();
535 let mut mode = 't'; for ch in slice.chars() {
538 match ch {
539 '#' => {
540 if mode == 't' && !current.is_empty() && tag.is_none() {
541 tag = Some(current.clone());
542 } else if mode == 'c' && !current.is_empty() {
543 classes.push(current.clone());
544 }
545 current.clear();
546 mode = 'i';
547 }
548 '.' => {
549 if mode == 't' && !current.is_empty() && tag.is_none() {
550 tag = Some(current.clone());
551 } else if mode == 'i' && !current.is_empty() {
552 id = Some(current.clone());
553 } else if mode == 'c' && !current.is_empty() {
554 classes.push(current.clone());
555 }
556 current.clear();
557 mode = 'c';
558 }
559 _ => current.push(ch),
560 }
561 }
562
563 if !current.is_empty() {
564 match mode {
565 't' if current != "*" => {
566 tag = Some(current);
567 }
568 'i' => id = Some(current),
569 'c' => classes.push(current),
570 _ => {}
571 }
572 }
573
574 SimpleSelector { tag, id, classes }
575}
576
577fn matches_selector_parts(tree: &DomTree, node_index: usize, parts: &[SelectorPart]) -> bool {
578 if parts.is_empty() {
579 return false;
580 }
581
582 let mut current_index = Some(node_index);
583 for (idx, part) in parts.iter().enumerate().rev() {
584 let node_idx = match current_index {
585 Some(i) => i,
586 None => return false,
587 };
588 if !matches_simple_selector(&tree.nodes[node_idx], &part.selector) {
589 return false;
590 }
591
592 if idx == 0 {
593 return true;
594 }
595
596 let next_part = &parts[idx - 1];
597 match part.combinator {
598 Combinator::Child => {
599 current_index = tree.nodes[node_idx].parent;
600 if let Some(parent_idx) = current_index {
601 if !matches_simple_selector(&tree.nodes[parent_idx], &next_part.selector) {
602 return false;
603 }
604 } else {
605 return false;
606 }
607 }
608 Combinator::Descendant => {
609 let mut parent = tree.nodes[node_idx].parent;
610 let mut matched = false;
611 while let Some(parent_idx) = parent {
612 if matches_simple_selector(&tree.nodes[parent_idx], &next_part.selector) {
613 matched = true;
614 current_index = Some(parent_idx);
615 break;
616 }
617 parent = tree.nodes[parent_idx].parent;
618 }
619 if !matched {
620 return false;
621 }
622 }
623 }
624 }
625
626 true
627}
628
629fn matches_simple_selector(node: &DomNode, selector: &SimpleSelector) -> bool {
630 if let Some(tag) = &selector.tag {
631 if !node.tag.eq_ignore_ascii_case(tag) {
632 return false;
633 }
634 }
635 if let Some(id) = &selector.id {
636 if node.id.as_deref() != Some(id) {
637 return false;
638 }
639 }
640 for class in &selector.classes {
641 if !node.classes.iter().any(|c| c == class) {
642 return false;
643 }
644 }
645 true
646}
647
648fn is_tag_name_char(b: u8) -> bool {
649 b.is_ascii_alphanumeric() || b == b'-' || b == b':'
650}
651
652fn is_attr_name_char(b: u8) -> bool {
653 b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b':'
654}
655
656fn is_void_tag(tag: &str) -> bool {
657 matches!(
658 tag,
659 "area"
660 | "base"
661 | "br"
662 | "col"
663 | "embed"
664 | "hr"
665 | "img"
666 | "input"
667 | "link"
668 | "meta"
669 | "param"
670 | "source"
671 | "track"
672 | "wbr"
673 )
674}
675
676fn starts_with(bytes: &[u8], idx: usize, pattern: &[u8]) -> bool {
677 bytes.len() >= idx + pattern.len() && &bytes[idx..idx + pattern.len()] == pattern
678}
679
680fn find_char(bytes: &[u8], start: usize, target: u8) -> Option<usize> {
681 (start..bytes.len()).find(|&i| bytes[i] == target)
682}
683
684fn parse_end_tag(html: &str, start: usize) -> Option<(String, usize)> {
685 let bytes = html.as_bytes();
686 let len = bytes.len();
687 if start + 2 >= len {
688 return None;
689 }
690 let mut i = start + 2;
691 while i < len && bytes[i].is_ascii_whitespace() {
692 i += 1;
693 }
694 let name_start = i;
695 while i < len && is_tag_name_char(bytes[i]) {
696 i += 1;
697 }
698 if name_start == i {
699 return None;
700 }
701 let tag_name = html[name_start..i].to_lowercase();
702 if let Some(end_pos) = find_char(bytes, i, b'>') {
703 return Some((tag_name, end_pos + 1));
704 }
705 None
706}
707
708fn find_block_content(html: &str, start: usize, tag: &str) -> Option<(usize, usize, usize)> {
709 let lower = html.to_lowercase();
710 let bytes = lower.as_bytes();
711 let len = bytes.len();
712 let target = format!("</{}", tag.to_lowercase());
713 let target_bytes = target.as_bytes();
714 let mut i = start;
715 while i + target_bytes.len() < len {
716 if bytes[i] == b'<' && starts_with(bytes, i, target_bytes) {
717 if let Some(end_pos) = find_char(bytes, i + target_bytes.len(), b'>') {
718 return Some((start, i, end_pos + 1));
719 }
720 }
721 i += 1;
722 }
723 None
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729
730 #[test]
731 fn test_dom_tree_basic() {
732 let html = r#"
733 <html>
734 <body>
735 <div class="container">
736 <p id="text">Hello</p>
737 </div>
738 </body>
739 </html>
740 "#;
741
742 let result = DomTree::parse(html);
743 let tree = result.dom_tree;
744 assert!(!tree.roots.is_empty());
745
746 let node = tree.find_node_at_position(50);
748 assert!(node.is_some());
749 }
750
751 #[test]
752 fn test_dom_tree_nested_structure() {
753 let html =
754 r#"<div class="outer"><div class="inner"><span id="item">Text</span></div></div>"#;
755
756 let result = DomTree::parse(html);
757 let tree = result.dom_tree;
758
759 assert!(!tree.roots.is_empty());
761
762 assert_eq!(tree.nodes.len(), 3); }
765
766 #[test]
767 fn test_dom_tree_multiple_classes() {
768 let html = r#"<div class="class1 class2 class3">Content</div>"#;
769
770 let result = DomTree::parse(html);
771 assert!(!result.dom_tree.roots.is_empty());
772 }
774
775 #[test]
776 fn test_dom_tree_self_closing_tags() {
777 let html = r#"<div><img src="test.jpg" /><br /><input type="text" /></div>"#;
778
779 let result = DomTree::parse(html);
780 let tree = result.dom_tree;
781
782 assert!(!tree.roots.is_empty());
783 }
785
786 #[test]
787 fn test_dom_tree_find_node_at_position() {
788 let html = r#"<div class="outer"><p id="para">Text</p></div>"#;
789
790 let result = DomTree::parse(html);
791 let tree = result.dom_tree;
792
793 let node = tree.find_node_at_position(5);
795 assert!(node.is_some());
796 }
797
798 #[test]
799 fn test_dom_tree_empty_html() {
800 let html = "";
801 let result = DomTree::parse(html);
802 let tree = result.dom_tree;
803 assert!(tree.roots.is_empty());
804 }
805
806 #[test]
807 fn test_dom_tree_malformed_html() {
808 let html = r#"<div><p>Text"#;
810 let result = DomTree::parse(html);
811 assert!(!result.dom_tree.roots.is_empty());
813 }
814
815 #[test]
816 fn test_parse_inline_styles() {
817 let html = r#"<div style="color: red; background: blue;"></div>"#;
818
819 let parsed = DomTree::parse(html);
820 assert_eq!(parsed.inline_styles.len(), 1);
821
822 let inline = &parsed.inline_styles[0];
823 assert!(inline.value.contains("color: red"));
824 assert!(inline.value.contains("background: blue"));
825 }
826
827 #[test]
828 fn test_parse_style_blocks() {
829 let html = r#"
830 <html>
831 <head>
832 <style>
833 .class { color: red; }
834 </style>
835 </head>
836 <body>
837 <style>
838 #id { background: blue; }
839 </style>
840 </body>
841 </html>
842 "#;
843
844 let parsed = DomTree::parse(html);
845 assert_eq!(parsed.style_blocks.len(), 2);
846
847 assert!(parsed.style_blocks[0].content.contains("color: red"));
848 assert!(parsed.style_blocks[1].content.contains("background: blue"));
849 }
850
851 #[test]
852 fn test_parse_nested_style_tags() {
853 let html = r#"<style>outer { color: red; }<style>inner</style></style>"#;
854
855 let parsed = DomTree::parse(html);
856 assert!(!parsed.style_blocks.is_empty());
858 }
859
860 #[test]
861 fn test_dom_tree_comment_handling() {
862 let html = r#"<div><!-- This is a comment --><p>Text</p></div>"#;
863
864 let result = DomTree::parse(html);
865 let tree = result.dom_tree;
866
867 assert!(!tree.roots.is_empty());
869 }
870
871 #[test]
872 fn test_attributes_with_quotes() {
873 let html = r#"<div class="test" id='myid' data-value=unquoted></div>"#;
874
875 let result = DomTree::parse(html);
876 let tree = result.dom_tree;
877
878 assert!(!tree.roots.is_empty());
880 }
881
882 #[test]
883 fn test_dom_tree_classname_alias() {
884 let html = r#"<div className="react-class">Content</div>"#;
885 let result = DomTree::parse(html);
886 let tree = result.dom_tree;
887 assert!(!tree.roots.is_empty());
888 let node = &tree.nodes[tree.roots[0]];
889 assert!(node.classes.contains(&"react-class".to_string()));
890 }
891
892 #[test]
900 fn test_dom_tree_html_entity_decoding() {
901 let test_cases = vec![
903 (" ", " "),
905 ("&", "&"),
906 ("<", "<"),
907 (">", ">"),
908 (""", "\""),
909 ("'", "'"),
910 ("'", "'"),
911 (" ", " "),
912 ("©", "©"),
913 ("®", "®"),
914 ];
915
916 for (entity, expected) in test_cases {
917 let html = format!(
918 r#"<div style="--test: '{}'; color: blue;">Content</div>"#,
919 entity
920 );
921
922 let result = DomTree::parse(&html);
923
924 let tree = result.dom_tree;
925 assert!(
926 !tree.roots.is_empty(),
927 "Tree should have nodes for: {}",
928 entity
929 );
930
931 let inline_styles = result.inline_styles;
934
935 assert!(
936 !inline_styles.is_empty(),
937 "Should have inline styles for: {}",
938 entity
939 );
940
941 let style = &inline_styles[0].value;
943
944 assert!(
948 style.contains(expected),
949 "Style should contain decoded entity '{}' but got: {} (for entity: {})",
950 expected,
951 style,
952 entity
953 );
954 }
955 }
956}