diff --git a/html5ever/src/tree_builder/mod.rs b/html5ever/src/tree_builder/mod.rs index 803a398e..9b4b3116 100644 --- a/html5ever/src/tree_builder/mod.rs +++ b/html5ever/src/tree_builder/mod.rs @@ -14,6 +14,7 @@ pub use crate::interface::{AppendNode, AppendText, Attribute, NodeOrText}; pub use crate::interface::{LimitedQuirks, NoQuirks, Quirks, QuirksMode}; pub use markup5ever::interface::tree_builder::create_element_with_flags; +use self::open_elems::{OpenElems, TrackedSet}; use self::types::*; use crate::tendril::StrTendril; @@ -39,6 +40,7 @@ use markup5ever::{expanded_name, local_name, namespace_prefix, ns}; mod tag_sets; mod data; +mod open_elems; mod rules; mod types; @@ -108,7 +110,7 @@ pub struct TreeBuilder { doc_handle: Handle, /// Stack of open elements, most recently added at end. - open_elems: RefCell>, + open_elems: RefCell>, /// List of active formatting elements. active_formatting: RefCell>>, @@ -268,7 +270,7 @@ where /// internal state. This is intended to support garbage-collected DOMs. pub fn trace_handles(&self, tracer: &dyn Tracer) { tracer.trace_handle(&self.doc_handle); - for e in &*self.open_elems.borrow() { + for e in self.open_elems.borrow().iter() { tracer.trace_handle(e); } @@ -544,7 +546,8 @@ where } fn end(&self) { - for elem in self.open_elems.borrow_mut().drain(..).rev() { + let mut open_elems = self.open_elems.borrow_mut(); + while let Some(elem) = open_elems.pop() { self.sink.pop(&elem); } } @@ -741,12 +744,7 @@ where }); }; - let Some(fmt_elem_stack_index) = self - .open_elems - .borrow() - .iter() - .rposition(|n| self.sink.same_node(n, &fmt_elem)) - else { + let Some(fmt_elem_stack_index) = self.position_in_open_elems(&fmt_elem) else { self.sink .parse_error(Borrowed("Formatting element not open")); self.active_formatting.borrow_mut().remove(fmt_elem_index); @@ -754,7 +752,11 @@ where }; // 7. - if !self.in_scope(default_scope, |n| self.sink.same_node(&n, &fmt_elem)) { + if !self + .open_elems + .borrow() + .in_scope(TrackedSet::DefaultScope, fmt_elem_stack_index) + { self.sink .parse_error(Borrowed("Formatting element not in scope")); return; @@ -776,7 +778,7 @@ where .find(|&(_, open_element)| self.elem_in(open_element, special_tag)) .map(|(i, h)| (i, h.clone())); - let Some((furthest_block_index, furthest_block)) = maybe_furthest_block else { + let Some((mut furthest_block_index, furthest_block)) = maybe_furthest_block else { // 10. self.open_elems.borrow_mut().truncate(fmt_elem_stack_index); self.active_formatting.borrow_mut().remove(fmt_elem_index); @@ -814,12 +816,14 @@ where self.position_in_active_formatting(&node) .map(|position| self.active_formatting.borrow_mut().remove(position)); self.open_elems.borrow_mut().remove(node_index); + furthest_block_index -= 1; continue; } let Some(node_formatting_index) = self.position_in_active_formatting(&node) else { // 13.6. self.open_elems.borrow_mut().remove(node_index); + furthest_block_index -= 1; continue; }; @@ -839,7 +843,11 @@ where tag.attrs.clone(), tag.had_duplicate_attributes, ); - self.open_elems.borrow_mut()[node_index] = new_element.clone(); + self.open_elems.borrow_mut().replace( + node_index, + new_element.clone(), + self.sink.elem_name(&new_element).expanded(), + ); self.active_formatting.borrow_mut()[node_formatting_index] = FormatEntry::Element(new_element.clone(), tag); node = new_element; @@ -905,23 +913,28 @@ where } // 19. - self.remove_from_stack(&fmt_elem); - let new_furthest_block_index = self - .open_elems - .borrow() - .iter() - .position(|n| self.sink.same_node(n, &furthest_block)) - .expect("furthest block missing from open element stack"); - self.open_elems - .borrow_mut() - .insert(new_furthest_block_index + 1, new_element); + // Move the formatting element up to just above the furthest block, which moves the + // elements in between down by one, and put the new element in its place. + { + let mut open_elems = self.open_elems.borrow_mut(); + open_elems.move_up(fmt_elem_stack_index, furthest_block_index); + open_elems.replace( + furthest_block_index, + new_element.clone(), + self.sink.elem_name(&new_element).expanded(), + ); + } + self.sink.pop(&fmt_elem); // 20. } } fn push(&self, elem: &Handle) { - self.open_elems.borrow_mut().push(elem.clone()); + let name = self.sink.elem_name(elem); + self.open_elems + .borrow_mut() + .push(elem.clone(), name.expanded()); } fn pop(&self) -> Handle { @@ -935,12 +948,24 @@ where elem } - fn remove_from_stack(&self, elem: &Handle) { - let position = self - .open_elems - .borrow() + /// The position of `elem` in the stack of open elements, if it is open. + fn position_in_open_elems(&self, elem: &Handle) -> Option { + let name = self.sink.elem_name(elem); + let open_elems = self.open_elems.borrow(); + let positions = if *name.ns() == ns!(html) { + open_elems.positions_named(name.local_name()) + } else { + open_elems.positions_foreign_named(name.local_name()) + }; + let position = positions .iter() - .rposition(|x| self.sink.same_node(elem, x)); + .rev() + .find(|&position| self.sink.same_node(&open_elems[position], elem)); + position + } + + fn remove_from_stack(&self, elem: &Handle) { + let position = self.position_in_open_elems(elem); if let Some(position) = position { self.open_elems.borrow_mut().remove(position); self.sink.pop(elem); @@ -950,12 +975,7 @@ where fn is_marker_or_open(&self, entry: &FormatEntry) -> bool { match *entry { FormatEntry::Marker => true, - FormatEntry::Element(ref node, _) => self - .open_elems - .borrow() - .iter() - .rev() - .any(|n| self.sink.same_node(n, node)), + FormatEntry::Element(ref node, _) => self.position_in_open_elems(node).is_some(), } } @@ -1056,48 +1076,22 @@ where /// Signal an error depending on the state of the stack of open elements at /// the end of the body. fn check_body_end(&self) { - declare_tag_set!(body_end_ok = - "dd" "dt" "li" "optgroup" "option" "p" "rp" "rt" "tbody" "td" "tfoot" "th" - "thead" "tr" "body" "html"); - - for elem in self.open_elems.borrow().iter() { - let error = { - let elem_name = self.sink.elem_name(elem); - let name = elem_name.expanded(); - if body_end_ok(name) { - continue; - } - - if self.opts.exact_errors { - Cow::from(format!("Unexpected open tag {name:?} at end of body")) - } else { - Cow::from("Unexpected open tag at end of body") - } - }; - self.sink.parse_error(error); - // FIXME: Do we keep checking after finding one bad tag? - // The spec suggests not. + let open_elems = self.open_elems.borrow(); + // FIXME: Do we keep checking after finding one bad tag? + // The spec suggests not. + let Some(position) = open_elems.positions_in(TrackedSet::NotBodyEndOk).first() else { return; - } - } - - fn in_scope(&self, scope: TagSet, pred: Pred) -> bool - where - TagSet: Fn(ExpandedName) -> bool, - Pred: Fn(Handle) -> bool, - { - for node in self.open_elems.borrow().iter().rev() { - if pred(node.clone()) { - return true; - } - if scope(self.sink.elem_name(node).expanded()) { - return false; + }; + let error = { + let elem_name = self.sink.elem_name(&open_elems[position]); + let name = elem_name.expanded(); + if self.opts.exact_errors { + Cow::from(format!("Unexpected open tag {name:?} at end of body")) + } else { + Cow::from("Unexpected open tag at end of body") } - } - - // supposed to be impossible, because is always in scope - - false + }; + self.sink.parse_error(error); } fn elem_in(&self, elem: &Handle, set: TagSet) -> bool @@ -1113,21 +1107,22 @@ where } fn in_html_elem_named(&self, name: LocalName) -> bool { - self.open_elems - .borrow() - .iter() - .any(|elem| self.html_elem_named(elem, name.clone())) + !self.open_elems.borrow().positions_named(&name).is_empty() } fn current_node_named(&self, name: LocalName) -> bool { self.html_elem_named(&self.current_node(), name) } - fn in_scope_named(&self, scope: TagSet, name: LocalName) -> bool - where - TagSet: Fn(ExpandedName) -> bool, - { - self.in_scope(scope, |elem| self.html_elem_named(&elem, name.clone())) + fn in_scope_named(&self, scope: TrackedSet, name: LocalName) -> bool { + self.open_elems.borrow().named_in_scope(scope, &name) + } + + fn in_scope_any_named(&self, scope: TrackedSet, names: &[LocalName]) -> bool { + let open_elems = self.open_elems.borrow(); + names + .iter() + .any(|name| open_elems.named_in_scope(scope, name)) } /// @@ -1216,7 +1211,7 @@ where } fn close_p_element_in_button_scope(&self) { - if self.in_scope_named(button_scope, local_name!("p")) { + if self.in_scope_named(TrackedSet::ButtonScope, local_name!("p")) { self.close_p_element(); } } @@ -1264,7 +1259,15 @@ where // https://html.spec.whatwg.org/multipage/#reset-the-insertion-mode-appropriately fn reset_insertion_mode(&self) -> InsertionMode { let open_elems = self.open_elems.borrow(); - for (i, mut node) in open_elems.iter().enumerate().rev() { + // The steps below stop at the topmost element in this set, unless it is the first one, + // which they look at with the context element in its place. + let topmost = open_elems + .positions_in(TrackedSet::ResetInsertionMode) + .last() + .filter(|&i| i > 0); + let first = (!open_elems.is_empty()).then_some(0); + for i in topmost.into_iter().chain(first) { + let mut node = &open_elems[i]; let last = i == 0usize; let context_elem = self.context_elem.borrow(); if let (true, Some(ctx)) = (last, context_elem.as_ref()) { @@ -1553,20 +1556,20 @@ where } fn process_end_tag_in_body(&self, tag: Tag) { - // Look back for a matching open element. - let mut match_idx = None; - for (i, elem) in self.open_elems.borrow().iter().enumerate().rev() { - if self.html_elem_named(elem, tag.name.clone()) { - match_idx = Some(i); - break; - } - - if self.elem_in(elem, special_tag) { + // Look back for a matching open element, as far as the topmost special element. + let match_idx = { + let open_elems = self.open_elems.borrow(); + let topmost_match = open_elems.positions_named(&tag.name).last(); + if topmost_match.is_some_and(|i| open_elems.in_scope(TrackedSet::Special, i)) { + topmost_match + } else if !open_elems.positions_in(TrackedSet::Special).is_empty() { self.sink .parse_error(Borrowed("Found special tag while closing generic tag")); return; + } else { + None } - } + }; let Some(match_idx) = match_idx else { // I believe this is impossible, because the root diff --git a/html5ever/src/tree_builder/open_elems.rs b/html5ever/src/tree_builder/open_elems.rs new file mode 100644 index 00000000..8ce2b4c1 --- /dev/null +++ b/html5ever/src/tree_builder/open_elems.rs @@ -0,0 +1,501 @@ +// 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. + +//! The stack of open elements. + +use std::collections::HashMap; +use std::hash::{BuildHasherDefault, Hasher}; +use std::ops::Deref; + +use crate::tree_builder::tag_sets::*; +use crate::{ExpandedName, LocalName}; +use markup5ever::ns; + +/// Sets of elements that the tree builder looks for in the stack of open elements. +/// +/// The stack keeps the positions of the open elements in each of these sets, so that finding +/// the topmost one doesn't need a walk down the stack. +#[derive(Clone, Copy, Debug)] +pub(crate) enum TrackedSet { + /// + DefaultScope, + /// + ListItemScope, + /// + ButtonScope, + /// + TableScope, + /// The special elements, where the "any other end tag" steps in body stop. + Special, + /// The special elements other than address, div and p, where the steps for a li, dd or dt + /// start tag in body stop. + SpecialExceptAddressDivP, + /// The elements where "reset the insertion mode appropriately" stops. + ResetInsertionMode, + /// The elements that are a parse error when they are still open at the end of the body. + NotBodyEndOk, + /// HTML elements, where the steps for an end tag in foreign content stop. + Html, +} + +const TRACKED_SETS: [TrackedSet; 9] = [ + TrackedSet::DefaultScope, + TrackedSet::ListItemScope, + TrackedSet::ButtonScope, + TrackedSet::TableScope, + TrackedSet::Special, + TrackedSet::SpecialExceptAddressDivP, + TrackedSet::ResetInsertionMode, + TrackedSet::NotBodyEndOk, + TrackedSet::Html, +]; + +impl TrackedSet { + fn contains(self, name: ExpandedName) -> bool { + match self { + TrackedSet::DefaultScope => default_scope(name), + TrackedSet::ListItemScope => list_item_scope(name), + TrackedSet::ButtonScope => button_scope(name), + TrackedSet::TableScope => table_scope(name), + TrackedSet::Special => special_tag(name), + TrackedSet::SpecialExceptAddressDivP => special_except_address_div_p(name), + TrackedSet::ResetInsertionMode => reset_insertion_mode_stop(name), + TrackedSet::NotBodyEndOk => !body_end_ok(name), + TrackedSet::Html => *name.ns == ns!(html), + } + } +} + +/// Hashes a `LocalName` by the hash that string_cache keeps for each atom, mixed so that short +/// atoms, whose hash is their inline representation, spread over the table too. +#[derive(Default)] +struct AtomHasher(u64); + +impl Hasher for AtomHasher { + fn write(&mut self, bytes: &[u8]) { + for &byte in bytes { + self.write_u64(u64::from(byte)); + } + } + + fn write_u64(&mut self, n: u64) { + self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(0x9e37_79b9_7f4a_7c15); + } + + fn finish(&self) -> u64 { + self.0 ^ (self.0 >> 32) + } +} + +type AtomMap = HashMap>; + +/// What the stack knows about one of its elements. +#[derive(Clone, Copy, PartialEq)] +struct Entry { + /// Index into `OpenElems::positions_by_name`. + name: u32, + /// The tracked sets the element is in, as a bit set indexed by `TrackedSet as usize`. + sets: u16, +} + +/// The stack of open elements, most recently added at the end. +/// +/// Besides the elements themselves, it keeps the stack positions of the open elements with each +/// name and of those in each [`TrackedSet`], lowest first. "Has an element in scope" then compares +/// the topmost element with that name to the topmost element that bounds the scope, instead of +/// walking down the stack, so that the cost of a token doesn't grow with the nesting depth. +/// +/// Pushing and popping update these positions in constant time. Removing an element below the top +/// re-indexes the elements above it, and moving one up re-indexes the elements it moves past. +pub(crate) struct OpenElems { + elems: Vec, + entries: Vec, + /// The entries of HTML elements, by name. + html_names: AtomMap, + /// Indices into `positions_by_name` for the names of foreign elements, lowercased, because + /// end tags in foreign content match them ASCII case-insensitively. + foreign_names: AtomMap, + /// Positions are stored as `u32` to halve the memory per open element; `push` refuses a + /// stack deeper than `u32::MAX`. + positions_by_name: Vec>, + positions_by_set: [Vec; TRACKED_SETS.len()], +} + +/// A list of stack positions, lowest first. +#[derive(Clone, Copy)] +pub(crate) struct Positions<'a>(&'a [u32]); + +impl<'a> Positions<'a> { + pub(crate) fn first(self) -> Option { + self.0.first().map(|&p| p as usize) + } + + pub(crate) fn last(self) -> Option { + self.0.last().map(|&p| p as usize) + } + + pub(crate) fn is_empty(self) -> bool { + self.0.is_empty() + } + + pub(crate) fn iter(self) -> impl DoubleEndedIterator + 'a { + self.0.iter().map(|&p| p as usize) + } +} + +impl Default for OpenElems { + fn default() -> Self { + OpenElems { + elems: vec![], + entries: vec![], + html_names: AtomMap::default(), + foreign_names: AtomMap::default(), + positions_by_name: vec![], + positions_by_set: Default::default(), + } + } +} + +impl Deref for OpenElems { + type Target = [Handle]; + + fn deref(&self) -> &[Handle] { + &self.elems + } +} + +impl OpenElems { + pub(crate) fn push(&mut self, elem: Handle, name: ExpandedName) { + let entry = self.entry_for(name); + assert!( + self.elems.len() < u32::MAX as usize, + "stack of open elements too deep" + ); + self.elems.push(elem); + self.entries.push(entry); + self.index_from(self.entries.len() - 1); + } + + pub(crate) fn pop(&mut self) -> Option { + let elem = self.elems.pop()?; + self.unindex_from(self.entries.len() - 1); + self.entries.pop(); + Some(elem) + } + + pub(crate) fn truncate(&mut self, len: usize) { + while self.elems.len() > len { + self.pop(); + } + } + + pub(crate) fn remove(&mut self, index: usize) -> Handle { + self.unindex_from(index); + self.entries.remove(index); + let elem = self.elems.remove(index); + self.index_from(index); + elem + } + + /// Move the element at `from` up to `to`, which moves the elements above it, up to and + /// including `to`, down by one. + pub(crate) fn move_up(&mut self, from: usize, to: usize) { + // Only the lists with a position from `from` to `to` change, and only there. + let moved = self.entries[from]; + let mut names: Vec = self.entries[from..=to].iter().map(|e| e.name).collect(); + names.sort_unstable(); + names.dedup(); + for name in names { + let positions = &mut self.positions_by_name[name as usize]; + move_position_up(positions, from, to, name == moved.name); + } + let sets = self.entries[from..=to] + .iter() + .fold(0, |sets, e| sets | e.sets); + for set in TRACKED_SETS { + let bit = 1 << set as u16; + if sets & bit != 0 { + let positions = &mut self.positions_by_set[set as usize]; + move_position_up(positions, from, to, moved.sets & bit != 0); + } + } + self.entries[from..=to].rotate_left(1); + self.elems[from..=to].rotate_left(1); + } + + pub(crate) fn replace(&mut self, index: usize, elem: Handle, name: ExpandedName) { + let entry = self.entry_for(name); + // The adoption agency algorithm replaces an element with a new one with the same name, + // which leaves the positions as they are. + if entry != self.entries[index] { + self.unindex_from(index); + self.entries[index] = entry; + self.index_from(index); + } + self.elems[index] = elem; + } + + /// The stack positions of the open HTML elements named `name`, lowest first. + pub(crate) fn positions_named(&self, name: &LocalName) -> Positions<'_> { + Positions(match self.html_names.get(name) { + Some(entry) => &self.positions_by_name[entry.name as usize], + None => &[], + }) + } + + /// The stack positions of the open foreign elements whose name is an ASCII case-insensitive + /// match for `name`, lowest first. + pub(crate) fn positions_foreign_named(&self, name: &LocalName) -> Positions<'_> { + Positions(match self.foreign_names.get(&name.to_ascii_lowercase()) { + Some(&id) => &self.positions_by_name[id as usize], + None => &[], + }) + } + + /// The stack positions of the open elements in `set`, lowest first. + pub(crate) fn positions_in(&self, set: TrackedSet) -> Positions<'_> { + Positions(&self.positions_by_set[set as usize]) + } + + /// Is the element at `position` in the scope that `scope` bounds? It is unless an element + /// in `scope` is above it. + pub(crate) fn in_scope(&self, scope: TrackedSet, position: usize) -> bool { + self.positions_in(scope) + .last() + .is_none_or(|boundary| position >= boundary) + } + + /// Is an HTML element named `name` in the scope that `scope` bounds? + pub(crate) fn named_in_scope(&self, scope: TrackedSet, name: &LocalName) -> bool { + self.positions_named(name) + .last() + .is_some_and(|position| self.in_scope(scope, position)) + } + + fn entry_for(&mut self, name: ExpandedName) -> Entry { + if *name.ns == ns!(html) { + // The sets an HTML element is in depend only on its local name, so they are worked + // out once for each name. + if let Some(&entry) = self.html_names.get(name.local) { + return entry; + } + let entry = Entry { + name: self.new_name(), + sets: sets_of(name), + }; + self.html_names.insert(name.local.clone(), entry); + entry + } else { + let key = name.local.to_ascii_lowercase(); + let id = match self.foreign_names.get(&key) { + Some(&id) => id, + None => { + let id = self.new_name(); + self.foreign_names.insert(key, id); + id + }, + }; + Entry { + name: id, + sets: sets_of(name), + } + } + } + + fn new_name(&mut self) -> u32 { + // There are never more names than elements pushed, which `push` keeps below `u32::MAX`. + self.positions_by_name.push(vec![]); + (self.positions_by_name.len() - 1) as u32 + } + + /// Call `f` on each list of positions that an element with this entry is in. + fn for_each_list(&mut self, entry: Entry, mut f: impl FnMut(&mut Vec)) { + f(&mut self.positions_by_name[entry.name as usize]); + for set in TRACKED_SETS { + if entry.sets & (1 << set as u16) != 0 { + f(&mut self.positions_by_set[set as usize]); + } + } + } + + /// Record the positions of the elements from `start` to the top. + fn index_from(&mut self, start: usize) { + for position in start..self.entries.len() { + self.for_each_list(self.entries[position], |positions| { + positions.push(position as u32) + }); + } + } + + /// Forget the positions of the elements from `start` to the top. They are the highest + /// recorded positions, so each is the last in its list. + fn unindex_from(&mut self, start: usize) { + for position in (start..self.entries.len()).rev() { + self.for_each_list(self.entries[position], |positions| { + positions.pop(); + }); + } + } +} + +/// Update a list of positions for `OpenElems::move_up`: the positions above `from`, up to and +/// including `to`, move down by one, and if the list has the moved element, `from` becomes `to`. +fn move_position_up(positions: &mut [u32], from: usize, to: usize, has_moved: bool) { + let (from, to) = (from as u32, to as u32); + let start = positions.partition_point(|&position| position < from); + let end = positions.partition_point(|&position| position <= to); + let mut changed = &mut positions[start..end]; + if has_moved { + changed.rotate_left(1); + let (moved, others) = changed.split_last_mut().unwrap(); + *moved = to; + changed = others; + } + for position in changed { + *position -= 1; + } +} + +fn sets_of(name: ExpandedName) -> u16 { + let mut sets = 0; + for set in TRACKED_SETS { + if set.contains(name) { + sets |= 1 << set as u16; + } + } + sets +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{local_name, Namespace, QualName}; + + const NAMES: [(Namespace, &str); 14] = [ + (ns!(html), "html"), + (ns!(html), "body"), + (ns!(html), "div"), + (ns!(html), "p"), + (ns!(html), "li"), + (ns!(html), "ul"), + (ns!(html), "button"), + (ns!(html), "table"), + (ns!(html), "b"), + (ns!(svg), "foreignObject"), + (ns!(svg), "g"), + (ns!(mathml), "mi"), + (ns!(mathml), "g"), + (ns!(mathml), "foreignobject"), + ]; + + fn qual_name(i: usize) -> QualName { + let (ns, local) = NAMES[i].clone(); + QualName::new(None, ns, LocalName::from(local)) + } + + /// Check every recorded position against a walk down the stack. + fn check(stack: &OpenElems) { + let names: Vec = stack.iter().map(|&i| qual_name(i)).collect(); + for set in TRACKED_SETS { + let expected: Vec = (0..names.len()) + .filter(|&i| set.contains(names[i].expanded())) + .collect(); + assert_eq!( + stack.positions_in(set).iter().collect::>(), + expected, + "{set:?}" + ); + } + for (_, local) in NAMES { + let local = LocalName::from(local); + let html: Vec = (0..names.len()) + .filter(|&i| names[i].ns == ns!(html) && names[i].local == local) + .collect(); + assert_eq!( + stack.positions_named(&local).iter().collect::>(), + html, + "{local}" + ); + // Foreign elements match whatever their namespace, and ignoring ASCII case. + let foreign: Vec = (0..names.len()) + .filter(|&i| { + names[i].ns != ns!(html) && names[i].local.eq_ignore_ascii_case(&local) + }) + .collect(); + assert_eq!( + stack + .positions_foreign_named(&local) + .iter() + .collect::>(), + foreign, + "{local}" + ); + let uppercase = local.to_ascii_uppercase(); + assert_eq!( + stack + .positions_foreign_named(&uppercase) + .iter() + .collect::>(), + foreign, + "{local}" + ); + } + } + + #[test] + fn positions_follow_the_stack() { + let mut stack = OpenElems::default(); + // A fixed pseudo-random sequence of pushes, pops, removals, moves and replacements. + let mut state = 0x2545_f491_4f6c_dd1du64; + let mut next = |n: usize| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + (state % n as u64) as usize + }; + for _ in 0..2000 { + let name = next(NAMES.len()); + let len = stack.len(); + match next(6) { + 0 | 1 => stack.push(name, qual_name(name).expanded()), + 2 => { + stack.pop(); + }, + 3 if len > 0 => { + stack.remove(next(len)); + }, + 4 if len > 0 => { + let from = next(len); + stack.move_up(from, from + next(len - from)); + }, + 5 if len > 0 => { + let index = next(len); + stack.replace(index, name, qual_name(name).expanded()); + }, + _ => {}, + } + check(&stack); + } + } + + #[test] + fn scope() { + let mut stack = OpenElems::default(); + for name in [0, 1, 3, 6, 2] { + stack.push(name, qual_name(name).expanded()); + } + // html body p button div: the button bounds the button scope, not the default scope. + assert!(stack.named_in_scope(TrackedSet::DefaultScope, &local_name!("p"))); + assert!(!stack.named_in_scope(TrackedSet::ButtonScope, &local_name!("p"))); + assert!(stack.named_in_scope(TrackedSet::ButtonScope, &local_name!("button"))); + assert!(!stack.named_in_scope(TrackedSet::DefaultScope, &local_name!("li"))); + stack.remove(3); + assert!(stack.named_in_scope(TrackedSet::ButtonScope, &local_name!("p"))); + } +} diff --git a/html5ever/src/tree_builder/rules.rs b/html5ever/src/tree_builder/rules.rs index ed5291cf..3ac62e77 100644 --- a/html5ever/src/tree_builder/rules.rs +++ b/html5ever/src/tree_builder/rules.rs @@ -13,6 +13,10 @@ use crate::encoding::extract_a_character_encoding_from_a_meta_element; use crate::interface::Quirks; use crate::tokenizer::states::{Rawtext, Rcdata, ScriptData}; use crate::tokenizer::TagKind::{EndTag, StartTag}; +use crate::tree_builder::open_elems::Positions; +use crate::tree_builder::open_elems::TrackedSet::{ + self, ButtonScope, DefaultScope, ListItemScope, TableScope, +}; use crate::tree_builder::tag_sets::*; use crate::tree_builder::types::*; use crate::tree_builder::{ @@ -242,7 +246,7 @@ where self.sink.mark_script_already_started(&elem); } self.insert_appropriately(AppendNode(elem.clone()), None); - self.open_elems.borrow_mut().push(elem); + self.push(&elem); self.to_raw_text_mode(ScriptData) }, @@ -484,7 +488,7 @@ where }, Token::Tag(tag!()) => { - if self.in_scope_named(default_scope, local_name!("body")) { + if self.in_scope_named(DefaultScope, local_name!("body")) { self.check_body_end(); self.mode.set(InsertionMode::AfterBody); } else { @@ -495,7 +499,7 @@ where }, Token::Tag(tag!()) => { - if self.in_scope_named(default_scope, local_name!("body")) { + if self.in_scope_named(DefaultScope, local_name!("body")) { self.check_body_end(); ProcessResult::Reprocess(InsertionMode::AfterBody, token) } else { @@ -556,34 +560,32 @@ where }, Token::Tag(tag @ tag!(
  • |
    |
    )) => { - declare_tag_set!(close_list = "li"); - declare_tag_set!(close_defn = "dd" "dt"); - declare_tag_set!(extra_special = [special_tag] - "address" "div" "p"); - let list = match tag.name { - local_name!("li") => true, - local_name!("dd") | local_name!("dt") => false, + let can_close: &[_] = match tag.name { + local_name!("li") => &[local_name!("li")], + local_name!("dd") | local_name!("dt") => { + &[local_name!("dd"), local_name!("dt")] + }, _ => unreachable!(), }; self.frameset_ok.set(false); - let mut to_close = None; - for node in self.open_elems.borrow().iter().rev() { - let elem_name = self.sink.elem_name(node); - let name = elem_name.expanded(); - let can_close = if list { - close_list(name) - } else { - close_defn(name) - }; - if can_close { - to_close = Some(name.local.clone()); - break; - } - if extra_special(name) { - break; - } - } + // The topmost element we can close, unless a special element other than + // address, div or p is above it. + let to_close = { + let open_elems = self.open_elems.borrow(); + can_close + .iter() + .filter_map(|name| { + let position = open_elems.positions_named(name).last()?; + Some((position, name)) + }) + .max_by_key(|&(position, _)| position) + .filter(|&(position, _)| { + open_elems.in_scope(TrackedSet::SpecialExceptAddressDivP, position) + }) + .map(|(_, name)| name.clone()) + }; if let Some(name) = to_close { self.generate_implied_end_except(name.clone()); @@ -602,7 +604,7 @@ where }, Token::Tag(tag @ tag!(
  • | | )) => { let in_scope = if tag.name == local_name!("li") { - self.in_scope_named(list_item_scope, tag.name.clone()) + self.in_scope_named(ListItemScope, tag.name.clone()) } else { - self.in_scope_named(default_scope, tag.name.clone()) + self.in_scope_named(DefaultScope, tag.name.clone()) }; if in_scope { self.generate_implied_end_except(tag.name.clone()); @@ -715,7 +718,15 @@ where }, Token::Tag(tag @ tag!( | | | | | )) => { - if self.in_scope(default_scope, |n| self.elem_in(&n, heading_tag)) { + let headings = [ + local_name!("h1"), + local_name!("h2"), + local_name!("h3"), + local_name!("h4"), + local_name!("h5"), + local_name!("h6"), + ]; + if self.in_scope_any_named(DefaultScope, &headings) { self.generate_implied_end_tags(cursory_implied_end); if !self.current_node_named(tag.name) { self.sink.parse_error(Borrowed("Closing wrong heading tag")); @@ -745,7 +756,7 @@ where Token::Tag(tag @ tag!()) => { self.reconstruct_active_formatting_elements(); - if self.in_scope_named(default_scope, local_name!("nobr")) { + if self.in_scope_named(DefaultScope, local_name!("nobr")) { self.sink.parse_error(Borrowed("Nested ")); self.adoption_agency(local_name!("nobr")); self.reconstruct_active_formatting_elements(); @@ -773,7 +784,7 @@ where }, Token::Tag(tag @ tag!( | | )) => { - if !self.in_scope_named(default_scope, tag.name.clone()) { + if !self.in_scope_named(DefaultScope, tag.name.clone()) { self.unexpected(&tag); } else { self.generate_implied_end_tags(cursory_implied_end); @@ -822,7 +833,7 @@ where self.unexpected(&tag); } - if self.in_scope_named(default_scope, local_name!("select")) { + if self.in_scope_named(DefaultScope, local_name!("select")) { self.unexpected(&tag); self.pop_until_named(local_name!("select")); } @@ -846,10 +857,10 @@ where Token::Tag(tag @ tag!(
    )) => { self.close_p_element_in_button_scope(); - if self.in_scope_named(default_scope, local_name!("select")) { + if self.in_scope_named(DefaultScope, local_name!("select")) { self.generate_implied_end_tags(cursory_implied_end); - if self.in_scope_named(default_scope, local_name!("option")) - || self.in_scope_named(default_scope, local_name!("optgroup")) + if self.in_scope_named(DefaultScope, local_name!("option")) + || self.in_scope_named(DefaultScope, local_name!("optgroup")) { self.sink.parse_error(Borrowed("hr in option")); } @@ -900,7 +911,7 @@ where ) { self.unexpected(&tag); - } else if self.in_scope_named(default_scope, local_name!("select")) { + } else if self.in_scope_named(DefaultScope, local_name!("select")) { self.unexpected(&tag); self.pop_until_named(local_name!("select")); } else { @@ -913,9 +924,9 @@ where }, Token::Tag(tag @ tag!()) => { - if self.in_scope_named(default_scope, local_name!("select")) { + if self.in_scope_named(DefaultScope, local_name!("select")) { self.generate_implied_end_tags(cursory_implied_end); - if self.in_scope_named(default_scope, local_name!("option")) - || self.in_scope_named(default_scope, local_name!("optgroup")) + if self.in_scope_named(DefaultScope, local_name!("option")) + || self.in_scope_named(DefaultScope, local_name!("optgroup")) { self.sink.parse_error(Borrowed("nested options")); } @@ -945,7 +956,7 @@ where }, Token::Tag(tag @ tag!( | )) => { - if self.in_scope_named(default_scope, local_name!("ruby")) { + if self.in_scope_named(DefaultScope, local_name!("ruby")) { self.generate_implied_end_tags(cursory_implied_end); } if !self.current_node_named(local_name!("ruby")) { @@ -956,7 +967,7 @@ where }, Token::Tag(tag @ tag!( | )) => { - if self.in_scope_named(default_scope, local_name!("ruby")) { + if self.in_scope_named(DefaultScope, local_name!("ruby")) { self.generate_implied_end_except(local_name!("rtc")); } if !self.current_node_named(local_name!("rtc")) @@ -1077,7 +1088,7 @@ where Token::Tag(tag!()) => { self.unexpected(&token); - if self.in_scope_named(table_scope, local_name!("table")) { + if self.in_scope_named(TableScope, local_name!("table")) { self.pop_until_named(local_name!("table")); ProcessResult::Reprocess(self.reset_insertion_mode(), token) } else { @@ -1086,7 +1097,7 @@ where }, Token::Tag(tag!(
    )) => { - if self.in_scope_named(table_scope, local_name!("table")) { + if self.in_scope_named(TableScope, local_name!("table")) { self.pop_until_named(local_name!("table")); self.mode.set(self.reset_insertion_mode()); } else { @@ -1175,7 +1186,7 @@ where tag @ tag!( | | | | | | | | | | ), ) => { - if self.in_scope_named(table_scope, local_name!("caption")) { + if self.in_scope_named(TableScope, local_name!("caption")) { self.generate_implied_end_tags(cursory_implied_end); self.expect_to_close(local_name!("caption")); self.clear_active_formatting_to_marker(); @@ -1266,7 +1277,7 @@ where }, Token::Tag(tag @ tag!( | | )) => { - if self.in_scope_named(table_scope, tag.name.clone()) { + if self.in_scope_named(TableScope, tag.name.clone()) { self.pop_until_current(table_body_context); self.pop(); self.mode.set(InsertionMode::InTable); @@ -1279,8 +1290,12 @@ where Token::Tag( tag!( | | | | | | ), ) => { - declare_tag_set!(table_outer = "table" "tbody" "tfoot"); - if self.in_scope(table_scope, |e| self.elem_in(&e, table_outer)) { + let table_outer = [ + local_name!("table"), + local_name!("tbody"), + local_name!("tfoot"), + ]; + if self.in_scope_any_named(TableScope, &table_outer) { self.pop_until_current(table_body_context); self.pop(); ProcessResult::Reprocess(InsertionMode::InTable, token) @@ -1310,7 +1325,7 @@ where }, Token::Tag(tag!()) => { - if self.in_scope_named(table_scope, local_name!("tr")) { + if self.in_scope_named(TableScope, local_name!("tr")) { self.pop_until_current(table_row_context); let node = self.pop(); self.assert_named(&node, local_name!("tr")); @@ -1324,7 +1339,7 @@ where Token::Tag( tag!( | | | | | | | ), ) => { - if self.in_scope_named(table_scope, local_name!("tr")) { + if self.in_scope_named(TableScope, local_name!("tr")) { self.pop_until_current(table_row_context); let node = self.pop(); self.assert_named(&node, local_name!("tr")); @@ -1335,8 +1350,8 @@ where }, Token::Tag(tag @ tag!( | | )) => { - if self.in_scope_named(table_scope, tag.name.clone()) { - if self.in_scope_named(table_scope, local_name!("tr")) { + if self.in_scope_named(TableScope, tag.name.clone()) { + if self.in_scope_named(TableScope, local_name!("tr")) { self.pop_until_current(table_row_context); let node = self.pop(); self.assert_named(&node, local_name!("tr")); @@ -1360,7 +1375,7 @@ where // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intd InsertionMode::InCell => match token { Token::Tag(tag @ tag!( | )) => { - if self.in_scope_named(table_scope, tag.name.clone()) { + if self.in_scope_named(TableScope, tag.name.clone()) { self.generate_implied_end_tags(cursory_implied_end); self.expect_to_close(tag.name); self.clear_active_formatting_to_marker(); @@ -1374,7 +1389,8 @@ where Token::Tag( tag!( | | | | | | | | ), ) => { - if self.in_scope(table_scope, |n| self.elem_in(&n, td_th)) { + if self.in_scope_any_named(TableScope, &[local_name!("td"), local_name!("th")]) + { self.close_the_cell(); ProcessResult::Reprocess(InsertionMode::InRow, token) } else { @@ -1387,7 +1403,7 @@ where }, Token::Tag(tag @ tag!( | | | | )) => { - if self.in_scope_named(table_scope, tag.name.clone()) { + if self.in_scope_named(TableScope, tag.name.clone()) { self.close_the_cell(); ProcessResult::Reprocess(InsertionMode::InRow, Token::Tag(tag)) } else { @@ -1650,36 +1666,43 @@ where // FIXME(#118): in SVG Token::Tag(tag @ tag!()) => { - let mut first = true; - let mut stack_idx = self.open_elems.borrow().len() - 1; - loop { - if stack_idx == 0 { - return ProcessResult::Done; - } + let current_idx = self.open_elems.borrow().len() - 1; + if current_idx == 0 { + return ProcessResult::Done; + } - let html; - let eq; - { - let open_elems = self.open_elems.borrow(); - let node_name = self.sink.elem_name(&open_elems[stack_idx]); - html = *node_name.ns() == ns!(html); - eq = node_name.local_name().eq_ignore_ascii_case(&tag.name); - } - if !first && html { + let eq = self + .sink + .elem_name(&self.current_node()) + .local_name() + .eq_ignore_ascii_case(&tag.name); + if eq { + self.open_elems.borrow_mut().truncate(current_idx); + return ProcessResult::Done; + } + self.unexpected(&tag); + + // Below the current node, look for a foreign element with the tag's name, as far + // as the topmost HTML element, and never at the first element of the stack. + let (html_idx, eq_idx) = { + let open_elems = self.open_elems.borrow(); + let below_current = + |positions: Positions| positions.iter().rev().find(|&i| i < current_idx); + ( + below_current(open_elems.positions_in(TrackedSet::Html)), + below_current(open_elems.positions_foreign_named(&tag.name)), + ) + }; + match eq_idx { + Some(eq_idx) if eq_idx > 0 && html_idx.is_none_or(|i| eq_idx > i) => { + self.open_elems.borrow_mut().truncate(eq_idx); + ProcessResult::Done + }, + _ if html_idx.is_some_and(|i| i > 0) => { let mode = self.mode.get(); - return self.step(mode, Token::Tag(tag)); - } - - if eq { - self.open_elems.borrow_mut().truncate(stack_idx); - return ProcessResult::Done; - } - - if first { - self.unexpected(&tag); - first = false; - } - stack_idx -= 1; + self.step(mode, Token::Tag(tag)) + }, + _ => ProcessResult::Done, } }, diff --git a/html5ever/src/tree_builder/tag_sets.rs b/html5ever/src/tree_builder/tag_sets.rs index 47fece1c..e9d61e7e 100644 --- a/html5ever/src/tree_builder/tag_sets.rs +++ b/html5ever/src/tree_builder/tag_sets.rs @@ -86,6 +86,16 @@ declare_tag_set!(pub special_tag = "ul" "wbr" "xmp"); //§ END +declare_tag_set!(pub special_except_address_div_p = [special_tag] - "address" "div" "p"); + +declare_tag_set!(pub reset_insertion_mode_stop = + "td" "th" "tr" "tbody" "thead" "tfoot" "caption" "colgroup" "table" "template" "head" "body" + "frameset" "html"); + +declare_tag_set!(pub body_end_ok = + "dd" "dt" "li" "optgroup" "option" "p" "rp" "rt" "tbody" "td" "tfoot" "th" + "thead" "tr" "body" "html"); + pub(crate) fn mathml_text_integration_point(p: ExpandedName) -> bool { matches!( p, diff --git a/html5ever/tests/tree_builder.rs b/html5ever/tests/tree_builder.rs new file mode 100644 index 00000000..8c4fd49e --- /dev/null +++ b/html5ever/tests/tree_builder.rs @@ -0,0 +1,138 @@ +use html5ever::interface::{ElementFlags, NodeOrText, QuirksMode}; +use html5ever::tendril::TendrilSink; +use html5ever::{ + parse_document, tree_builder::TreeSink, Attribute, ExpandedName, ParseOpts, QualName, +}; +use markup5ever::tendril::StrTendril; +use std::borrow::Cow; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; + +/// A sink that builds no tree, and counts how often the tree builder asks it for an element's +/// name or compares two nodes. The tree builder does both for each element it looks at in the +/// stack of open elements. +#[derive(Default)] +struct Sink { + next_id: Cell, + names: RefCell>, + lookups: Cell, +} + +impl TreeSink for Sink { + type Handle = usize; + type Output = usize; + type ElemName<'a> = ExpandedName<'a>; + fn finish(self) -> usize { + self.lookups.get() + } + + fn get_document(&self) -> usize { + 0 + } + + fn get_template_contents(&self, target: &usize) -> usize { + target + 1 + } + + fn same_node(&self, x: &usize, y: &usize) -> bool { + self.lookups.set(self.lookups.get() + 1); + x == y + } + + fn elem_name(&self, target: &usize) -> ExpandedName<'_> { + self.lookups.set(self.lookups.get() + 1); + self.names + .borrow() + .get(target) + .expect("not an element") + .expanded() + } + + fn create_element(&self, name: QualName, _: Vec, _: ElementFlags) -> usize { + let id = self.next_id.get() + 2; + self.next_id.set(id); + // Leaked, to keep the sink small. + self.names + .borrow_mut() + .insert(id, Box::leak(Box::new(name))); + id + } + + fn create_comment(&self, _text: StrTendril) -> usize { + let id = self.next_id.get() + 2; + self.next_id.set(id); + id + } + + fn create_pi(&self, _target: StrTendril, _value: StrTendril) -> usize { + unimplemented!() + } + + fn append_before_sibling(&self, _sibling: &usize, _new_node: NodeOrText) {} + + fn append_based_on_parent_node( + &self, + _element: &usize, + _prev_element: &usize, + _new_node: NodeOrText, + ) { + } + + fn parse_error(&self, _msg: Cow<'static, str>) {} + fn set_quirks_mode(&self, _mode: QuirksMode) {} + fn append(&self, _parent: &usize, _child: NodeOrText) {} + + fn append_doctype_to_document(&self, _: StrTendril, _: StrTendril, _: StrTendril) {} + fn add_attrs_if_missing(&self, _target: &usize, _attrs: Vec) {} + fn remove_from_parent(&self, _target: &usize) {} + fn reparent_children(&self, _node: &usize, _new_parent: &usize) {} + fn mark_script_already_started(&self, _node: &usize) {} +} + +/// How many elements the tree builder looks at, per token, while parsing `html`. +fn lookups_per_token(html: &str, tokens: usize) -> usize { + let lookups = parse_document(Sink::default(), ParseOpts::default()).one(html); + lookups / tokens +} + +/// Deeply nested markup, followed by tokens that each make the tree builder look for something +/// in the stack of open elements. Each shape is `(prefix, nested, tokens)`: `nested` and then +/// `tokens` are repeated `n` times. +const SHAPES: &[(&str, &str, &str)] = &[ + // https://github.com/servo/html5ever/issues/788 + ("", "
    ", "
    "), + ("", "
    • ", ""), + ("", "", ""), + // Has an element in scope. + ("", "
      ", "
    • "), + ("", "
      ", ""), + ("", "
      ", "

      "), + ("", "
      ", ""), + ("", "
      ", ""), + ("

      ", "
      ", ""), + ("

      "), + ("", "

      ", ""), + ("
      ", "
      ", ""), + ("", "", ""), + ("", "
      ", "
      "), + ("", "
      ", ""), + ("", "", ""), +]; + +#[test] +fn stack_lookups_do_not_grow_with_depth() { + // Walking the stack would look at about n / 2 elements per token. + let n = 2000; + for &(prefix, nested, tokens) in SHAPES { + let html = format!("{prefix}{}{}", nested.repeat(n), tokens.repeat(n)); + let lookups = lookups_per_token(&html, 2 * n); + assert!( + lookups < 20, + "{prefix}{nested}…{tokens}…: {lookups} lookups per token" + ); + } +}