1use crate::engine::html::tokenizer::{Attribute, Token, Tokenizer};
4use crate::engine::html::util as html_util;
5use crate::engine::{
6 css::{
7 matcher::{ElementChain, ElementInfo},
8 parser::{CssNodeType, Parser as CssParser},
9 },
10 tree::{NodeRef, Tree, TreeNode},
11};
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15use std::sync::Arc;
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ShadowRootMode {
18 Open,
19 Closed,
20}
21
22#[derive(Debug, Clone)]
23pub enum HtmlNodeType {
24 Document,
25 DocumentFragment,
26 Element {
27 tag_name: String,
28 attributes: Vec<Attribute>,
29 },
30 ShadowRoot {
33 mode: ShadowRootMode,
34 },
35 Text(String),
36 Comment(String),
37 Doctype {
38 name: Option<String>,
39 public_id: Option<String>,
40 system_id: Option<String>,
41 },
42 ProcessingInstruction {
44 target: String,
45 data: String,
46 },
47 InvalidNode(Token, String), }
49
50impl HtmlNodeType {
51 pub fn tag_name(&self) -> Option<&str> {
52 match self {
53 HtmlNodeType::Element { tag_name, .. } => Some(tag_name),
54 _ => None,
55 }
56 }
57
58 pub fn get_attr(&self, name: &str) -> Option<&str> {
59 match self {
60 HtmlNodeType::Element { attributes, .. } => attributes
61 .iter()
62 .find(|attr| attr.name == name)
63 .map(|attr| attr.value.as_str()),
64 _ => None,
65 }
66 }
67 pub fn set_attr(&mut self, name: &str, value: String) {
68 if let HtmlNodeType::Element { attributes, .. } = self {
69 if let Some(attr) = attributes.iter_mut().find(|attr| attr.name == name) {
70 attr.value = value;
71 } else {
72 attributes.push(Attribute {
73 name: name.to_string(),
74 value,
75 });
76 }
77 }
78 }
79 pub fn remove_attr(&mut self, name: &str) -> Option<String> {
80 if let HtmlNodeType::Element { attributes, .. } = self {
81 attributes
82 .iter()
83 .position(|attr| attr.name == name)
84 .map(|pos| attributes.remove(pos).value)
85 } else {
86 None
87 }
88 }
89 pub fn has_attr(&self, name: &str) -> bool {
90 match self {
91 HtmlNodeType::Element { attributes, .. } => {
92 attributes.iter().any(|attr| attr.name == name)
93 }
94 _ => false,
95 }
96 }
97}
98
99pub type DomTree = Tree<HtmlNodeType>;
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ClassicScriptSource {
104 Inline(String),
105 External(String),
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum ScriptingMode {
113 #[default]
114 Enabled,
115 Disabled,
116}
117
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
120pub enum ClassicScriptExecution {
121 #[default]
122 Default,
123 Defer,
124 Async,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct ClassicScriptDescriptor {
130 pub source: ClassicScriptSource,
131 pub execution: ClassicScriptExecution,
132}
133
134impl DomTree {
135 pub fn get_elements_by_tag_name(&self, tag_name: &str) -> Vec<NodeRef<HtmlNodeType>> {
137 self.find_all(|n| {
138 if let HtmlNodeType::Element { tag_name: t, .. } = n {
139 t.eq_ignore_ascii_case(tag_name)
140 } else {
141 false
142 }
143 })
144 .into_iter()
145 .filter(|node| !is_inside_template(node))
146 .collect()
147 }
148
149 pub fn get_element_by_id(&self, id: &str) -> Option<NodeRef<HtmlNodeType>> {
151 self.find_all(|n| {
152 if let HtmlNodeType::Element { attributes, .. } = n {
153 attributes
154 .iter()
155 .any(|attr| attr.name == "id" && attr.value == id)
156 } else {
157 false
158 }
159 })
160 .into_iter()
161 .find(|node| !is_inside_template(node))
162 }
163
164 pub fn get_elements_by_class_name(&self, class_name: &str) -> Vec<NodeRef<HtmlNodeType>> {
166 self.find_all(|n| {
167 if let HtmlNodeType::Element { attributes, .. } = n {
168 attributes.iter().any(|attr| {
169 attr.name == "class" && attr.value.split_whitespace().any(|c| c == class_name)
170 })
171 } else {
172 false
173 }
174 })
175 .into_iter()
176 .filter(|node| !is_inside_template(node))
177 .collect()
178 }
179
180 pub fn inner_text(node: &NodeRef<HtmlNodeType>) -> String {
183 let n = node.borrow();
184 match &n.value {
185 HtmlNodeType::Text(content) => content.clone(),
186 HtmlNodeType::Element { .. } | HtmlNodeType::DocumentFragment => n
187 .children()
188 .iter()
189 .filter(|c| !matches!(c.borrow().value, HtmlNodeType::ShadowRoot { .. }))
191 .map(DomTree::inner_text)
192 .collect(),
193 HtmlNodeType::ShadowRoot { .. } => {
194 n.children().iter().map(DomTree::inner_text).collect()
196 }
197 _ => "".to_string(),
198 }
199 }
200
201 pub fn set_text_content(node: &NodeRef<HtmlNodeType>, new_text: &str) {
203 if let HtmlNodeType::Text(content) = &mut node.borrow_mut().value {
205 *content = new_text.to_string();
206 return;
207 }
208
209 let is_element = matches!(node.borrow().value, HtmlNodeType::Element { .. });
212 if is_element {
213 node.borrow_mut().clear_children();
215 let text_node = TreeNode::new(HtmlNodeType::Text(new_text.to_string()));
216 TreeNode::add_child(node, text_node);
217 }
218 }
219
220 pub fn collect_text_by_tag(&self, tag_name: &str) -> Vec<String> {
222 let mut texts = Vec::new();
223
224 self.traverse(|node| {
225 let n = node.borrow();
226 if let HtmlNodeType::Element { tag_name: t, .. } = &n.value
227 && t.eq_ignore_ascii_case(tag_name)
228 {
229 let text_of_this_node: String = n
230 .children()
231 .iter()
232 .filter_map(|child| {
233 let child_ref = child.borrow();
234 if let HtmlNodeType::Text(content) = &child_ref.value {
235 Some(content.clone())
236 } else {
237 None
238 }
239 })
240 .collect();
241
242 texts.push(text_of_this_node);
243 }
244 });
245
246 texts
247 }
248
249 pub fn collect_classic_scripts(&self) -> Vec<ClassicScriptSource> {
254 self.collect_classic_script_descriptors()
255 .into_iter()
256 .map(|script| script.source)
257 .collect()
258 }
259
260 pub fn query_selector(&self, selector: &str) -> Option<NodeRef<HtmlNodeType>> {
262 self.query_selector_all(selector).into_iter().next()
263 }
264
265 pub fn query_selector_all(&self, selector: &str) -> Vec<NodeRef<HtmlNodeType>> {
267 query_selector_all_from(&self.root, selector, true)
268 }
269
270 pub fn query_selector_within(
274 scope: &NodeRef<HtmlNodeType>,
275 selector: &str,
276 ) -> Option<NodeRef<HtmlNodeType>> {
277 Self::query_selector_all_within(scope, selector)
278 .into_iter()
279 .next()
280 }
281
282 pub fn query_selector_all_within(
286 scope: &NodeRef<HtmlNodeType>,
287 selector: &str,
288 ) -> Vec<NodeRef<HtmlNodeType>> {
289 query_selector_all_from(scope, selector, false)
290 }
291
292 pub fn element_matches_selector(node: &NodeRef<HtmlNodeType>, selector: &str) -> bool {
294 let selectors = parse_query_selectors(selector);
295 if selectors.is_empty() {
296 return false;
297 }
298 let chain = element_chain(node);
299 selectors.iter().any(|s| s.matches(&chain))
300 }
301
302 pub fn element_closest(
305 node: &NodeRef<HtmlNodeType>,
306 selector: &str,
307 ) -> Option<NodeRef<HtmlNodeType>> {
308 let selectors = parse_query_selectors(selector);
309 if selectors.is_empty() {
310 return None;
311 }
312
313 let mut current = Some(Rc::clone(node));
314 while let Some(n) = current.clone() {
315 if matches!(n.borrow().value, HtmlNodeType::ShadowRoot { .. }) {
317 break;
318 }
319 let chain = element_chain(&n);
320 if selectors.iter().any(|s| s.matches(&chain)) {
321 return Some(Rc::clone(&n));
322 }
323 current = n.borrow().parent();
324 }
325
326 None
327 }
328
329 pub fn collect_classic_script_descriptors(&self) -> Vec<ClassicScriptDescriptor> {
334 self.get_elements_by_tag_name("script")
335 .into_iter()
336 .filter(|node| !is_inside_template(node))
337 .filter_map(|node| {
338 let n = node.borrow();
339 let script_type = n.value.get_attr("type").unwrap_or("").trim();
340 if !is_classic_javascript_type(script_type) {
341 return None;
342 }
343
344 let is_module = script_type.eq_ignore_ascii_case("module");
345 match n.value.get_attr("src").map(str::trim) {
346 Some(src) if !src.is_empty() => Some(ClassicScriptDescriptor {
348 source: ClassicScriptSource::External(src.to_string()),
349 execution: if is_module {
350 if n.value.has_attr("async") {
351 ClassicScriptExecution::Async
352 } else {
353 ClassicScriptExecution::Defer
355 }
356 } else if n.value.has_attr("async") {
357 ClassicScriptExecution::Async
358 } else if n.value.has_attr("defer") {
359 ClassicScriptExecution::Defer
360 } else {
361 ClassicScriptExecution::Default
362 },
363 }),
364 Some(_) => None,
365 None => {
366 let source = DomTree::inner_text(&node);
367 if is_module && contains_top_level_module_keyword(&source) {
373 return None;
374 }
375 Some(ClassicScriptDescriptor {
376 source: ClassicScriptSource::Inline(source),
377 execution: if is_module {
379 if n.value.has_attr("async") {
380 ClassicScriptExecution::Async
381 } else {
382 ClassicScriptExecution::Defer
383 }
384 } else {
385 ClassicScriptExecution::Default
386 },
387 })
388 }
389 }
390 })
391 .collect()
392 }
393
394 pub fn collect_inline_scripts(&self) -> Vec<String> {
398 self.collect_classic_scripts()
399 .into_iter()
400 .filter_map(|script| match script {
401 ClassicScriptSource::Inline(source) => Some(source),
402 ClassicScriptSource::External(_) => None,
403 })
404 .collect()
405 }
406}
407
408fn query_selector_all_from(
409 scope: &NodeRef<HtmlNodeType>,
410 selector: &str,
411 include_scope: bool,
412) -> Vec<NodeRef<HtmlNodeType>> {
413 let selectors = parse_query_selectors(selector);
414 if selectors.is_empty() {
415 return Vec::new();
416 }
417
418 let mut candidates = Vec::new();
419 collect_element_nodes(scope, include_scope, &mut candidates);
420 candidates
421 .into_iter()
422 .filter(|node| {
423 let chain = element_chain(node);
424 selectors.iter().any(|selector| selector.matches(&chain))
425 })
426 .collect()
427}
428
429pub(crate) fn parse_query_selectors(
430 selector: &str,
431) -> Vec<crate::engine::css::parser::ComplexSelector> {
432 if selector.trim().is_empty() {
433 return Vec::new();
434 }
435
436 let source = format!("{selector} {{}} ");
437 let Ok(stylesheet) = CssParser::new(&source).parse() else {
438 return Vec::new();
439 };
440 stylesheet
441 .children()
442 .iter()
443 .find_map(|node| match node.node() {
444 CssNodeType::Rule { selectors } => Some(selectors.clone()),
445 _ => None,
446 })
447 .unwrap_or_default()
448}
449
450pub(crate) fn collect_element_nodes(
451 node: &NodeRef<HtmlNodeType>,
452 include_node: bool,
453 output: &mut Vec<NodeRef<HtmlNodeType>>,
454) {
455 let (is_element, is_shadow, is_template, children) = {
456 let node = node.borrow();
457 (
458 matches!(node.value, HtmlNodeType::Element { .. }),
459 matches!(node.value, HtmlNodeType::ShadowRoot { .. }),
460 node.value
461 .tag_name()
462 .is_some_and(|t| t.eq_ignore_ascii_case("template")),
463 node.children().to_vec(),
464 )
465 };
466 if include_node && is_element {
467 output.push(Rc::clone(node));
468 }
469 if !is_shadow {
473 for child in children {
474 let is_child_shadow = matches!(child.borrow().value, HtmlNodeType::ShadowRoot { .. });
476 let is_child_template_content =
482 is_template && matches!(child.borrow().value, HtmlNodeType::DocumentFragment);
483 if !is_child_shadow && !is_child_template_content {
484 collect_element_nodes(&child, true, output);
485 }
486 }
487 } else {
488 for child in children {
491 collect_element_nodes(&child, true, output);
492 }
493 }
494}
495
496pub(crate) fn element_chain(node: &NodeRef<HtmlNodeType>) -> ElementChain {
497 let mut chain = Vec::new();
498 let mut current = Some(Rc::clone(node));
499 while let Some(node) = current {
500 if matches!(node.borrow().value, HtmlNodeType::ShadowRoot { .. }) {
502 break;
503 }
504 if let Some(info) = element_info(&node) {
505 chain.push(info);
506 }
507 current = node.borrow().parent();
508 }
509 ElementChain::from_vec(chain)
510}
511
512pub fn checkedness(node: &NodeRef<HtmlNodeType>) -> bool {
516 let node = node.borrow();
517 node.checkedness()
518 .unwrap_or_else(|| node.value.get_attr("checked").is_some())
519}
520
521fn effective_lang(node: &NodeRef<HtmlNodeType>) -> Option<Arc<str>> {
525 let mut current = Some(Rc::clone(node));
526 while let Some(candidate) = current {
527 if let Some(lang) = candidate
528 .borrow()
529 .value
530 .get_attr("lang")
531 .map(str::trim)
532 .filter(|value| !value.is_empty())
533 {
534 return Some(Arc::from(lang));
535 }
536 current = candidate.borrow().parent();
537 }
538 None
539}
540
541fn is_empty_element(node: &NodeRef<HtmlNodeType>) -> bool {
545 node.borrow()
546 .children()
547 .iter()
548 .all(|child| match &child.borrow().value {
549 HtmlNodeType::Text(text) => text.is_empty(),
550 HtmlNodeType::Element { .. } => false,
551 _ => true,
552 })
553}
554
555fn element_info(node: &NodeRef<HtmlNodeType>) -> Option<ElementInfo> {
556 let (tag_name, attributes, parent) = {
557 let node = node.borrow();
558 let HtmlNodeType::Element {
559 tag_name,
560 attributes,
561 } = &node.value
562 else {
563 return None;
564 };
565 (tag_name.clone(), attributes.clone(), node.parent())
566 };
567
568 let has_parent = has_element_parent(node);
569 let lang = effective_lang(node);
570 let checked = checkedness(node);
571 let siblings = parent
572 .map(|parent| parent.borrow().children().to_vec())
573 .unwrap_or_else(|| vec![Rc::clone(node)]);
574 let sibling_elements: Vec<_> = siblings
575 .into_iter()
576 .filter_map(|sibling| basic_element_info(&sibling).map(|info| (sibling, info)))
577 .collect();
578 let element_count = sibling_elements.len();
579 let mut type_counts = HashMap::<String, usize>::new();
580 for (_, sibling) in &sibling_elements {
581 *type_counts.entry(sibling.tag_name.to_string()).or_default() += 1;
582 }
583
584 let position = sibling_elements
585 .iter()
586 .position(|(sibling, _)| Rc::ptr_eq(sibling, node))?;
587 let element_index = position + 1;
588 let type_index = sibling_elements[..=position]
589 .iter()
590 .filter(|(_, sibling)| sibling.tag_name.as_ref() == tag_name)
591 .count();
592 let previous_siblings = ElementChain::from_document_order(
593 sibling_elements[..position]
594 .iter()
595 .map(|(_, sibling)| sibling.clone()),
596 );
597
598 Some(ElementInfo {
599 tag_name: tag_name.as_str().into(),
600 id: attributes
601 .iter()
602 .find(|attribute| attribute.name.eq_ignore_ascii_case("id"))
603 .map(|attribute| Arc::from(attribute.value.as_str())),
604 classes: attributes
605 .iter()
606 .find(|attribute| attribute.name.eq_ignore_ascii_case("class"))
607 .map(|attribute| {
608 attribute
609 .value
610 .split_whitespace()
611 .map(Arc::<str>::from)
612 .collect()
613 })
614 .unwrap_or_default(),
615 attributes: attributes
616 .into_iter()
617 .map(|attribute| {
618 (
619 Arc::<str>::from(attribute.name),
620 Arc::<str>::from(attribute.value),
621 )
622 })
623 .collect(),
624 element_index,
625 element_count,
626 type_index,
627 type_count: type_counts[&tag_name],
628 previous_siblings,
629 has_parent,
630 is_empty: is_empty_element(node),
631 lang,
632 checked,
633 hovered: false,
634 active: false,
635 })
636}
637
638fn has_element_parent(node: &NodeRef<HtmlNodeType>) -> bool {
641 node.borrow()
642 .parent()
643 .is_some_and(|parent| matches!(parent.borrow().value, HtmlNodeType::Element { .. }))
644}
645
646fn basic_element_info(node: &NodeRef<HtmlNodeType>) -> Option<ElementInfo> {
647 let has_parent = has_element_parent(node);
648 let is_empty = is_empty_element(node);
649 let lang = effective_lang(node);
650 let checked = checkedness(node);
651 let node = node.borrow();
652 let HtmlNodeType::Element {
653 tag_name,
654 attributes,
655 } = &node.value
656 else {
657 return None;
658 };
659 Some(ElementInfo {
660 tag_name: tag_name.as_str().into(),
661 id: attributes
662 .iter()
663 .find(|attribute| attribute.name.eq_ignore_ascii_case("id"))
664 .map(|attribute| Arc::from(attribute.value.as_str())),
665 classes: attributes
666 .iter()
667 .find(|attribute| attribute.name.eq_ignore_ascii_case("class"))
668 .map(|attribute| {
669 attribute
670 .value
671 .split_whitespace()
672 .map(Arc::<str>::from)
673 .collect()
674 })
675 .unwrap_or_default(),
676 attributes: attributes
677 .iter()
678 .map(|attribute| {
679 (
680 Arc::<str>::from(attribute.name.as_str()),
681 Arc::<str>::from(attribute.value.as_str()),
682 )
683 })
684 .collect(),
685 element_index: 1,
686 element_count: 1,
687 type_index: 1,
688 type_count: 1,
689 previous_siblings: ElementChain::default(),
690 has_parent,
691 is_empty,
692 lang,
693 checked,
694 hovered: false,
695 active: false,
696 })
697}
698
699fn contains_top_level_module_keyword(source: &str) -> bool {
705 for keyword in ["import", "export"] {
706 let mut search_from = 0;
707 while search_from < source.len() {
708 let Some(pos) = source[search_from..].find(keyword) else {
709 break;
710 };
711 let abs = search_from + pos;
712 let before_ok = abs == 0
713 || !source[..abs]
714 .chars()
715 .last()
716 .map(|c| c.is_alphanumeric() || c == '_')
717 .unwrap_or(false);
718 let after_abs = abs + keyword.len();
719 let after_char = source[after_abs..].chars().next();
720 let after_ok = after_char
721 .map(|c| !c.is_alphanumeric() && c != '_')
722 .unwrap_or(true);
723 if before_ok && after_ok {
724 if keyword == "import" {
727 let rest = source[after_abs..].trim_start();
728 if rest.starts_with('(') {
729 search_from = after_abs;
730 continue;
731 }
732 }
733 return true;
734 }
735 search_from = abs + 1;
736 }
737 }
738 false
739}
740
741fn is_classic_javascript_type(script_type: &str) -> bool {
745 if script_type.is_empty() {
746 return true;
747 }
748
749 matches!(
750 script_type.to_ascii_lowercase().as_str(),
751 "text/javascript"
752 | "application/javascript"
753 | "text/ecmascript"
754 | "application/ecmascript"
755 | "application/x-javascript"
756 | "module"
758 )
759}
760
761pub struct Parser<'a> {
762 tokenizer: Tokenizer<'a>,
763 tree: DomTree,
764 stack: Vec<Rc<RefCell<TreeNode<HtmlNodeType>>>>,
765 tag_stack: Vec<String>,
766 special_text_mode: Option<String>, scripting_mode: ScriptingMode,
768}
769
770impl<'a> Parser<'a> {
771 pub fn new(input: &'a str) -> Self {
772 let document = Tree::new(HtmlNodeType::Document);
773
774 Self {
775 tokenizer: Tokenizer::new(input),
776 tree: document.clone(),
777 stack: vec![document.root],
778 tag_stack: vec![],
779 special_text_mode: None,
780 scripting_mode: ScriptingMode::default(),
781 }
782 }
783
784 pub fn with_scripting_mode(mut self, mode: ScriptingMode) -> Self {
786 self.scripting_mode = mode;
787 self
788 }
789
790 pub fn parse(&mut self) -> DomTree {
791 while let Some(token) = self.tokenizer.next_token() {
792 log::debug!(target:"HtmlParser::Token" ,"Processing token: {token:?}");
793 match token {
794 Token::StartTag { .. } => self.handle_start_tag(token),
795 Token::EndTag { .. } => self.handle_end_tag(token),
796 Token::Doctype { .. } => self.handle_doctype(token),
797 Token::Comment(_) => self.handle_comment(token),
798 Token::Text(_) => self.handle_text(token),
799 }
800 }
801 self.autofill_elements();
802 apply_suspense_replacements(&self.tree);
803
804 self.tree.clone()
805 }
806
807 fn handle_start_tag(&mut self, token: Token) {
808 if let Token::StartTag {
809 name,
810 attributes,
811 self_closing,
812 } = token
813 {
814 let mut parent = Rc::clone(self.stack.last().unwrap());
815 if self.special_text_mode.is_some() {
816 TreeNode::add_child_value(&parent, HtmlNodeType::Text(format!("<{}>", name)));
819 return;
820 }
821
822 if matches!(name.as_str(), "tr" | "td" | "th")
826 && let Some(top) = self.stack.last()
827 && top
828 .borrow()
829 .value
830 .tag_name()
831 .is_some_and(|t| t.eq_ignore_ascii_case("table"))
832 {
833 let tbody = TreeNode::add_child_value(
834 &parent,
835 HtmlNodeType::Element {
836 tag_name: "tbody".to_string(),
837 attributes: Vec::new(),
838 },
839 );
840 self.tag_stack.push("tbody".to_string());
841 self.stack.push(tbody);
842 parent = Rc::clone(self.stack.last().unwrap());
843 }
844
845 if name == "noscript" {
847 self.handle_noscript(attributes);
848 return;
849 }
850
851 if name == "template" {
855 let template = TreeNode::add_child_value(
856 &parent,
857 HtmlNodeType::Element {
858 tag_name: name.clone(),
859 attributes,
860 },
861 );
862 let content = TreeNode::new(HtmlNodeType::DocumentFragment);
863 TreeNode::add_child(&template, Rc::clone(&content));
864 self.tag_stack.push(name);
865 self.stack.push(Rc::clone(&template));
868 self.stack.push(content);
869 return;
870 }
871
872 while self.check_start_tag_with_invalid_nesting(&name, &parent) {
873 if let HtmlNodeType::Element { tag_name, .. } = &parent.borrow().value {
874 log::info!(target:"HtmlParser::AutoClosing" ,"Auto-closing tag: <{}> to allow <{}> inside it.", tag_name, name);
875 self.handle_end_tag(Token::EndTag {
876 name: tag_name.clone(),
877 });
878 }
879 parent = Rc::clone(self.stack.last().unwrap());
880 }
881
882 let new_node = TreeNode::add_child_value(
883 &parent,
884 HtmlNodeType::Element {
885 tag_name: name.clone(),
886 attributes,
887 },
888 );
889
890 if name == "script" || name == "style" {
892 self.special_text_mode = Some(name.clone());
893 }
894
895 let is_void = matches!(
897 name.as_str(),
898 "area"
899 | "base"
900 | "br"
901 | "col"
902 | "embed"
903 | "hr"
904 | "img"
905 | "input"
906 | "link"
907 | "meta"
908 | "param"
909 | "source"
910 | "track"
911 | "wbr"
912 );
913 if !self_closing && !is_void {
915 self.tag_stack.push(name.clone());
916 self.stack.push(new_node);
917 log::debug!(target:"HtmlParser::Stack" ,"Stack len: {}, +Pushed <{}> to stack.", self.stack.len(), name);
918 }
919 }
920 }
921
922 fn handle_noscript(&mut self, attributes: Vec<Attribute>) {
923 let in_head = self
924 .stack
925 .last()
926 .and_then(|node| node.borrow().value.tag_name().map(str::to_string))
927 .is_some_and(|tag| tag.eq_ignore_ascii_case("head"));
928
929 if in_head {
930 self.handle_noscript_in_head(attributes);
931 } else {
932 self.handle_noscript_in_body(attributes);
933 }
934 }
935
936 fn handle_noscript_in_head(&mut self, attributes: Vec<Attribute>) {
940 match self.scripting_mode {
941 ScriptingMode::Enabled => self.parse_noscript_as_raw_text(attributes),
942 ScriptingMode::Disabled => self.parse_noscript_as_html(attributes),
943 }
944 }
945
946 fn handle_noscript_in_body(&mut self, attributes: Vec<Attribute>) {
949 match self.scripting_mode {
950 ScriptingMode::Enabled => self.parse_noscript_as_raw_text(attributes),
951 ScriptingMode::Disabled => self.parse_noscript_as_html(attributes),
952 }
953 }
954
955 fn parse_noscript_as_raw_text(&mut self, attributes: Vec<Attribute>) {
957 self.push_element("noscript", attributes);
958 self.special_text_mode = Some("noscript".to_string());
959 }
960
961 fn parse_noscript_as_html(&mut self, attributes: Vec<Attribute>) {
963 self.push_element("noscript", attributes);
964 }
965
966 fn push_element(&mut self, name: &str, attributes: Vec<Attribute>) {
968 let parent = Rc::clone(self.stack.last().unwrap());
969 let node = TreeNode::add_child_value(
970 &parent,
971 HtmlNodeType::Element {
972 tag_name: name.to_string(),
973 attributes,
974 },
975 );
976 self.tag_stack.push(name.to_string());
977 self.stack.push(node);
978 }
979
980 fn handle_end_tag(&mut self, token: Token) {
981 if let Token::EndTag { ref name } = token {
982 if self.special_text_mode.as_deref() == Some(name.as_str()) {
984 self.special_text_mode = None;
985 }
986
987 if self.special_text_mode.is_some() {
988 let parent = Rc::clone(self.stack.last().unwrap());
989 TreeNode::add_child_value(&parent, HtmlNodeType::Text(format!("</{}>", name)));
990 return;
991 }
992
993 let name = name.clone();
994 if self.tag_stack.contains(&name) {
995 while let Some(top) = self.stack.pop() {
996 if let HtmlNodeType::Element { tag_name, .. } = &top.borrow().value {
997 self.tag_stack.pop();
998 if tag_name == &name {
999 log::debug!(target:"HtmlParser::Stack" ,"Stack len: {}, -Popped </{}> from stack.", self.stack.len(), name);
1000 break;
1001 } else {
1002 log::debug!(target:"HtmlParser::Stack" ,"Stack len: {}, Unmatched end tag: </{}>, Find <{}>", self.stack.len(), name, tag_name);
1003 }
1004 }
1005 }
1006 } else {
1007 let parent = Rc::clone(self.stack.last().unwrap());
1008 TreeNode::add_child_value(
1009 &parent,
1010 HtmlNodeType::InvalidNode(
1011 token,
1012 format!("No matching start tag for </{}>", name),
1013 ),
1014 );
1015 log::debug!(target:"HtmlParser::Invalid" ,"Invalid end tag: </{}>", name);
1016 }
1017 }
1018 }
1019
1020 fn handle_text(&mut self, token: Token) {
1021 if let Token::Text(data) = token {
1022 let parent = Rc::clone(self.stack.last().unwrap());
1023
1024 if self.special_text_mode.is_some() {
1026 TreeNode::add_child_value(&parent, HtmlNodeType::Text(data));
1027 return;
1028 }
1029
1030 TreeNode::add_child_value(&parent, HtmlNodeType::Text(data));
1031 }
1032 }
1033
1034 fn handle_comment(&mut self, token: Token) {
1035 if let Token::Comment(data) = token {
1036 let parent = Rc::clone(self.stack.last().unwrap());
1037 TreeNode::add_child_value(&parent, HtmlNodeType::Comment(data));
1038 }
1039 }
1040
1041 fn handle_doctype(&mut self, token: Token) {
1042 if let Token::Doctype {
1043 name,
1044 public_id,
1045 system_id,
1046 ..
1047 } = token
1048 {
1049 let parent = Rc::clone(self.stack.last().unwrap());
1050 TreeNode::add_child_value(
1051 &parent,
1052 HtmlNodeType::Doctype {
1053 name,
1054 public_id,
1055 system_id,
1056 },
1057 );
1058 }
1059 }
1060
1061 fn check_start_tag_with_invalid_nesting(
1062 &self,
1063 name: &String,
1064 parent: &Rc<RefCell<TreeNode<HtmlNodeType>>>,
1065 ) -> bool {
1066 if let HtmlNodeType::Element { tag_name, .. } = &parent.borrow().value {
1067 if tag_name != "html" && name == "body" {
1069 println!("here we can see 「お行儀の悪いコード」");
1070 return true;
1071 }
1072 if tag_name == "p" && name == "p" {
1074 return true;
1075 }
1076 if tag_name == "li" && name == "li" {
1078 return true;
1079 }
1080 if tag_name == "a" && name == "a" {
1082 return true;
1083 }
1084 if tag_name == "dt" && (name == "dt" || name == "dd") {
1086 return true;
1087 }
1088 if tag_name == "dd" && (name == "dt" || name == "dd") {
1090 return true;
1091 }
1092 if tag_name == "option" && name == "option" {
1094 return true;
1095 }
1096 if matches!(
1098 tag_name.as_str(),
1099 "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
1100 ) && html_util::is_block_level_element(name)
1101 {
1102 return true;
1103 }
1104 }
1105 false
1106 }
1107
1108 fn autofill_elements(&mut self) {
1110 let root = Rc::clone(&self.stack[0]);
1111 let html_node = root
1112 .borrow()
1113 .children()
1114 .iter()
1115 .find(|c| {
1116 matches!(&c.borrow().value, HtmlNodeType::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("html"))
1117 })
1118 .map(Rc::clone);
1119 if let Some(html_node) = html_node {
1120 let has_head_and_body = {
1122 let children = html_node.borrow().children().to_vec();
1123 let has_head = children.iter().any(|c| {
1124 matches!(&c.borrow().value, HtmlNodeType::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("head"))
1125 });
1126 let has_body = children.iter().any(|c| {
1127 matches!(&c.borrow().value, HtmlNodeType::Element { tag_name, .. } if tag_name.eq_ignore_ascii_case("body"))
1128 });
1129 has_head && has_body
1130 };
1131 if !has_head_and_body {
1132 let children = html_node.borrow().children().to_vec();
1133 let (head_node, body_node, head_content, body_content) =
1134 split_head_and_body(children);
1135 let head_node = match head_node {
1136 Some(node) => node,
1137 None => TreeNode::new(HtmlNodeType::Element {
1138 tag_name: "head".to_string(),
1139 attributes: vec![],
1140 }),
1141 };
1142 let body_node = match body_node {
1143 Some(node) => node,
1144 None => TreeNode::new(HtmlNodeType::Element {
1145 tag_name: "body".to_string(),
1146 attributes: vec![],
1147 }),
1148 };
1149 html_node.borrow_mut().clear_children();
1150 TreeNode::add_child(&html_node, Rc::clone(&head_node));
1151 TreeNode::add_child(&html_node, Rc::clone(&body_node));
1152 for node in head_content {
1153 TreeNode::add_child(&head_node, node);
1154 }
1155 for node in body_content {
1156 TreeNode::add_child(&body_node, node);
1157 }
1158 }
1159 return;
1160 }
1161
1162 let mut doctype_node = None;
1165 let mut orphan_nodes = Vec::new();
1166 let root_children = root.borrow().children().to_vec();
1167 for child in root_children {
1168 let is_doctype = matches!(&child.borrow().value, HtmlNodeType::Doctype { .. });
1169 if is_doctype {
1170 doctype_node = Some(child);
1171 } else {
1172 orphan_nodes.push(child);
1173 }
1174 }
1175
1176 root.borrow_mut().clear_children();
1177
1178 if let Some(dt) = doctype_node {
1179 TreeNode::add_child(&root, dt);
1180 } else {
1181 TreeNode::add_child_value(
1182 &root,
1183 HtmlNodeType::Doctype {
1184 name: Some("html".to_string()),
1185 public_id: None,
1186 system_id: None,
1187 },
1188 );
1189 }
1190
1191 let html_node = TreeNode::add_child_value(
1192 &root,
1193 HtmlNodeType::Element {
1194 tag_name: "html".to_string(),
1195 attributes: vec![],
1196 },
1197 );
1198
1199 let (head_node, body_node, head_content, body_content) = split_head_and_body(orphan_nodes);
1200 let head_node = match head_node {
1201 Some(node) => node,
1202 None => TreeNode::new(HtmlNodeType::Element {
1203 tag_name: "head".to_string(),
1204 attributes: vec![],
1205 }),
1206 };
1207 let body_node = match body_node {
1208 Some(node) => node,
1209 None => TreeNode::new(HtmlNodeType::Element {
1210 tag_name: "body".to_string(),
1211 attributes: vec![],
1212 }),
1213 };
1214
1215 TreeNode::add_child(&html_node, Rc::clone(&head_node));
1216 TreeNode::add_child(&html_node, Rc::clone(&body_node));
1217
1218 for node in head_content {
1219 TreeNode::add_child(&head_node, node);
1220 }
1221 for node in body_content {
1222 TreeNode::add_child(&body_node, node);
1223 }
1224 }
1225}
1226
1227fn is_inside_template(node: &NodeRef<HtmlNodeType>) -> bool {
1233 let mut current = node.borrow().parent();
1234 while let Some(n) = current {
1235 if n.borrow().value.tag_name() == Some("template") {
1236 return true;
1237 }
1238 current = n.borrow().parent();
1239 }
1240 false
1241}
1242
1243fn apply_suspense_replacements(tree: &DomTree) {
1260 let replacements: Vec<(String, String)> = tree
1262 .find_all(|n| n.tag_name() == Some("suspense-replace"))
1263 .iter()
1264 .filter_map(|node| {
1265 let n = node.borrow();
1266 let target = n.value.get_attr("target")?.trim().to_string();
1267 let template = n.value.get_attr("template")?.trim().to_string();
1268 Some((target, template))
1269 })
1270 .collect();
1271 if replacements.is_empty() {
1272 return;
1273 }
1274
1275 for (target, template_selector) in replacements {
1278 let template_id = template_selector
1279 .strip_prefix("template[for=")
1280 .and_then(|rest| rest.strip_suffix(']'))
1281 .map(str::to_string);
1282 let Some(template_id) = template_id else {
1283 log::warn!(
1284 target: "HtmlParser::Suspense",
1285 "unsupported suspense-replace template selector: {template_selector}"
1286 );
1287 continue;
1288 };
1289
1290 let fragment = tree
1292 .find_all(|n| n.tag_name() == Some("template"))
1293 .iter()
1294 .find(|node| {
1295 node.borrow()
1296 .value
1297 .get_attr("for")
1298 .is_some_and(|f| f == template_id)
1299 })
1300 .and_then(|template| {
1301 template.borrow().children().first().and_then(|first| {
1302 let is_fragment =
1303 matches!(&first.borrow().value, HtmlNodeType::DocumentFragment);
1304 is_fragment.then(|| Rc::clone(first))
1305 })
1306 });
1307 let Some(fragment) = fragment else {
1308 log::warn!(
1309 target: "HtmlParser::Suspense",
1310 "suspense-replace target {target}: no <template for={template_id}> content"
1311 );
1312 continue;
1313 };
1314
1315 let Some(placeholder) = find_element_by_id(tree, target.trim_start_matches('#')) else {
1317 continue;
1318 };
1319 let Some(placeholder_parent) = placeholder.borrow().parent() else {
1320 continue;
1321 };
1322
1323 let fragment_children: Vec<_> = fragment.borrow().children().to_vec();
1326 let mut all_inserted = true;
1327 for child in fragment_children {
1328 let clone = deep_clone_subtree(&child);
1329 all_inserted &= TreeNode::insert_before(&placeholder_parent, clone, &placeholder);
1330 }
1331 if all_inserted {
1332 TreeNode::remove_child(&placeholder_parent, &placeholder);
1333 }
1334
1335 let marker = tree
1337 .find_all(|n| n.tag_name() == Some("suspense-replace"))
1338 .into_iter()
1339 .find(|node| {
1340 node.borrow()
1341 .value
1342 .get_attr("target")
1343 .is_some_and(|t| t.trim() == target)
1344 });
1345 if let Some(marker) = marker {
1346 let marker_parent = marker.borrow().parent();
1347 if let Some(marker_parent) = marker_parent {
1348 TreeNode::remove_child(&marker_parent, &marker);
1349 }
1350 }
1351 }
1352}
1353
1354fn find_element_by_id(tree: &DomTree, id: &str) -> Option<NodeRef<HtmlNodeType>> {
1356 tree.find_all(|n| n.tag_name().is_some() && n.get_attr("id").is_some_and(|v| v == id))
1357 .iter()
1358 .find(|node| !is_inside_template(node))
1359 .map(Rc::clone)
1360}
1361
1362fn deep_clone_subtree(node: &NodeRef<HtmlNodeType>) -> NodeRef<HtmlNodeType> {
1364 let clone = TreeNode::new(node.borrow().value.clone());
1365 for child in node.borrow().children() {
1366 let cloned_child = deep_clone_subtree(child);
1367 TreeNode::add_child(&clone, cloned_child);
1368 }
1369 clone
1370}
1371
1372fn is_head_only_element(tag_name: &str) -> bool {
1374 matches!(
1375 tag_name.to_ascii_lowercase().as_str(),
1376 "base"
1377 | "basefont"
1378 | "bgsound"
1379 | "link"
1380 | "meta"
1381 | "noscript"
1382 | "script"
1383 | "style"
1384 | "template"
1385 | "title"
1386 )
1387}
1388
1389type HeadBodySplit = (
1393 Option<NodeRef<HtmlNodeType>>,
1394 Option<NodeRef<HtmlNodeType>>,
1395 Vec<NodeRef<HtmlNodeType>>,
1396 Vec<NodeRef<HtmlNodeType>>,
1397);
1398
1399fn split_head_and_body(nodes: Vec<NodeRef<HtmlNodeType>>) -> HeadBodySplit {
1405 let mut head_node: Option<NodeRef<HtmlNodeType>> = None;
1406 let mut body_node: Option<NodeRef<HtmlNodeType>> = None;
1407 let mut head_content: Vec<NodeRef<HtmlNodeType>> = Vec::new();
1408 let mut body_content: Vec<NodeRef<HtmlNodeType>> = Vec::new();
1409 let mut body_started = false;
1410
1411 for node in nodes {
1412 enum Slot {
1413 Head,
1414 Body,
1415 HeadContent,
1416 BodyContent,
1417 Drop,
1418 }
1419 let slot = match &node.borrow().value {
1420 HtmlNodeType::Element { tag_name, .. }
1421 if tag_name.eq_ignore_ascii_case("head") && head_node.is_none() =>
1422 {
1423 Slot::Head
1424 }
1425 HtmlNodeType::Element { tag_name, .. }
1426 if tag_name.eq_ignore_ascii_case("body") && body_node.is_none() =>
1427 {
1428 Slot::Body
1429 }
1430 HtmlNodeType::Element { tag_name, .. }
1431 if is_head_only_element(tag_name) && body_node.is_none() && !body_started =>
1432 {
1433 Slot::HeadContent
1434 }
1435 HtmlNodeType::Text(text) if text.trim().is_empty() => Slot::Drop,
1436 HtmlNodeType::Comment(_) => Slot::Drop,
1437 _ => Slot::BodyContent,
1438 };
1439 if matches!(slot, Slot::Body | Slot::BodyContent) {
1440 body_started = true;
1441 }
1442 match slot {
1443 Slot::Head => head_node = Some(node),
1444 Slot::Body => body_node = Some(node),
1445 Slot::HeadContent => head_content.push(node),
1446 Slot::BodyContent => body_content.push(node),
1447 Slot::Drop => {}
1448 }
1449 }
1450
1451 (head_node, body_node, head_content, body_content)
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456 use super::*;
1457
1458 fn parse(html: &str) -> DomTree {
1459 Parser::new(html).parse()
1460 }
1461
1462 fn tag_of(node: &NodeRef<HtmlNodeType>) -> Option<String> {
1463 node.borrow()
1464 .value
1465 .tag_name()
1466 .map(|t| t.to_ascii_lowercase())
1467 }
1468
1469 fn children_of(node: &NodeRef<HtmlNodeType>) -> Vec<NodeRef<HtmlNodeType>> {
1470 node.borrow().children().to_vec()
1471 }
1472
1473 fn find_first_fragment(node: &NodeRef<HtmlNodeType>) -> NodeRef<HtmlNodeType> {
1474 children_of(node)
1475 .into_iter()
1476 .find(|c| matches!(c.borrow().value, HtmlNodeType::DocumentFragment))
1477 .expect("template must own a DocumentFragment")
1478 }
1479
1480 #[test]
1481 fn checkedness_falls_back_to_the_attribute_but_live_state_wins() {
1482 let tree = parse(r#"<input id="a" checked><input id="b">"#);
1483
1484 let marked = tree.query_selector("#a").expect("marked input");
1486 assert!(checkedness(&marked));
1487
1488 let plain = tree.query_selector("#b").expect("plain input");
1489 assert!(!checkedness(&plain));
1490
1491 plain.borrow().set_checkedness(true);
1493 assert!(checkedness(&plain));
1494 marked.borrow().set_checkedness(false);
1495 assert!(!checkedness(&marked));
1496 assert!(marked.borrow().value.get_attr("checked").is_some());
1498 }
1499
1500 #[test]
1501 fn empty_and_first_child_respect_the_tree_shape() {
1502 let tree = parse("<html><head></head><body><p>hi</p><p> </p></body></html>");
1503
1504 let root = tree.query_selector("html").expect("root element");
1507 assert!(DomTree::element_matches_selector(&root, "html"));
1508 for selector in [
1509 ":first-child",
1510 ":last-child",
1511 ":only-child",
1512 ":first-of-type",
1513 ":last-of-type",
1514 ":only-of-type",
1515 ] {
1516 assert!(
1517 !DomTree::element_matches_selector(&root, selector),
1518 "{selector} matched the parentless root element"
1519 );
1520 }
1521 assert!(!DomTree::element_matches_selector(
1523 &tree.root,
1524 ":first-child"
1525 ));
1526
1527 let head = tree.query_selector("head").expect("head");
1530 let body = tree.query_selector("body").expect("body");
1531 assert!(DomTree::element_matches_selector(&head, ":empty"));
1532 assert!(!DomTree::element_matches_selector(&body, ":empty"));
1533
1534 let paragraphs = tree.query_selector_all("p");
1535 assert_eq!(paragraphs.len(), 2);
1536 assert!(!DomTree::element_matches_selector(¶graphs[0], ":empty"));
1537 assert!(!DomTree::element_matches_selector(¶graphs[1], ":empty"));
1539 }
1540
1541 #[test]
1542 fn lang_pseudo_class_is_inherited_from_the_nearest_ancestor() {
1543 let tree = parse(r#"<div lang="en-GB"><p><em>x</em></p></div><div lang="fr"></div>"#);
1544
1545 let british = tree.query_selector("div[lang]").expect("outer div");
1546 let descendant = tree.query_selector("em").expect("descendant");
1547 let french = tree
1548 .query_selector_all("div")
1549 .into_iter()
1550 .nth(1)
1551 .expect("second div");
1552
1553 assert!(DomTree::element_matches_selector(&british, ":lang(en)"));
1554 assert!(DomTree::element_matches_selector(&descendant, ":lang(en)"));
1555 assert!(!DomTree::element_matches_selector(&descendant, ":lang(fr)"));
1556 assert!(!DomTree::element_matches_selector(&french, ":lang(en)"));
1557 }
1558
1559 #[test]
1560 fn template_contents_go_into_its_content_fragment() {
1561 let tree = parse(r#"<template><div id="x">hi</div></template>"#);
1562 let template = tree
1563 .query_selector("template")
1564 .expect("template must be in the document");
1565 assert_eq!(tag_of(&template).as_deref(), Some("template"));
1566
1567 let fragment = find_first_fragment(&template);
1568 let content = children_of(&fragment);
1569 assert_eq!(content.len(), 1);
1570 assert_eq!(tag_of(&content[0]).as_deref(), Some("div"));
1571
1572 assert!(tree.query_selector("#x").is_none());
1575 let body = tree.query_selector("html > body").unwrap();
1576 assert!(
1577 !children_of(&body)
1578 .iter()
1579 .any(|c| tag_of(c).as_deref() == Some("div"))
1580 );
1581
1582 let text = DomTree::inner_text(&content[0]);
1583 assert_eq!(text, "hi");
1584 }
1585
1586 #[test]
1587 fn scripts_inside_template_are_inert() {
1588 let tree = parse(r#"<template><script>leak()</script></template><script>ok()</script>"#);
1589 let scripts = tree.collect_classic_scripts();
1590 assert_eq!(
1591 scripts,
1592 vec![ClassicScriptSource::Inline("ok()".to_string())]
1593 );
1594 }
1595
1596 #[test]
1597 fn nested_templates_own_separate_fragments() {
1598 let tree = parse(r#"<template><template><b>deep</b></template><i>x</i></template>"#);
1599
1600 let templates = tree.get_elements_by_tag_name("template");
1603 assert_eq!(templates.len(), 1);
1604
1605 let outer = Rc::clone(&templates[0]);
1606 let outer_fragment = find_first_fragment(&outer);
1607 let outer_children = children_of(&outer_fragment);
1608 assert_eq!(outer_children.len(), 2);
1610 assert_eq!(tag_of(&outer_children[0]).as_deref(), Some("template"));
1611 assert_eq!(tag_of(&outer_children[1]).as_deref(), Some("i"));
1612
1613 let inner_fragment = find_first_fragment(&outer_children[0]);
1614 let inner_children = children_of(&inner_fragment);
1615 assert_eq!(inner_children.len(), 1);
1616 assert_eq!(tag_of(&inner_children[0]).as_deref(), Some("b"));
1617 assert_eq!(DomTree::inner_text(&inner_children[0]), "deep");
1618 }
1619
1620 #[test]
1621 fn template_in_head_stays_in_head() {
1622 let tree = parse(r#"<template><title>t</title></template>"#);
1623 let template = tree
1624 .query_selector("html > head > template")
1625 .expect("template in head stays in head");
1626 assert!(tree.query_selector("title").is_none());
1628 assert_eq!(DomTree::inner_text(&find_first_fragment(&template)), "t");
1629 }
1630}