Skip to main content

orinium_browser/engine/html/
parser.rs

1//! HTMLパーサー。トークンストリームをDOMツリーに変換する。
2
3use 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    /// Shadow root attached to a host element.
31    /// Children live here and are not part of the host's normal children.
32    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    /// Processing instruction (e.g. `<?xml-stylesheet ...?>`).
43    ProcessingInstruction {
44        target: String,
45        data: String,
46    },
47    InvalidNode(Token, String), // 不正なトークン用
48}
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/// Source of a classic JavaScript script in document order.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum ClassicScriptSource {
104    Inline(String),
105    External(String),
106}
107
108/// Whether scripting is enabled while parsing.
109///
110/// Browsers run scripts by default, so the default mode is [`ScriptingMode::Enabled`].
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum ScriptingMode {
113    #[default]
114    Enabled,
115    Disabled,
116}
117
118/// Scheduling mode selected by attributes on a classic script element.
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
120pub enum ClassicScriptExecution {
121    #[default]
122    Default,
123    Defer,
124    Async,
125}
126
127/// A classic script source together with its requested scheduling mode.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct ClassicScriptDescriptor {
130    pub source: ClassicScriptSource,
131    pub execution: ClassicScriptExecution,
132}
133
134impl DomTree {
135    /// Returns all elements with the given tag name
136    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    /// Returns the element with the given id
150    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    /// Returns all elements that have the given class
165    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    /// Returns the concatenated text content of this node (including children).
181    /// Shadow root children are skipped (they are not part of light DOM text).
182    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                // Skip shadow root children — not part of light DOM.
190                .filter(|c| !matches!(c.borrow().value, HtmlNodeType::ShadowRoot { .. }))
191                .map(DomTree::inner_text)
192                .collect(),
193            HtmlNodeType::ShadowRoot { .. } => {
194                // Inside a shadow root, traverse its children.
195                n.children().iter().map(DomTree::inner_text).collect()
196            }
197            _ => "".to_string(),
198        }
199    }
200
201    /// Replace all text content of this node with the given string
202    pub fn set_text_content(node: &NodeRef<HtmlNodeType>, new_text: &str) {
203        // Do not hold a borrow across child mutations (would double-borrow).
204        if let HtmlNodeType::Text(content) = &mut node.borrow_mut().value {
205            *content = new_text.to_string();
206            return;
207        }
208
209        // `node.borrow()` must end before mutating below, so evaluate the
210        // condition in its own statement.
211        let is_element = matches!(node.borrow().value, HtmlNodeType::Element { .. });
212        if is_element {
213            // remove all children and add a single Text node
214            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    /// 指定したタグ名の要素のテキストノードをすべて集める
221    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    /// Collects classic scripts in document order.
250    ///
251    /// Module scripts and data blocks with a non-JavaScript MIME type are not
252    /// classic scripts and are ignored here.
253    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    /// Returns the first element matching `selector` in document order.
261    pub fn query_selector(&self, selector: &str) -> Option<NodeRef<HtmlNodeType>> {
262        self.query_selector_all(selector).into_iter().next()
263    }
264
265    /// Returns all elements matching `selector` in document order.
266    pub fn query_selector_all(&self, selector: &str) -> Vec<NodeRef<HtmlNodeType>> {
267        query_selector_all_from(&self.root, selector, true)
268    }
269
270    /// Returns the first matching descendant of `scope` in document order.
271    ///
272    /// The scope element itself is not considered, matching Element's DOM API.
273    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    /// Returns all matching descendants of `scope` in document order.
283    ///
284    /// The scope element itself is not included in the result.
285    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    /// Returns `true` if the element itself matches the given CSS selector.
293    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    /// Walks ancestors starting from `node` and returns the first ancestor
303    /// (including the node itself) that matches the given CSS selector.
304    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            // Stop at shadow boundary — closest() should not cross it.
316            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    /// Collects classic scripts and their scheduling attributes in document order.
330    ///
331    /// Scripts nested inside a `<template>` element are skipped: template
332    /// contents are inert until the template is cloned.
333    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                    // TODO: For real ES modules, use module graph loader; collect separately.
347                    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                                // HTML modules are deferred by default.
354                                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                        // Inline module scripts: skip collection when the source
368                        // contains top-level `import`/`export` keywords, since
369                        // feeding ES module syntax to the classic parser would
370                        // raise a SyntaxError.
371                        // TODO: parse modules properly with pixi_byte module support.
372                        if is_module && contains_top_level_module_keyword(&source) {
373                            return None;
374                        }
375                        Some(ClassicScriptDescriptor {
376                            source: ClassicScriptSource::Inline(source),
377                            // `async` and `defer` have no effect on inline classic scripts.
378                            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    /// Collects only inline classic scripts.
395    ///
396    /// Kept for callers that do not yet fetch external script resources.
397    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    // Shadow root children are inside the shadow tree — only
470    // reachable through the shadow root, not through the host element's
471    // light DOM traversal.  Skip them when traversing light DOM.
472    if !is_shadow {
473        for child in children {
474            // Skip shadow root children during light DOM traversal.
475            let is_child_shadow = matches!(child.borrow().value, HtmlNodeType::ShadowRoot { .. });
476            // Skip template contents: a `<template>`'s DocumentFragment child
477            // is inert, so document-wide element queries never descend into
478            // it (matching browsers; `template.content` must be queried
479            // directly, in which case the fragment itself is the traversal
480            // root and this skip does not apply).
481            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        // We're inside a shadow root — do traverse its children
489        // (shadow tree is traversable from within).
490        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        // Stop at shadow boundary — element chains should not cross it.
501        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
512/// The effective checkedness of a checkbox or radio: the live state when the
513/// script has set one, otherwise the presence of the `checked` content
514/// attribute, which supplies the initial value (`defaultChecked`).
515pub 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
521/// The `lang` attribute in effect for a node: its own, or failing that the
522/// nearest ancestor's. `:lang()` matches against this rather than the raw
523/// attribute, so language is inherited through the tree.
524fn 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
541/// The Selectors 4 `:empty` test: no element children, and no text node whose
542/// data is non-empty. Comments and processing instructions are ignored, as are
543/// empty text nodes.
544fn 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
638/// Whether `node` sits below a parent *element*. A document or shadow root does
639/// not count, so the root element fails `:first-child` and friends.
640fn 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
699/// Heuristic check for top-level ES module `import`/`export` keywords.
700///
701/// Used to skip inline module scripts that cannot be parsed by the classic
702/// script parser. This is a best-effort textual check (not a full parse),
703/// since only inline text is available at collection time.
704fn 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                // Dynamic `import(...)` is valid in classic scripts, so do not
725                // treat it as an ES module import statement.
726                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
741// TODO: temporary handling of ES modules as classic scripts until real ES
742// module support exists (module graph, import/export, top-level await,
743// strict mode, CORS, etc.). Remove once proper module loading is implemented.
744fn 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                // TODO: With full ES module support, modules should NOT be treated as classic scripts.
757                | "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>, // script/style/noscript 用
767    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    /// Sets the scripting mode used while parsing `<noscript>` contents.
785    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                // TODO:
817                // attributes, self_closing
818                TreeNode::add_child_value(&parent, HtmlNodeType::Text(format!("<{}>", name)));
819                return;
820            }
821
822            // Table-context auto-insertion: a <tbody> is implied around
823            // <tr>/<td>/<th> (and sections around <caption>/<col>/<colgroup>)
824            // inserted directly into a <table>.
825            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            // noscript は scripting フラグに応じて特別な処理を行う
846            if name == "noscript" {
847                self.handle_noscript(attributes);
848                return;
849            }
850
851            // <template> keeps its children in a DocumentFragment
852            // (template.content). The contents are inert: layout and script
853            // collection never descend into the fragment.
854            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                // Push the template element (for end-tag matching) and the
866                // fragment on top, so contents are inserted into the fragment.
867                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            // script/style は special mode に
891            if name == "script" || name == "style" {
892                self.special_text_mode = Some(name.clone());
893            }
894
895            // HTML の void 要素は自行終了扱い(stack に push しない)
896            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            // Self-closing タグは stack に push しない
914            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    /// 現時点では body と同じ扱い(scripting 有効なら raw text)で、
937    /// spec の "in head noscript" 挿入モード(link/meta/style の処理など)は
938    /// head の挿入モードを導入した際に実装する。
939    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    /// scripting 有効なら raw text
947    /// scripting 無効なら 通常の HTML
948    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    /// `<noscript>` の内容を raw text としてパースする
956    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    /// `<noscript>` の内容を通常の HTML としてパースする
962    fn parse_noscript_as_html(&mut self, attributes: Vec<Attribute>) {
963        self.push_element("noscript", attributes);
964    }
965
966    /// 要素を生成して stack に push する。
967    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            // special mode を解除
983            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            // special mode 中はそのままテキスト追加
1025            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            // <html> 以外の中に <body> が来た場合、そのタグを閉じる
1068            if tag_name != "html" && name == "body" {
1069                println!("here we can see 「お行儀の悪いコード」");
1070                return true;
1071            }
1072            // <p> の中に <p> が来た場合、前の <p> を閉じる
1073            if tag_name == "p" && name == "p" {
1074                return true;
1075            }
1076            // <li> の中に <li> が来た場合、前の <li> を閉じる
1077            if tag_name == "li" && name == "li" {
1078                return true;
1079            }
1080            // <a> の中に <a> が来た場合、前の <a> を閉じる
1081            if tag_name == "a" && name == "a" {
1082                return true;
1083            }
1084            // <dt> の中に <dt> または <dd> が来た場合、前の <dt> を閉じる
1085            if tag_name == "dt" && (name == "dt" || name == "dd") {
1086                return true;
1087            }
1088            // <dd> の中に <dt> または <dd> が来た場合、前の <dd> を閉じる
1089            if tag_name == "dd" && (name == "dt" || name == "dd") {
1090                return true;
1091            }
1092            // <option> の中に <option> が来た場合、前の <option> を閉じる
1093            if tag_name == "option" && name == "option" {
1094                return true;
1095            }
1096            // <p> の中にブロック要素が来た場合、前の <p> を閉じる
1097            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    /// DOCTYPE宣言、html, head, body 要素が存在しない場合に補完する
1109    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            // <html> が明示されている場合でも head/body が無ければ補完する
1121            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        // <html> が無い場合: ドキュメント直下のノードを head/body に振り分けて
1163        // 暗示的な <html> で包む。既存の <head>/<body> 要素はそのまま再利用する。
1164        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
1227/// Returns true if `node` is nested inside a `<template>` element.
1228///
1229/// Template contents are inert, so this excludes them from collectable
1230/// things such as document classic scripts. The `<template>` element
1231/// itself is *not* considered inside (it is a normal element).
1232fn 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
1243/// Applies Reddit-style `<suspense-replace>` streaming-SSR swaps.
1244///
1245/// Reddit (and other suspense-based frontends) stream pages as:
1246///
1247/// ```html
1248/// <suspense-placeholder id="s_1">…skeleton…</suspense-placeholder>
1249/// <template for="s_1">…real content (shreddit-feed, posts)…</template>
1250/// <suspense-replace target="#s_1" template="template[for=s_1]"></suspense-replace>
1251/// ```
1252///
1253/// The real content lives inside a `<template>`, which is inert per the HTML
1254/// spec, and their client script swaps it into the placeholder on load. Since
1255/// real posts are only reachable after that swap, the parser performs it
1256/// eagerly: each placeholder is replaced by a deep clone of the matching
1257/// template's fragment children, and the `<suspense-replace>` markers are
1258/// dropped. Pages without this pattern are unaffected.
1259fn apply_suspense_replacements(tree: &DomTree) {
1260    // Collect (target_id, template_selector) from every <suspense-replace>.
1261    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    // The `template` attribute value is a selector of the shape
1276    // `template[for=<id>]`; extract the id once per replacement.
1277    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        // Source content: the <template for=...>'s DocumentFragment child.
1291        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        // Destination: the placeholder element with the target id.
1316        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        // Insert a deep clone of every fragment child at the placeholder's
1324        // position, then drop the placeholder (and its skeleton contents).
1325        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        // Remove the <suspense-replace> marker element itself.
1336        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
1354/// Finds an element by `id` anywhere in the tree (outside templates).
1355fn 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
1362/// Deep-clones a subtree, dropping parent links.
1363fn 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
1372/// 要素が head 内にのみ置ける要素 (meta, title, link, style, script, ...) か
1373fn 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
1389/// ドキュメント直下 (または <html> 直下) のノード列を head 側と body 側に
1390/// 振り分ける。既存の <head>/<body> 要素があればそれをそのまま返し、
1391/// 無ければ None を返す (呼び出し側が新規作成する)。
1392type HeadBodySplit = (
1393    Option<NodeRef<HtmlNodeType>>,
1394    Option<NodeRef<HtmlNodeType>>,
1395    Vec<NodeRef<HtmlNodeType>>,
1396    Vec<NodeRef<HtmlNodeType>>,
1397);
1398
1399/// - 既存の <head> より前の head 系要素・コメント・空白テキストは head へ
1400/// - <body> が現れるまでの head 系要素は head へ
1401/// - それ以降のノードはすべて body へ
1402///
1403///   と振り分ける。空白のみのテキストノードは破棄する。
1404fn 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        // The `checked` attribute seeds the initial value.
1485        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        // The live state overrides the attribute in both directions.
1492        plain.borrow().set_checkedness(true);
1493        assert!(checkedness(&plain));
1494        marked.borrow().set_checkedness(false);
1495        assert!(!checkedness(&marked));
1496        // ...and writing the live state leaves the attribute alone.
1497        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        // The document above `<html>` is not a parent element, so the root
1505        // element must not match the structural pseudo-classes.
1506        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        // The document node itself is not an element, so nothing matches it.
1522        assert!(!DomTree::element_matches_selector(
1523            &tree.root,
1524            ":first-child"
1525        ));
1526
1527        // `<head>` has no children, `<body>` has elements, and a text node with
1528        // data keeps an element from being `:empty`.
1529        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(&paragraphs[0], ":empty"));
1537        // Whitespace-only text still counts, per the HTML definition of `:empty`.
1538        assert!(!DomTree::element_matches_selector(&paragraphs[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        // Nothing leaks to the page: <div id="x"> lives only inside the
1573        // template, so document-wide queries never find it.
1574        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        // The inner template lives in inert content, so only the outer one is
1601        // reachable from document-level queries (matching browsers).
1602        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        // Inner template element and the trailing <i>, in order.
1609        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        // The <title> is inert template content: unreachable from the document.
1627        assert!(tree.query_selector("title").is_none());
1628        assert_eq!(DomTree::inner_text(&find_first_fragment(&template)), "t");
1629    }
1630}