diff --git a/html5ever/src/tree_builder/mod.rs b/html5ever/src/tree_builder/mod.rs index 803a398e..533c21bb 100644 --- a/html5ever/src/tree_builder/mod.rs +++ b/html5ever/src/tree_builder/mod.rs @@ -413,6 +413,41 @@ where self.context_elem.borrow().is_some() } + /// Call `f` with the [stack of open elements]. + /// + /// The first element is the topmost node of the stack, the root `html` element, and the last + /// one is the bottommost node, the [current node]. The stack is empty before the root element + /// has been inserted and after [`end`](TokenSink::end) has been called. When parsing a + /// fragment, the context element is not on the stack. + /// + /// # Panics + /// + /// The stack is borrowed while `f` runs, so processing a token with this tree builder from + /// within `f` may panic. + /// + /// [stack of open elements]: https://html.spec.whatwg.org/#stack-of-open-elements + /// [current node]: https://html.spec.whatwg.org/#current-node + pub fn with_open_elements(&self, f: F) -> R + where + F: FnOnce(&[Handle]) -> R, + { + f(&self.open_elems.borrow()) + } + + /// Is the [insertion mode] ["text"]? + /// + /// The tree builder switches to this insertion mode after inserting an element whose contents + /// are parsed as raw text or RCDATA, such as `script`, `style`, `title` or `textarea`, and + /// switches back at the element's end tag or at the end of the input. In this insertion mode, + /// character tokens are inserted into the [current node], which is that element. + /// + /// [insertion mode]: https://html.spec.whatwg.org/#insertion-mode + /// ["text"]: https://html.spec.whatwg.org/#parsing-main-incdata + /// [current node]: https://html.spec.whatwg.org/#current-node + pub fn is_in_text_insertion_mode(&self) -> bool { + self.mode.get() == InsertionMode::Text + } + /// https://html.spec.whatwg.org/multipage/#appropriate-place-for-inserting-a-node fn appropriate_place_for_insertion( &self, diff --git a/rcdom/tests/html-tree-builder-state.rs b/rcdom/tests/html-tree-builder-state.rs new file mode 100644 index 00000000..c0b4cfdf --- /dev/null +++ b/rcdom/tests/html-tree-builder-state.rs @@ -0,0 +1,170 @@ +// Copyright 2014-2017 The html5ever Project Developers. See the +// COPYRIGHT file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use html5ever::driver::{self, ParseOpts}; +use html5ever::tendril::TendrilSink; +use html5ever::tree_builder::{TreeBuilder, TreeBuilderOpts}; +use html5ever::{local_name, ns, QualName}; +use markup5ever_rcdom::{Handle, NodeData, RcDom}; + +/// The names of the open elements, written like in the html5lib tree construction tests. +fn open_elements(tree_builder: &TreeBuilder) -> Vec { + tree_builder.with_open_elements(|elements| { + elements + .iter() + .map(|element| { + let NodeData::Element { ref name, .. } = element.data else { + panic!("not an element"); + }; + match name.ns { + ns!(html) => name.local.to_string(), + ns!(svg) => format!("svg {}", name.local), + ns!(mathml) => format!("math {}", name.local), + _ => panic!("unexpected namespace"), + } + }) + .collect() + }) +} + +/// The text of the children of the current node. +fn current_node_text(tree_builder: &TreeBuilder) -> String { + tree_builder.with_open_elements(|elements| { + let current_node = elements.last().expect("no current node"); + let mut text = String::new(); + for child in current_node.children.borrow().iter() { + if let NodeData::Text { ref contents } = child.data { + text.push_str(&contents.borrow()); + } + } + text + }) +} + +#[test] +fn open_elements_in_document() { + let mut parser = driver::parse_document(RcDom::default(), ParseOpts::default()); + parser.process("".into()); + assert!(open_elements(&parser.tokenizer.sink).is_empty()); + + parser.process("
".into()); + assert_eq!( + open_elements(&parser.tokenizer.sink), + ["html", "body", "table", "tbody", "tr", "td", "b"] + ); + + parser.process("
".into()); + assert_eq!( + open_elements(&parser.tokenizer.sink), + ["html", "body", "svg svg", "svg circle"] + ); + + parser.tokenizer.end(); + assert!(open_elements(&parser.tokenizer.sink).is_empty()); +} + +#[test] +fn open_elements_in_fragment() { + let context = QualName::new(None, ns!(html), local_name!("ul")); + let mut parser = driver::parse_fragment( + RcDom::default(), + ParseOpts::default(), + context, + vec![], + true, + ); + assert_eq!(open_elements(&parser.tokenizer.sink), ["html"]); + + parser.process("
  • ".into()); + assert_eq!(open_elements(&parser.tokenizer.sink), ["html", "li", "p"]); +} + +#[test] +fn text_insertion_mode() { + for name in [ + "iframe", "noembed", "noframes", "noscript", "script", "style", "textarea", "title", "xmp", + ] { + let mut parser = driver::parse_document(RcDom::default(), ParseOpts::default()); + parser.process("".into()); + assert!(!parser.tokenizer.sink.is_in_text_insertion_mode()); + + parser.process(format!("<{name}>").into()); + assert!(parser.tokenizer.sink.is_in_text_insertion_mode(), "{name}"); + assert_eq!( + open_elements(&parser.tokenizer.sink), + ["html", "body", name] + ); + + // Character tokens are inserted into the current node, which is that element. + parser.process("

    ".into()); + assert!(parser.tokenizer.sink.is_in_text_insertion_mode(), "{name}"); + assert_eq!(current_node_text(&parser.tokenizer.sink), "

    ", "{name}"); + + parser.process(format!("").into()); + assert!(!parser.tokenizer.sink.is_in_text_insertion_mode(), "{name}"); + assert_eq!(open_elements(&parser.tokenizer.sink), ["html", "body"]); + } +} + +#[test] +fn text_insertion_mode_ends_at_eof() { + let mut parser = driver::parse_document(RcDom::default(), ParseOpts::default()); + parser.process("