Skip to main content

html5_parser/
lib.rs

1// Public API surface: `parse` plus the read-only tree types
2// (`Document`/`NodeId`/`NodeKind`/`Attribute`/`Position`/`Node`/`Children`)
3// needed to walk its output — just enough for html-conform's
4// `src/infoset.rs::normalize()` to consume, per Step 1 of this crate's
5// two-stage scope (see `CLAUDE.md`). `Tokenizer`/`TreeBuilder` and
6// everything else stay crate-internal; there's no commitment to their
7// shape yet. See plan/DECISIONS.md.
8
9mod document;
10mod entities;
11mod tokenizer;
12mod tree_builder;
13
14pub use document::{Attribute, Children, Document, Node, NodeId, NodeKind};
15pub use tokenizer::{ParseError, ParseErrorKind, Position};
16
17use tokenizer::Tokenizer;
18use tree_builder::TreeBuilder;
19
20/// [`parse`]'s return value: the parsed [`Document`] tree plus every
21/// WHATWG "parse error" (§13.2.2) encountered along the way. Both fields
22/// together, not a `Result` — parse errors are never fatal, `document`
23/// is always complete regardless of how many occurred (see
24/// [`ParseError`]'s doc comment).
25#[derive(Debug)]
26pub struct ParseResult {
27    pub document: Document,
28    pub errors: Vec<ParseError>,
29}
30
31/// The driver loop (§13.2 "Parsing HTML documents", the "tokenization and
32/// tree construction" step): parses `input` into a [`Document`] tree with
33/// per-node source positions, feeding it through the tokenizer and handing
34/// each token to the tree builder, applying the two pieces of feedback
35/// tree construction sends back to the tokenizer — a state switch
36/// (`Tokenizer::switch_to`, for RCDATA/RAWTEXT/script-data/PLAINTEXT
37/// elements) and the foreign-content flag (`Tokenizer::set_in_foreign_content`,
38/// consulted only by CDATA-section handling).
39///
40/// The tokenizer's iterator yields exactly one `Eof` token and then ends
41/// (`None`) on the next call, so the loop needs no separate condition for
42/// *when* to stop feeding it tokens. `TreeBuilder::stop_parsing` (§13.2.7
43/// "The end") still runs once, explicitly, right after — its one
44/// tree-shape-relevant step ("pop all the nodes off the stack of open
45/// elements") isn't implied by the loop simply ending.
46///
47/// Returns [`ParseResult`], not a bare [`Document`], as of Phase 07
48/// (`plan/07-parse-errors.md`) — `errors` currently covers every
49/// tokenizer-level parse error (`src/tokenizer.rs`'s `error()` call
50/// sites); tree-construction-level errors (e.g. stray end tags) are
51/// follow-up work, not yet collected here.
52pub fn parse(input: &str) -> ParseResult {
53    let mut tokenizer = Tokenizer::new(input);
54    let mut tree_builder = TreeBuilder::new();
55    while let Some(token) = tokenizer.next() {
56        if let Some(state) = tree_builder.process_token(&token.kind, token.position) {
57            tokenizer.switch_to(state);
58        }
59        tokenizer.set_in_foreign_content(tree_builder.is_in_foreign_content());
60    }
61    tree_builder.stop_parsing();
62    ParseResult {
63        document: tree_builder.into_document(),
64        errors: tokenizer.take_errors(),
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::parse;
71    use crate::document::{Document, NodeId, NodeKind};
72    use crate::tree_builder::{HTML_NAMESPACE, MATHML_NAMESPACE, SVG_NAMESPACE};
73
74    /// Navigates root -> html -> body, the common starting point for most
75    /// of this module's tree-shape assertions.
76    fn body_of(document: &Document) -> NodeId {
77        let root = document.root();
78        // Skip a possible leading DOCTYPE sibling to find the html
79        // element, root's first *element* child rather than its first
80        // child outright.
81        let html = document
82            .children(root)
83            .find(|&node| matches!(document.node(node).kind, NodeKind::Element { .. }))
84            .unwrap();
85        document.children(html).nth(1).unwrap()
86    }
87
88    #[test]
89    fn parses_a_minimal_document_into_the_expected_tree_shape() {
90        let document = parse(
91            "<!DOCTYPE html><html><head><title>Hi</title></head><body><p>Hello</p></body></html>",
92        )
93        .document;
94
95        let root = document.root();
96        let root_children: Vec<_> = document.children(root).collect();
97        assert_eq!(root_children.len(), 2);
98        assert_eq!(
99            document.node(root_children[0]).kind,
100            NodeKind::Doctype {
101                name: Some("html".to_owned()),
102                public_identifier: Some(String::new()),
103                system_identifier: Some(String::new()),
104            }
105        );
106
107        let html = root_children[1];
108        let html_children: Vec<_> = document.children(html).collect();
109        assert_eq!(html_children.len(), 2);
110        let (head, body) = (html_children[0], html_children[1]);
111
112        let title = document.children(head).next().unwrap();
113        let NodeKind::Element { name, .. } = &document.node(title).kind else {
114            unreachable!()
115        };
116        assert_eq!(name, "title");
117        let title_text = document.children(title).next().unwrap();
118        assert_eq!(
119            document.node(title_text).kind,
120            NodeKind::Text {
121                content: "Hi".to_owned()
122            }
123        );
124
125        let p = document.children(body).next().unwrap();
126        let NodeKind::Element { name, .. } = &document.node(p).kind else {
127            unreachable!()
128        };
129        assert_eq!(name, "p");
130        let p_text = document.children(p).next().unwrap();
131        assert_eq!(
132            document.node(p_text).kind,
133            NodeKind::Text {
134                content: "Hello".to_owned()
135            }
136        );
137    }
138
139    #[test]
140    fn parses_implied_html_head_body_when_missing() {
141        let document = parse("<p>Hello</p>").document;
142
143        let root = document.root();
144        assert_eq!(document.children(root).count(), 1);
145        let html = document.children(root).next().unwrap();
146        let html_children: Vec<_> = document.children(html).collect();
147        assert_eq!(html_children.len(), 2);
148        let body = html_children[1];
149
150        let p = document.children(body).next().unwrap();
151        let NodeKind::Element { name, .. } = &document.node(p).kind else {
152            unreachable!()
153        };
154        assert_eq!(name, "p");
155    }
156
157    #[test]
158    fn rcdata_element_content_is_not_parsed_as_markup() {
159        let document = parse("<title><b>not bold</b></title>").document;
160
161        let root = document.root();
162        let html = document.children(root).next().unwrap();
163        let head = document.children(html).next().unwrap();
164        let title = document.children(head).next().unwrap();
165        let text = document.children(title).next().unwrap();
166        assert_eq!(
167            document.node(text).kind,
168            NodeKind::Text {
169                content: "<b>not bold</b>".to_owned()
170            }
171        );
172    }
173
174    #[test]
175    fn parse_syncs_in_foreign_content_for_cdata_sections() {
176        // Exercises the one piece of driver-loop wiring no lower-level
177        // test can reach: Tokenizer::set_in_foreign_content is only
178        // ever called from here, based on the *real* TreeBuilder state
179        // after processing the <svg> start tag.
180        let document = parse("<svg><![CDATA[hello]]></svg>").document;
181
182        let root = document.root();
183        let html = document.children(root).next().unwrap();
184        let body = document.children(html).nth(1).unwrap();
185        let svg = document.children(body).next().unwrap();
186        let content = document.children(svg).next().unwrap();
187        assert_eq!(
188            document.node(content).kind,
189            NodeKind::Text {
190                content: "hello".to_owned()
191            }
192        );
193    }
194
195    #[test]
196    fn cdata_outside_foreign_content_becomes_a_bogus_comment() {
197        let document = parse("<p><![CDATA[hello]]></p>").document;
198
199        let root = document.root();
200        let html = document.children(root).next().unwrap();
201        let body = document.children(html).nth(1).unwrap();
202        let p = document.children(body).next().unwrap();
203        let content = document.children(p).next().unwrap();
204        assert_eq!(
205            document.node(content).kind,
206            NodeKind::Comment {
207                content: "[CDATA[hello]]".to_owned()
208            }
209        );
210    }
211
212    // The test matrix from plan/03-tree-construction.md's "Testmatrix"
213    // step: cases ported from html-conform's own src/infoset.rs test
214    // matrix (so the eventual switch from its current HTML5-parsing
215    // dependency to this crate is behavior-preserving), plus spec-
216    // derived cases html-conform doesn't cover at all (adoption agency,
217    // quirks mode's effect on tree shape, tables without explicit
218    // tbody/tr) that the phase's exit criteria call out explicitly.
219
220    #[test]
221    fn optional_end_tags_produce_sibling_li_elements() {
222        // Ported from html-conform's optional_end_tags_produce_sibling_elements.
223        let document = parse("<ul><li>a<li>b</ul>").document;
224        let body = body_of(&document);
225        let ul = document.children(body).next().unwrap();
226        let items: Vec<_> = document.children(ul).collect();
227        assert_eq!(items.len(), 2);
228        for (&li, expected_text) in items.iter().zip(["a", "b"]) {
229            let NodeKind::Element { name, .. } = &document.node(li).kind else {
230                unreachable!()
231            };
232            assert_eq!(name, "li");
233            let text = document.children(li).next().unwrap();
234            assert_eq!(
235                document.node(text).kind,
236                NodeKind::Text {
237                    content: expected_text.to_owned()
238                }
239            );
240        }
241    }
242
243    #[test]
244    fn svg_element_keeps_svg_namespace_end_to_end() {
245        // Ported from html-conform's svg_elements_keep_svg_namespace.
246        let document = parse("<svg><circle/></svg>").document;
247        let body = body_of(&document);
248        let svg = document.children(body).next().unwrap();
249        assert_eq!(
250            document.node(svg).kind,
251            NodeKind::Element {
252                name: "svg".to_owned(),
253                namespace: Some(SVG_NAMESPACE.to_owned()),
254                attributes: vec![],
255            }
256        );
257        let circle = document.children(svg).next().unwrap();
258        assert_eq!(
259            document.node(circle).kind,
260            NodeKind::Element {
261                name: "circle".to_owned(),
262                namespace: Some(SVG_NAMESPACE.to_owned()),
263                attributes: vec![],
264            }
265        );
266    }
267
268    #[test]
269    fn mathml_element_keeps_mathml_namespace_end_to_end() {
270        // Ported from html-conform's mathml_elements_keep_mathml_namespace.
271        let document = parse("<math><mi>x</mi></math>").document;
272        let body = body_of(&document);
273        let math = document.children(body).next().unwrap();
274        assert_eq!(
275            document.node(math).kind,
276            NodeKind::Element {
277                name: "math".to_owned(),
278                namespace: Some(MATHML_NAMESPACE.to_owned()),
279                attributes: vec![],
280            }
281        );
282        let mi = document.children(math).next().unwrap();
283        assert_eq!(
284            document.node(mi).kind,
285            NodeKind::Element {
286                name: "mi".to_owned(),
287                namespace: Some(MATHML_NAMESPACE.to_owned()),
288                attributes: vec![],
289            }
290        );
291        let text = document.children(mi).next().unwrap();
292        assert_eq!(
293            document.node(text).kind,
294            NodeKind::Text {
295                content: "x".to_owned()
296            }
297        );
298    }
299
300    #[test]
301    fn script_content_is_not_tokenized_as_markup() {
302        // Ported from html-conform's script_and_style_content_normalize_to_plain_text.
303        let document = parse("<script>1 < 2;</script>").document;
304        let root = document.root();
305        let html = document.children(root).next().unwrap();
306        let head = document.children(html).next().unwrap();
307        let script = document.children(head).next().unwrap();
308        let NodeKind::Element { name, .. } = &document.node(script).kind else {
309            unreachable!()
310        };
311        assert_eq!(name, "script");
312        let text = document.children(script).next().unwrap();
313        assert_eq!(
314            document.node(text).kind,
315            NodeKind::Text {
316                content: "1 < 2;".to_owned()
317            }
318        );
319    }
320
321    #[test]
322    fn style_content_is_not_tokenized_as_markup() {
323        // Ported from html-conform's script_and_style_content_normalize_to_plain_text.
324        let document = parse("<style>a{color:red}</style>").document;
325        let root = document.root();
326        let html = document.children(root).next().unwrap();
327        let head = document.children(html).next().unwrap();
328        let style = document.children(head).next().unwrap();
329        let NodeKind::Element { name, .. } = &document.node(style).kind else {
330            unreachable!()
331        };
332        assert_eq!(name, "style");
333        let text = document.children(style).next().unwrap();
334        assert_eq!(
335            document.node(text).kind,
336            NodeKind::Text {
337                content: "a{color:red}".to_owned()
338            }
339        );
340    }
341
342    #[test]
343    fn named_character_references_resolve_to_decoded_text() {
344        // Ported from html-conform's named_entities_resolve_to_decoded_text.
345        let document = parse("<p>&amp; &copy;</p>").document;
346        let body = body_of(&document);
347        let p = document.children(body).next().unwrap();
348        let text = document.children(p).next().unwrap();
349        assert_eq!(
350            document.node(text).kind,
351            NodeKind::Text {
352                content: "& \u{a9}".to_owned()
353            }
354        );
355    }
356
357    #[test]
358    fn custom_element_gets_html_namespace_like_any_plain_element() {
359        // Ported from html-conform's custom_element_gets_xhtml_namespace_like_any_plain_element.
360        let document = parse("<my-widget>hi</my-widget>").document;
361        let body = body_of(&document);
362        let widget = document.children(body).next().unwrap();
363        assert_eq!(
364            document.node(widget).kind,
365            NodeKind::Element {
366                name: "my-widget".to_owned(),
367                namespace: Some(HTML_NAMESPACE.to_owned()),
368                attributes: vec![],
369            }
370        );
371        let text = document.children(widget).next().unwrap();
372        assert_eq!(
373            document.node(text).kind,
374            NodeKind::Text {
375                content: "hi".to_owned()
376            }
377        );
378    }
379
380    #[test]
381    fn xml_lang_attribute_stays_a_literal_unnamespaced_attribute_name() {
382        // On a plain HTML element (not inside SVG/MathML foreign
383        // content), general HTML5 parsing never namespace-splits
384        // xml:lang - "adjust foreign attributes" (§13.2.6.1) only
385        // applies while inserting a foreign element. html-conform's own
386        // xml:lang tests are about its RELAX-NG-schema adapter's own
387        // remapping on top of this, a schema-validation-specific
388        // concern, not a general parsing fact this crate should assert
389        // - this just verifies the parser's own (unremapped) output.
390        let document = parse(r#"<p xml:lang="de">hi</p>"#).document;
391        let body = body_of(&document);
392        let p = document.children(body).next().unwrap();
393        let NodeKind::Element { attributes, .. } = &document.node(p).kind else {
394            unreachable!()
395        };
396        assert_eq!(attributes.len(), 1);
397        assert_eq!(attributes[0].name, "xml:lang");
398        assert_eq!(attributes[0].value, "de");
399        assert_eq!(attributes[0].namespace, None);
400    }
401
402    #[test]
403    fn table_without_tbody_or_tr_gets_them_synthesized() {
404        // Spec-derived case (§13.2.6.4.9/.13/.14's implied-tag rules),
405        // not covered by html-conform's own test matrix at all.
406        let document = parse("<table><td>x</td></table>").document;
407        let body = body_of(&document);
408        let table = document.children(body).next().unwrap();
409        let tbody = document.children(table).next().unwrap();
410        let NodeKind::Element { name, .. } = &document.node(tbody).kind else {
411            unreachable!()
412        };
413        assert_eq!(name, "tbody");
414        let tr = document.children(tbody).next().unwrap();
415        let NodeKind::Element { name, .. } = &document.node(tr).kind else {
416            unreachable!()
417        };
418        assert_eq!(name, "tr");
419        let td = document.children(tr).next().unwrap();
420        let NodeKind::Element { name, .. } = &document.node(td).kind else {
421            unreachable!()
422        };
423        assert_eq!(name, "td");
424        let text = document.children(td).next().unwrap();
425        assert_eq!(
426            document.node(text).kind,
427            NodeKind::Text {
428                content: "x".to_owned()
429            }
430        );
431    }
432
433    #[test]
434    fn adoption_agency_spec_example_misnested_b_i_tags() {
435        // §13.2.10.1 "Misnested tags: <b><i></b></i>" - the spec's own
436        // fully worked, non-normative example, including its final DOM
437        // tree spelled out in prose: html > head, body > p > [#text:1,
438        // b > [#text:2, i > #text:3], i > #text:4, #text:5]. Not
439        // covered by html-conform's own test matrix at all.
440        let document = parse("<p>1<b>2<i>3</b>4</i>5</p>").document;
441        let body = body_of(&document);
442        let p = document.children(body).next().unwrap();
443        let p_children: Vec<_> = document.children(p).collect();
444        assert_eq!(p_children.len(), 4);
445        assert_eq!(
446            document.node(p_children[0]).kind,
447            NodeKind::Text {
448                content: "1".to_owned()
449            }
450        );
451
452        let b = p_children[1];
453        let NodeKind::Element { name, .. } = &document.node(b).kind else {
454            unreachable!()
455        };
456        assert_eq!(name, "b");
457        let b_children: Vec<_> = document.children(b).collect();
458        assert_eq!(b_children.len(), 2);
459        assert_eq!(
460            document.node(b_children[0]).kind,
461            NodeKind::Text {
462                content: "2".to_owned()
463            }
464        );
465        let inner_i = b_children[1];
466        let NodeKind::Element { name, .. } = &document.node(inner_i).kind else {
467            unreachable!()
468        };
469        assert_eq!(name, "i");
470        let inner_i_text = document.children(inner_i).next().unwrap();
471        assert_eq!(
472            document.node(inner_i_text).kind,
473            NodeKind::Text {
474                content: "3".to_owned()
475            }
476        );
477
478        let outer_i = p_children[2];
479        let NodeKind::Element { name, .. } = &document.node(outer_i).kind else {
480            unreachable!()
481        };
482        assert_eq!(name, "i");
483        let outer_i_text = document.children(outer_i).next().unwrap();
484        assert_eq!(
485            document.node(outer_i_text).kind,
486            NodeKind::Text {
487                content: "4".to_owned()
488            }
489        );
490
491        // The trailing "5" lands as p's own child, not inside the
492        // reconstructed i - </i> closes before it arrives.
493        assert_eq!(
494            document.node(p_children[3]).kind,
495            NodeKind::Text {
496                content: "5".to_owned()
497            }
498        );
499    }
500
501    #[test]
502    fn adoption_agency_spec_example_indirectly_nests_two_a_elements_via_table_misnesting() {
503        // The spec's own example, quoted directly in §13.2.6.4.7's <a>
504        // start-tag rule: "In the non-conforming stream <a href="a">
505        // a<table><a href="b">b</table>x, the first a element would be
506        // closed upon seeing the second one, and the "x" character
507        // would be inside a link to "b", not to "a" [...] The result is
508        // that the two a elements are indirectly nested inside each
509        // other." Not covered by html-conform's own test matrix.
510        let document = parse(r#"<a href="a">a<table><a href="b">b</table>x"#).document;
511        let body = body_of(&document);
512        let body_children: Vec<_> = document.children(body).collect();
513        assert_eq!(body_children.len(), 2);
514
515        let a1 = body_children[0];
516        let a1_children: Vec<_> = document.children(a1).collect();
517        assert_eq!(a1_children.len(), 3);
518        assert_eq!(
519            document.node(a1_children[0]).kind,
520            NodeKind::Text {
521                content: "a".to_owned()
522            }
523        );
524        let a2 = a1_children[1];
525        let NodeKind::Element { name, .. } = &document.node(a2).kind else {
526            unreachable!()
527        };
528        assert_eq!(name, "a");
529        let a2_text = document.children(a2).next().unwrap();
530        assert_eq!(
531            document.node(a2_text).kind,
532            NodeKind::Text {
533                content: "b".to_owned()
534            }
535        );
536        let table = a1_children[2];
537        let NodeKind::Element { name, .. } = &document.node(table).kind else {
538            unreachable!()
539        };
540        assert_eq!(name, "table");
541        assert_eq!(document.children(table).count(), 0);
542
543        // The final "x" lands in a *fresh* a element (href="b",
544        // reconstructed from the active formatting elements list),
545        // a sibling of a1 - not a1 itself.
546        let a3 = body_children[1];
547        let NodeKind::Element { name, .. } = &document.node(a3).kind else {
548            unreachable!()
549        };
550        assert_eq!(name, "a");
551        let a3_text = document.children(a3).next().unwrap();
552        assert_eq!(
553            document.node(a3_text).kind,
554            NodeKind::Text {
555                content: "x".to_owned()
556            }
557        );
558    }
559
560    #[test]
561    fn quirks_mode_changes_whether_table_closes_an_open_p_element() {
562        // Spec-derived case, not covered by html-conform's own test
563        // matrix (which drops DOCTYPE/quirks-mode entirely) - the one
564        // place quirks mode actually shapes the produced tree
565        // (§13.2.6.4.7's <table> start-tag rule).
566        let no_quirks = parse("<!DOCTYPE html><p><table></table>").document;
567        let body = body_of(&no_quirks);
568        let children: Vec<_> = no_quirks.children(body).collect();
569        assert_eq!(children.len(), 2);
570        let NodeKind::Element { name, .. } = &no_quirks.node(children[0]).kind else {
571            unreachable!()
572        };
573        assert_eq!(name, "p");
574        assert_eq!(no_quirks.children(children[0]).count(), 0);
575        let NodeKind::Element { name, .. } = &no_quirks.node(children[1]).kind else {
576            unreachable!()
577        };
578        assert_eq!(name, "table");
579
580        let quirks = parse("<p><table></table>").document; // no DOCTYPE at all -> quirks mode
581        let body = body_of(&quirks);
582        let children: Vec<_> = quirks.children(body).collect();
583        assert_eq!(children.len(), 1);
584        let NodeKind::Element { name, .. } = &quirks.node(children[0]).kind else {
585            unreachable!()
586        };
587        assert_eq!(name, "p");
588        let p_children: Vec<_> = quirks.children(children[0]).collect();
589        assert_eq!(p_children.len(), 1);
590        let NodeKind::Element { name, .. } = &quirks.node(p_children[0]).kind else {
591            unreachable!()
592        };
593        assert_eq!(name, "table");
594    }
595
596    // The next two tests pin the minimal reproductions of two infinite
597    // loops the html5lib-tests conformance corpus (tests/html5lib_conformance.rs)
598    // surfaced — both fixed in `tree_builder.rs`. Neither asserts much
599    // about the resulting tree shape; the property under test is that
600    // `parse` returns at all (a regression would hang this test rather
601    // than fail it cleanly, same as it hung `cargo test` before the fix).
602
603    #[test]
604    fn end_tag_that_walks_out_of_foreign_content_does_not_loop_forever() {
605        // `</a>` while the current node is `<svg>` (no HTML-special
606        // element between them) sends foreign content's "any other end
607        // tag" rule (§13.2.6.5) all the way up to the first HTML-namespace
608        // ancestor without ever popping anything itself — it must then
609        // hand off to that insertion mode's own HTML-content rules
610        // directly, not via `TokenOutcome::Reprocess` (which re-checks
611        // foreign-content dispatch against the very node whose
612        // foreign-namespace-ness never changed, looping forever).
613        let document = parse("<a><svg></a>").document;
614        let body = body_of(&document);
615        assert_eq!(document.children(body).count(), 1);
616    }
617
618    #[test]
619    fn template_end_tag_resets_a_stale_insertion_mode() {
620        // `<template>` inside `<thead>` implicitly opens a `<tr><td>`
621        // (this crate treats `<template>` as a plain element — no
622        // template insertion-modes stack, see README.md's "Known
623        // limitations" — so the insertion mode active for that implicit
624        // `<td>`, `InCell`, is never restored). `</template>` then pops
625        // `<td>`/`<tr>`/`<template>` off the stack without resetting the
626        // insertion mode; the next token (`</table>`) processed under
627        // the still-`InCell` mode assumes a `td`/`th` remains on the
628        // stack, which no longer holds — `close_the_cell` would then
629        // pop the now-empty stack forever looking for one.
630        let document = parse("<table><thead><template><td></template></table>").document;
631        let body = body_of(&document);
632        assert_eq!(document.children(body).count(), 1);
633    }
634
635    #[test]
636    fn frameset_document_replaces_body_and_ignores_stray_text() {
637        // html5lib-tests' tests2.dat#5: a bare `<frameset>` after the
638        // DOCTYPE, with trailing character data "in frameset" mode's
639        // "anything else" rule drops entirely (no `<body>` at all in
640        // the result — frameset and body are mutually exclusive).
641        let document = parse("<!DOCTYPE html><frameset>test").document;
642
643        let root = document.root();
644        let root_children: Vec<_> = document.children(root).collect();
645        assert_eq!(root_children.len(), 2);
646        assert_eq!(
647            document.node(root_children[0]).kind,
648            NodeKind::Doctype {
649                name: Some("html".to_owned()),
650                public_identifier: Some(String::new()),
651                system_identifier: Some(String::new()),
652            }
653        );
654        let html = root_children[1];
655
656        let html_children: Vec<_> = document.children(html).collect();
657        assert_eq!(html_children.len(), 2);
658        let NodeKind::Element { name, .. } = &document.node(html_children[0]).kind else {
659            unreachable!()
660        };
661        assert_eq!(name, "head");
662        let frameset = html_children[1];
663        let NodeKind::Element { name, .. } = &document.node(frameset).kind else {
664            unreachable!()
665        };
666        assert_eq!(name, "frameset");
667        assert_eq!(document.children(frameset).count(), 0);
668    }
669
670    #[test]
671    fn template_content_is_a_separate_fragment_from_the_template_element() {
672        // html5lib-tests' template.dat#0: `<template>`'s real content
673        // model (§13.2.6.4.4/.16) — its child is a `DocumentFragment`
674        // ("template contents"), not the text directly.
675        let document = parse("<body><template>Hello</template>").document;
676        let body = body_of(&document);
677
678        let template = document.children(body).next().unwrap();
679        let NodeKind::Element { name, .. } = &document.node(template).kind else {
680            unreachable!()
681        };
682        assert_eq!(name, "template");
683
684        let template_children: Vec<_> = document.children(template).collect();
685        assert_eq!(template_children.len(), 1);
686        let content = template_children[0];
687        assert_eq!(document.node(content).kind, NodeKind::DocumentFragment);
688
689        let content_children: Vec<_> = document.children(content).collect();
690        assert_eq!(content_children.len(), 1);
691        assert_eq!(
692            document.node(content_children[0]).kind,
693            NodeKind::Text {
694                content: "Hello".to_owned()
695            }
696        );
697    }
698
699    #[test]
700    fn selected_option_content_is_mirrored_into_selectedcontent() {
701        // html5lib-tests' webkit02.dat#47: the explicitly `selected`
702        // option (not the first one) is the one mirrored, and — since
703        // it's the last token in the input — only `stop_parsing`'s
704        // final pop of the still-open `<option>` makes that observable.
705        let document =
706            parse("<select><button><selectedcontent></button><option>X<option selected>Y").document;
707        let body = body_of(&document);
708
709        let select = document.children(body).next().unwrap();
710        let button = document.children(select).next().unwrap();
711        let selectedcontent = document.children(button).next().unwrap();
712        let selectedcontent_children: Vec<_> = document.children(selectedcontent).collect();
713        assert_eq!(selectedcontent_children.len(), 1);
714        assert_eq!(
715            document.node(selectedcontent_children[0]).kind,
716            NodeKind::Text {
717                content: "Y".to_owned()
718            }
719        );
720    }
721}