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!(