`_accept` collapsed every non-ACCEPT filter result into a single boolean, so FILTER_REJECT behaved exactly like FILTER_SKIP: the node was not returned, but the walk still descended into its subtree. Per DOM 6.1, FILTER_REJECT rejects a node *and* its descendants. This was unreachable until #440 exposed the real NodeFilter constants, which made the canonical prune idiom if (node.tagName === 'SCRIPT') return NodeFilter.FILTER_REJECT; start yielding nodes from inside the pruned subtree instead of failing loudly — sanitizers and text extractors built on TreeWalker silently got extra content. Split the filter into a three-valued `_filter` (keeping `_accept` as the boolean wrapper the sibling/child movers use) and give nextNode a second traversal step for the reject case. That step lives in the DOM layer as `DomTree::next_after_subtree`, mirroring how #447 put `next_in_subtree` there: both share the ancestor climb, and REJECT costs no more JS/native crossings than SKIP. NodeIterator shares this walker but has no subtree pruning (DOM 6.2: FILTER_REJECT behaves as FILTER_SKIP), so `createNodeIterator` marks the walker `_rejectIsSkip` and keeps the plain step. Covered by its own test so the shared implementation cannot drift.
1137 lines
41 KiB
Rust
1137 lines
41 KiB
Rust
use html5ever::{LocalName, Namespace, QualName};
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct NodeId(pub(crate) u32);
|
|
|
|
impl NodeId {
|
|
pub fn new(val: u32) -> Self {
|
|
NodeId(val)
|
|
}
|
|
|
|
pub fn index(self) -> usize {
|
|
self.0 as usize
|
|
}
|
|
|
|
pub fn raw(self) -> u32 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for NodeId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "NodeId({})", self.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Attribute {
|
|
pub name: QualName,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum NodeData {
|
|
Document,
|
|
Doctype {
|
|
name: String,
|
|
public_id: String,
|
|
system_id: String,
|
|
},
|
|
Element {
|
|
name: QualName,
|
|
attrs: Vec<Attribute>,
|
|
template_contents: Option<NodeId>,
|
|
mathml_annotation_xml_integration_point: bool,
|
|
},
|
|
Text {
|
|
contents: String,
|
|
},
|
|
Comment {
|
|
contents: String,
|
|
},
|
|
ProcessingInstruction {
|
|
target: String,
|
|
data: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Node {
|
|
pub id: NodeId,
|
|
pub parent: Option<NodeId>,
|
|
pub first_child: Option<NodeId>,
|
|
pub last_child: Option<NodeId>,
|
|
pub prev_sibling: Option<NodeId>,
|
|
pub next_sibling: Option<NodeId>,
|
|
pub data: NodeData,
|
|
}
|
|
|
|
impl Node {
|
|
pub fn is_document(&self) -> bool {
|
|
matches!(self.data, NodeData::Document)
|
|
}
|
|
|
|
pub fn is_element(&self) -> bool {
|
|
matches!(self.data, NodeData::Element { .. })
|
|
}
|
|
|
|
pub fn is_text(&self) -> bool {
|
|
matches!(self.data, NodeData::Text { .. })
|
|
}
|
|
|
|
pub fn as_element(&self) -> Option<&QualName> {
|
|
match &self.data {
|
|
NodeData::Element { name, .. } => Some(name),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn attrs(&self) -> Option<&[Attribute]> {
|
|
match &self.data {
|
|
NodeData::Element { attrs, .. } => Some(attrs),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn attrs_mut(&mut self) -> Option<&mut Vec<Attribute>> {
|
|
match &mut self.data {
|
|
NodeData::Element { attrs, .. } => Some(attrs),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_attribute(&self, name: &str) -> Option<&str> {
|
|
self.attrs()?.iter().find_map(|a| {
|
|
if a.name.local.as_ref() == name {
|
|
Some(a.value.as_str())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn set_attribute(&mut self, name: &str, value: String) {
|
|
if let NodeData::Element { attrs, .. } = &mut self.data {
|
|
if let Some(attr) = attrs.iter_mut().find(|a| a.name.local.as_ref() == name) {
|
|
attr.value = value;
|
|
} else {
|
|
attrs.push(Attribute {
|
|
name: QualName::new(None, Namespace::default(), LocalName::from(name)),
|
|
value,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn text_content_of_text_node(&self) -> Option<&str> {
|
|
match &self.data {
|
|
NodeData::Text { contents } => Some(contents),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct DomTree {
|
|
inner: RefCell<DomTreeInner>,
|
|
}
|
|
|
|
pub(crate) struct DomTreeInner {
|
|
pub(crate) nodes: Vec<Option<Node>>,
|
|
pub(crate) free_list: Vec<u32>,
|
|
pub(crate) document: NodeId,
|
|
pub(crate) id_index: HashMap<String, NodeId>,
|
|
}
|
|
|
|
impl DomTree {
|
|
pub fn new() -> Self {
|
|
let doc_node = Node {
|
|
id: NodeId(0),
|
|
parent: None,
|
|
first_child: None,
|
|
last_child: None,
|
|
prev_sibling: None,
|
|
next_sibling: None,
|
|
data: NodeData::Document,
|
|
};
|
|
DomTree {
|
|
inner: RefCell::new(DomTreeInner {
|
|
nodes: vec![Some(doc_node)],
|
|
free_list: Vec::new(),
|
|
document: NodeId(0),
|
|
id_index: HashMap::new(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn document(&self) -> NodeId {
|
|
self.inner.borrow().document
|
|
}
|
|
|
|
pub(crate) fn borrow_inner(&self) -> std::cell::Ref<'_, DomTreeInner> {
|
|
self.inner.borrow()
|
|
}
|
|
|
|
pub fn new_node(&self, data: NodeData) -> NodeId {
|
|
let mut inner = self.inner.borrow_mut();
|
|
let id = if let Some(slot) = inner.free_list.pop() {
|
|
NodeId(slot)
|
|
} else {
|
|
let idx = inner.nodes.len() as u32;
|
|
inner.nodes.push(None);
|
|
NodeId(idx)
|
|
};
|
|
|
|
if let NodeData::Element { ref attrs, .. } = data {
|
|
if let Some(id_attr) = attrs.iter().find(|a| a.name.local.as_ref() == "id") {
|
|
// Keep the FIRST element created with a given id. Parse order is
|
|
// document order, so getElementById / querySelector('#id') return
|
|
// the first-in-tree-order element on duplicate ids, per spec.
|
|
inner.id_index.entry(id_attr.value.clone()).or_insert(id);
|
|
}
|
|
}
|
|
|
|
inner.nodes[id.index()] = Some(Node {
|
|
id,
|
|
parent: None,
|
|
first_child: None,
|
|
last_child: None,
|
|
prev_sibling: None,
|
|
next_sibling: None,
|
|
data,
|
|
});
|
|
id
|
|
}
|
|
|
|
pub fn get_node(&self, id: NodeId) -> Option<Node> {
|
|
self.inner.borrow().nodes.get(id.index())?.clone()
|
|
}
|
|
|
|
pub fn with_node<F, R>(&self, id: NodeId, f: F) -> Option<R>
|
|
where
|
|
F: FnOnce(&Node) -> R,
|
|
{
|
|
let inner = self.inner.borrow();
|
|
inner.nodes.get(id.index())?.as_ref().map(f)
|
|
}
|
|
|
|
pub fn with_node_mut<F, R>(&self, id: NodeId, f: F) -> Option<R>
|
|
where
|
|
F: FnOnce(&mut Node) -> R,
|
|
{
|
|
let mut inner = self.inner.borrow_mut();
|
|
inner.nodes.get_mut(id.index())?.as_mut().map(f)
|
|
}
|
|
|
|
pub fn append_child(&self, parent_id: NodeId, child_id: NodeId) {
|
|
// Per DOM spec, appending a node to itself is a HierarchyRequestError;
|
|
// here we treat it as a no-op rather than panic. Without this the
|
|
// sibling-pointer fixup below sets the node's prev_sibling to itself
|
|
// and every later child-walk loops forever (same failure mode that
|
|
// insert_before's self-cycle guard was added to prevent).
|
|
if parent_id == child_id {
|
|
return;
|
|
}
|
|
// Appending an ancestor of the parent under that parent makes the
|
|
// parent/child graph cyclic, and every later descendants()/children()/
|
|
// textContent walk (none carry a visited set) would loop forever, pinning
|
|
// the thread in native Rust where neither tokio nor the V8 watchdog can
|
|
// interrupt it. Per the DOM spec this is a HierarchyRequestError; treat it
|
|
// as a no-op, like the self-append guard above. Only a node that already
|
|
// has children can be an ancestor, so a fresh/leaf child (the common
|
|
// append) skips the walk: O(1) hot path, O(depth) only when relocating a
|
|
// populated subtree.
|
|
{
|
|
let inner = self.inner.borrow();
|
|
let child_has_children = inner.nodes.get(child_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.map(|n| n.first_child.is_some())
|
|
.unwrap_or(false);
|
|
if child_has_children {
|
|
let mut cur = inner.nodes.get(parent_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.parent);
|
|
let mut steps = 0usize;
|
|
while let Some(p) = cur {
|
|
if p == child_id {
|
|
return;
|
|
}
|
|
steps += 1;
|
|
if steps > inner.nodes.len() {
|
|
return; // pre-existing corruption: refuse rather than risk a cycle
|
|
}
|
|
cur = inner.nodes.get(p.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.parent);
|
|
}
|
|
}
|
|
}
|
|
self.detach(child_id);
|
|
|
|
let mut inner = self.inner.borrow_mut();
|
|
|
|
let old_last = inner.nodes.get(parent_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.last_child);
|
|
|
|
if let Some(Some(child)) = inner.nodes.get_mut(child_id.index()) {
|
|
child.parent = Some(parent_id);
|
|
child.prev_sibling = old_last;
|
|
child.next_sibling = None;
|
|
}
|
|
|
|
if let Some(old_last_id) = old_last {
|
|
if let Some(Some(old_last_node)) = inner.nodes.get_mut(old_last_id.index()) {
|
|
old_last_node.next_sibling = Some(child_id);
|
|
}
|
|
}
|
|
|
|
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
|
|
if parent.first_child.is_none() {
|
|
parent.first_child = Some(child_id);
|
|
}
|
|
parent.last_child = Some(child_id);
|
|
}
|
|
}
|
|
|
|
pub fn insert_before(&self, existing_id: NodeId, new_sibling_id: NodeId) {
|
|
// Per DOM spec: if the node being inserted IS the reference node,
|
|
// the operation is a no-op (the node is already in its target
|
|
// position). Without this, the linked-list fixup below sets the
|
|
// node's prev_sibling and next_sibling to itself, creating a cycle
|
|
// -- every later traversal (childNodes, querySelectorAll, etc) then
|
|
// loops forever and the test page hangs while obscura burns RAM.
|
|
if existing_id == new_sibling_id {
|
|
return;
|
|
}
|
|
let parent_id = {
|
|
let inner = self.inner.borrow();
|
|
match inner.nodes.get(existing_id.index()).and_then(|n| n.as_ref()).and_then(|n| n.parent) {
|
|
Some(p) => p,
|
|
None => return,
|
|
}
|
|
};
|
|
|
|
// Inserting the parent itself, or any ancestor of the parent, as a child
|
|
// of that parent would create a cycle (same non-terminating-walk hang as
|
|
// append_child). Reject it, matching the self-insert guard above. Gate on
|
|
// the inserted node actually having children, so the common case (insert a
|
|
// fresh node) stays O(1).
|
|
{
|
|
let inner = self.inner.borrow();
|
|
let new_has_children = inner.nodes.get(new_sibling_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.map(|n| n.first_child.is_some())
|
|
.unwrap_or(false);
|
|
if new_has_children {
|
|
let mut cur = Some(parent_id);
|
|
let mut steps = 0usize;
|
|
while let Some(p) = cur {
|
|
if p == new_sibling_id {
|
|
return;
|
|
}
|
|
steps += 1;
|
|
if steps > inner.nodes.len() {
|
|
return;
|
|
}
|
|
cur = inner.nodes.get(p.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.detach(new_sibling_id);
|
|
|
|
// Read existing's prev AFTER detaching new. If new was existing's
|
|
// immediate previous sibling, detach moved that pointer; using the
|
|
// pre-detach value would splice new.next_sibling = new (a self-cycle)
|
|
// and hang every later sibling walk. This is what hung ebay.com.
|
|
let prev_id = {
|
|
let inner = self.inner.borrow();
|
|
inner.nodes.get(existing_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.prev_sibling)
|
|
};
|
|
|
|
let mut inner = self.inner.borrow_mut();
|
|
|
|
if let Some(Some(node)) = inner.nodes.get_mut(new_sibling_id.index()) {
|
|
node.parent = Some(parent_id);
|
|
node.prev_sibling = prev_id;
|
|
node.next_sibling = Some(existing_id);
|
|
}
|
|
|
|
if let Some(Some(node)) = inner.nodes.get_mut(existing_id.index()) {
|
|
node.prev_sibling = Some(new_sibling_id);
|
|
}
|
|
|
|
if let Some(prev) = prev_id {
|
|
if let Some(Some(node)) = inner.nodes.get_mut(prev.index()) {
|
|
node.next_sibling = Some(new_sibling_id);
|
|
}
|
|
} else if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
|
|
parent.first_child = Some(new_sibling_id);
|
|
}
|
|
}
|
|
|
|
pub fn detach(&self, node_id: NodeId) {
|
|
let mut inner = self.inner.borrow_mut();
|
|
|
|
let (parent_id, prev_id, next_id) = match inner.nodes.get(node_id.index()).and_then(|n| n.as_ref()) {
|
|
Some(node) => (node.parent, node.prev_sibling, node.next_sibling),
|
|
None => return,
|
|
};
|
|
|
|
if let Some(prev) = prev_id {
|
|
if let Some(Some(node)) = inner.nodes.get_mut(prev.index()) {
|
|
node.next_sibling = next_id;
|
|
}
|
|
} else if let Some(parent_id) = parent_id {
|
|
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
|
|
parent.first_child = next_id;
|
|
}
|
|
}
|
|
|
|
if let Some(next) = next_id {
|
|
if let Some(Some(node)) = inner.nodes.get_mut(next.index()) {
|
|
node.prev_sibling = prev_id;
|
|
}
|
|
} else if let Some(parent_id) = parent_id {
|
|
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
|
|
parent.last_child = prev_id;
|
|
}
|
|
}
|
|
|
|
if let Some(Some(node)) = inner.nodes.get_mut(node_id.index()) {
|
|
node.parent = None;
|
|
node.prev_sibling = None;
|
|
node.next_sibling = None;
|
|
}
|
|
}
|
|
|
|
/// Detach a node from its parent AND remove it (and all descendants)
|
|
/// from the id-index so that `getElementById` no longer returns them.
|
|
/// Unlike `remove()`, this does NOT free the nodes — the JS side may
|
|
/// still hold references to the wrappers.
|
|
pub fn remove_child(&self, node_id: NodeId) {
|
|
// Collect all id attribute values in the subtree. We snapshot them
|
|
// before detaching so `get_attribute` can still see the tree.
|
|
let ids_to_remove: Vec<String> = {
|
|
let descendants = self.descendants(node_id);
|
|
let inner = self.inner.borrow();
|
|
let mut ids: Vec<String> = Vec::new();
|
|
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
|
if let Some(id_val) = node.get_attribute("id") {
|
|
ids.push(id_val.to_string());
|
|
}
|
|
}
|
|
for desc_id in &descendants {
|
|
if let Some(Some(node)) = inner.nodes.get(desc_id.index()) {
|
|
if let Some(id_val) = node.get_attribute("id") {
|
|
ids.push(id_val.to_string());
|
|
}
|
|
}
|
|
}
|
|
ids
|
|
};
|
|
|
|
self.detach(node_id);
|
|
|
|
let mut inner = self.inner.borrow_mut();
|
|
for id_str in &ids_to_remove {
|
|
inner.id_index.remove(id_str);
|
|
}
|
|
}
|
|
|
|
pub fn remove(&self, node_id: NodeId) {
|
|
self.detach(node_id);
|
|
let descendants = self.descendants(node_id);
|
|
let mut inner = self.inner.borrow_mut();
|
|
|
|
let mut ids_to_remove = Vec::new();
|
|
for &desc_id in &descendants {
|
|
if let Some(Some(node)) = inner.nodes.get(desc_id.index()) {
|
|
if let Some(id_val) = node.get_attribute("id") {
|
|
ids_to_remove.push(id_val.to_string());
|
|
}
|
|
}
|
|
}
|
|
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
|
if let Some(id_val) = node.get_attribute("id") {
|
|
ids_to_remove.push(id_val.to_string());
|
|
}
|
|
}
|
|
|
|
for id_str in ids_to_remove {
|
|
inner.id_index.remove(&id_str);
|
|
}
|
|
|
|
for desc_id in descendants {
|
|
inner.nodes[desc_id.index()] = None;
|
|
inner.free_list.push(desc_id.0);
|
|
}
|
|
inner.nodes[node_id.index()] = None;
|
|
inner.free_list.push(node_id.0);
|
|
}
|
|
|
|
pub fn children(&self, node_id: NodeId) -> Vec<NodeId> {
|
|
let inner = self.inner.borrow();
|
|
let mut result = Vec::new();
|
|
let mut current = inner.nodes.get(node_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.first_child);
|
|
while let Some(child_id) = current {
|
|
result.push(child_id);
|
|
current = inner.nodes.get(child_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.next_sibling);
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn descendants(&self, node_id: NodeId) -> Vec<NodeId> {
|
|
let inner = self.inner.borrow();
|
|
let mut result = Vec::new();
|
|
let mut stack = Vec::new();
|
|
|
|
let mut first = inner.nodes.get(node_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.first_child);
|
|
let mut children_to_push = Vec::new();
|
|
while let Some(child_id) = first {
|
|
children_to_push.push(child_id);
|
|
if children_to_push.len() > inner.nodes.len() {
|
|
eprintln!("obscura: sibling-chain cap hit at node {} - cycle", node_id.index());
|
|
break;
|
|
}
|
|
first = inner.nodes.get(child_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.next_sibling);
|
|
}
|
|
for child_id in children_to_push.into_iter().rev() {
|
|
stack.push(child_id);
|
|
}
|
|
|
|
while let Some(current) = stack.pop() {
|
|
result.push(current);
|
|
// Defense in depth: a well-formed subtree has at most nodes.len()
|
|
// descendants. Exceeding that means the parent/child graph is cyclic
|
|
// (which the append_child / insert_before guards prevent); stop rather
|
|
// than grow the stack and result forever and wedge the engine. On a
|
|
// valid tree this bound is never reached, so the hot path is unchanged.
|
|
if result.len() > inner.nodes.len() {
|
|
eprintln!(
|
|
"obscura: descendants() cap hit at node {} ({} nodes) - tree has a cycle",
|
|
node_id.index(),
|
|
inner.nodes.len()
|
|
);
|
|
break;
|
|
}
|
|
|
|
let mut child = inner.nodes.get(current.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.first_child);
|
|
let mut children_to_push = Vec::new();
|
|
while let Some(child_id) = child {
|
|
children_to_push.push(child_id);
|
|
if children_to_push.len() > inner.nodes.len() {
|
|
eprintln!("obscura: sibling-chain cap hit at node {} - cycle", current.index());
|
|
break;
|
|
}
|
|
child = inner.nodes.get(child_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.next_sibling);
|
|
}
|
|
for child_id in children_to_push.into_iter().rev() {
|
|
stack.push(child_id);
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Returns the node after `current` in document order, without leaving the
|
|
/// subtree rooted at `root`.
|
|
///
|
|
/// Keeping the ancestor climb inside the DOM avoids one JS/native crossing
|
|
/// per ancestor when a TreeWalker reaches a deep leaf.
|
|
pub fn next_in_subtree(&self, root: NodeId, current: NodeId) -> Option<NodeId> {
|
|
let inner = self.inner.borrow();
|
|
let current_node = inner.nodes.get(current.index())?.as_ref()?;
|
|
if let Some(child) = current_node.first_child {
|
|
return Some(child);
|
|
}
|
|
Self::climb_to_next_sibling(&inner, root, current)
|
|
}
|
|
|
|
/// Returns the node after the whole subtree rooted at `current`, in document
|
|
/// order, without leaving the subtree rooted at `root`.
|
|
///
|
|
/// This is `next_in_subtree` minus the descend-into-children step, which is
|
|
/// what `NodeFilter.FILTER_REJECT` needs: it rejects a node *and* its
|
|
/// descendants, unlike `FILTER_SKIP`, which only skips the node itself and
|
|
/// is served by `next_in_subtree`.
|
|
pub fn next_after_subtree(&self, root: NodeId, current: NodeId) -> Option<NodeId> {
|
|
let inner = self.inner.borrow();
|
|
Self::climb_to_next_sibling(&inner, root, current)
|
|
}
|
|
|
|
/// Follow `current`'s next sibling, climbing ancestors until one has a next
|
|
/// sibling — without stepping outside `root`.
|
|
fn climb_to_next_sibling(
|
|
inner: &DomTreeInner,
|
|
root: NodeId,
|
|
current: NodeId,
|
|
) -> Option<NodeId> {
|
|
let mut node_id = current;
|
|
for _ in 0..=inner.nodes.len() {
|
|
if node_id == root {
|
|
return None;
|
|
}
|
|
let node = inner.nodes.get(node_id.index())?.as_ref()?;
|
|
if let Some(sibling) = node.next_sibling {
|
|
return Some(sibling);
|
|
}
|
|
node_id = node.parent?;
|
|
}
|
|
|
|
// Parent cycles are prevented by the mutation APIs. Keep a hard bound
|
|
// here as defense in depth for a malformed tree.
|
|
None
|
|
}
|
|
|
|
pub fn ancestors(&self, node_id: NodeId) -> Vec<NodeId> {
|
|
let inner = self.inner.borrow();
|
|
let mut result = Vec::new();
|
|
let mut current = inner.nodes.get(node_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.parent);
|
|
while let Some(parent_id) = current {
|
|
result.push(parent_id);
|
|
current = inner.nodes.get(parent_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.parent);
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn get_element_by_id(&self, id: &str) -> Option<NodeId> {
|
|
self.inner.borrow().id_index.get(id).copied()
|
|
}
|
|
|
|
pub fn text_content(&self, node_id: NodeId) -> String {
|
|
let inner = self.inner.borrow();
|
|
// Per DOM spec, calling textContent ON a CharacterData node
|
|
// (Text, Comment, ProcessingInstruction) returns its .data.
|
|
// Calling textContent on an Element walks descendants and
|
|
// concatenates Text node content only (Comment + PI are
|
|
// skipped). Handle the direct-CharacterData case here so the
|
|
// descent helper can keep its element-centric behavior.
|
|
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
|
match &node.data {
|
|
NodeData::Text { contents } => return contents.clone(),
|
|
NodeData::Comment { contents } => return contents.clone(),
|
|
NodeData::ProcessingInstruction { data, .. } => return data.clone(),
|
|
_ => {}
|
|
}
|
|
}
|
|
let mut result = String::new();
|
|
collect_text_inner(&inner, node_id, &mut result);
|
|
result
|
|
}
|
|
|
|
pub fn append_text(&self, parent_id: NodeId, text: &str) {
|
|
let last_child_is_text = {
|
|
let inner = self.inner.borrow();
|
|
inner.nodes.get(parent_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.last_child)
|
|
.and_then(|lc| inner.nodes.get(lc.index()))
|
|
.and_then(|n| n.as_ref())
|
|
.map(|n| n.is_text())
|
|
.unwrap_or(false)
|
|
};
|
|
|
|
if last_child_is_text {
|
|
// Re-read last_child without unwrap: if it vanished between the two
|
|
// borrows, fall through to appending a fresh text node rather than
|
|
// panicking (a panic here aborts the whole engine via V8_Fatal).
|
|
let last_child_id = {
|
|
let inner = self.inner.borrow();
|
|
inner.nodes.get(parent_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.last_child)
|
|
};
|
|
if let Some(last_child_id) = last_child_id {
|
|
let mut inner = self.inner.borrow_mut();
|
|
if let Some(Some(node)) = inner.nodes.get_mut(last_child_id.index()) {
|
|
if let NodeData::Text { contents } = &mut node.data {
|
|
contents.push_str(text);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let text_id = self.new_node(NodeData::Text {
|
|
contents: text.to_string(),
|
|
});
|
|
self.append_child(parent_id, text_id);
|
|
}
|
|
|
|
pub fn find_body_or_root(&self) -> NodeId {
|
|
let doc = self.document();
|
|
for child in self.children(doc) {
|
|
if let Some(n) = self.get_node(child) {
|
|
if n.as_element().map(|name| name.local.as_ref() == "html").unwrap_or(false) {
|
|
for html_child in self.children(child) {
|
|
if let Some(hc) = self.get_node(html_child) {
|
|
if hc.as_element().map(|name| name.local.as_ref() == "body").unwrap_or(false) {
|
|
return html_child;
|
|
}
|
|
}
|
|
}
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
doc
|
|
}
|
|
|
|
pub fn import_children_from(&self, parent_id: NodeId, source: &DomTree, source_node: NodeId) {
|
|
let source_children = source.children(source_node);
|
|
for source_child_id in source_children {
|
|
self.import_node_from(parent_id, source, source_child_id);
|
|
}
|
|
}
|
|
|
|
fn import_node_from(&self, parent_id: NodeId, source: &DomTree, source_node_id: NodeId) {
|
|
// Iterative DFS with an explicit (dest_parent, source_node) stack so a
|
|
// deeply nested source tree cannot overflow the thread stack and abort
|
|
// the process. Children are pushed in reverse so they are appended in
|
|
// document order (append_child always appends to the end, so each level
|
|
// keeps the source ordering).
|
|
let mut stack = vec![(parent_id, source_node_id)];
|
|
while let Some((dest_parent, src_id)) = stack.pop() {
|
|
let node_data = {
|
|
let source_inner = source.inner.borrow();
|
|
match source_inner.nodes.get(src_id.index()) {
|
|
Some(Some(node)) => node.data.clone(),
|
|
_ => continue,
|
|
}
|
|
};
|
|
|
|
let new_id = self.new_node(node_data);
|
|
self.append_child(dest_parent, new_id);
|
|
|
|
for child_id in source.children(src_id).into_iter().rev() {
|
|
stack.push((new_id, child_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.inner.borrow().nodes.iter().filter(|n| n.is_some()).count()
|
|
}
|
|
|
|
// Number of node slots (live plus freed), i.e. the same upper bound
|
|
// descendants() uses to cap a tree walk. A well-formed subtree has at most
|
|
// this many nodes, so it is a safe ceiling for iterative walkers that need a
|
|
// cycle backstop.
|
|
pub(crate) fn node_slot_count(&self) -> usize {
|
|
self.inner.borrow().nodes.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() <= 1
|
|
}
|
|
|
|
pub fn update_id_index(&self, node_id: NodeId, old_id: Option<&str>, new_id: Option<&str>) {
|
|
let mut inner = self.inner.borrow_mut();
|
|
if let Some(old) = old_id {
|
|
inner.id_index.remove(old);
|
|
}
|
|
if let Some(new) = new_id {
|
|
inner.id_index.insert(new.to_string(), node_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collect_text_inner(inner: &DomTreeInner, node_id: NodeId, buf: &mut String) {
|
|
// Iterative pre-order walk on an explicit heap stack. The recursive form
|
|
// overflowed the thread stack and aborted the process on deeply nested
|
|
// trees; descendants() is iterative + capped for the same reason. A valid
|
|
// subtree visits at most nodes.len() nodes, so exceeding that means the
|
|
// graph is cyclic (prevented by the append_child / insert_before guards);
|
|
// stop rather than spin forever.
|
|
let max_steps = inner.nodes.len().saturating_add(16);
|
|
let mut steps = 0usize;
|
|
let mut stack = vec![node_id];
|
|
|
|
while let Some(id) = stack.pop() {
|
|
steps += 1;
|
|
if steps > max_steps {
|
|
eprintln!("obscura: collect_text_inner cap hit - tree has a cycle");
|
|
break;
|
|
}
|
|
|
|
let node = match inner.nodes.get(id.index()) {
|
|
Some(Some(n)) => n,
|
|
_ => continue,
|
|
};
|
|
|
|
match &node.data {
|
|
NodeData::Text { contents } => buf.push_str(contents),
|
|
// Comment and ProcessingInstruction are intentionally NOT
|
|
// appended when traversing descendants: per spec, textContent
|
|
// on an Element only includes Text descendants. Direct
|
|
// textContent on a Comment/PI is handled by the caller.
|
|
_ => {
|
|
// Collect children, then push them in reverse so they pop in
|
|
// document order.
|
|
let mut kids = Vec::new();
|
|
let mut child = node.first_child;
|
|
while let Some(child_id) = child {
|
|
kids.push(child_id);
|
|
if kids.len() > inner.nodes.len() {
|
|
eprintln!("obscura: collect_text_inner sibling cap hit - cycle");
|
|
break;
|
|
}
|
|
child = inner.nodes.get(child_id.index())
|
|
.and_then(|n| n.as_ref())
|
|
.and_then(|n| n.next_sibling);
|
|
}
|
|
for child_id in kids.into_iter().rev() {
|
|
stack.push(child_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for DomTree {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_new_tree_has_document() {
|
|
let tree = DomTree::new();
|
|
assert_eq!(tree.len(), 1);
|
|
let node = tree.get_node(tree.document()).unwrap();
|
|
assert!(node.is_document());
|
|
}
|
|
|
|
#[test]
|
|
fn test_append_child() {
|
|
let tree = DomTree::new();
|
|
let child = tree.new_node(NodeData::Text {
|
|
contents: "hello".into(),
|
|
});
|
|
let doc = tree.document();
|
|
tree.append_child(doc, child);
|
|
|
|
assert_eq!(tree.len(), 2);
|
|
let doc_node = tree.get_node(doc).unwrap();
|
|
assert_eq!(doc_node.first_child, Some(child));
|
|
assert_eq!(doc_node.last_child, Some(child));
|
|
|
|
let child_node = tree.get_node(child).unwrap();
|
|
assert_eq!(child_node.parent, Some(doc));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_children() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
|
|
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
|
|
let c3 = tree.new_node(NodeData::Text { contents: "c".into() });
|
|
tree.append_child(doc, c1);
|
|
tree.append_child(doc, c2);
|
|
tree.append_child(doc, c3);
|
|
|
|
assert_eq!(tree.children(doc), vec![c1, c2, c3]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detach() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
|
|
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
|
|
tree.append_child(doc, c1);
|
|
tree.append_child(doc, c2);
|
|
|
|
tree.detach(c1);
|
|
assert_eq!(tree.children(doc), vec![c2]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_insert_before() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
|
|
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
|
|
let c3 = tree.new_node(NodeData::Text { contents: "c".into() });
|
|
tree.append_child(doc, c1);
|
|
tree.append_child(doc, c3);
|
|
tree.insert_before(c3, c2);
|
|
|
|
assert_eq!(tree.children(doc), vec![c1, c2, c3]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_content() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let div = tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), local_name!("div")),
|
|
attrs: vec![],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
});
|
|
tree.append_child(doc, div);
|
|
|
|
let t1 = tree.new_node(NodeData::Text { contents: "Hello ".into() });
|
|
let t2 = tree.new_node(NodeData::Text { contents: "World".into() });
|
|
tree.append_child(div, t1);
|
|
tree.append_child(div, t2);
|
|
|
|
assert_eq!(tree.text_content(div), "Hello World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_element_by_id() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let div = tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), local_name!("div")),
|
|
attrs: vec![Attribute {
|
|
name: QualName::new(None, Namespace::default(), LocalName::from("id")),
|
|
value: "main".into(),
|
|
}],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
});
|
|
tree.append_child(doc, div);
|
|
|
|
assert_eq!(tree.get_element_by_id("main"), Some(div));
|
|
assert_eq!(tree.get_element_by_id("nonexistent"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reparent_cycle_is_rejected() {
|
|
// document -> html -> body -> div. Moving an ancestor under one of its
|
|
// own descendants would make the parent/child graph cyclic and hang
|
|
// every later descendants() walk. Both append_child and insert_before
|
|
// must reject it as a no-op (DOM HierarchyRequestError).
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let mk = |n: &str| {
|
|
tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), LocalName::from(n)),
|
|
attrs: vec![],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
})
|
|
};
|
|
let html = mk("html");
|
|
let body = mk("body");
|
|
let div = mk("div");
|
|
tree.append_child(doc, html);
|
|
tree.append_child(html, body);
|
|
tree.append_child(body, div);
|
|
|
|
let before = tree.descendants(doc).len();
|
|
assert_eq!(before, 3);
|
|
|
|
// append_child: html is an ancestor of div -> must be a no-op, no cycle.
|
|
tree.append_child(div, html);
|
|
assert_eq!(tree.descendants(doc).len(), before, "cyclic append must be a no-op");
|
|
assert_eq!(tree.descendants(div).len(), 0, "div must stay a leaf");
|
|
|
|
// insert_before: html is an ancestor of body (div's parent) -> no-op.
|
|
tree.insert_before(div, html);
|
|
assert_eq!(tree.descendants(doc).len(), before, "cyclic insert_before must be a no-op");
|
|
|
|
// self-append / self-insert remain no-ops (existing guards).
|
|
tree.append_child(div, div);
|
|
tree.insert_before(div, div);
|
|
assert_eq!(tree.descendants(doc).len(), before);
|
|
}
|
|
|
|
#[test]
|
|
fn test_insert_before_previous_sibling_no_cycle() {
|
|
// Inserting a node before its own immediate previous sibling is a no-op
|
|
// reorder that frameworks do constantly. It used to splice
|
|
// next_sibling = self via a prev_id captured before detach, hanging every
|
|
// later sibling walk (this hung ebay.com). The result must stay a
|
|
// well-formed [a, b] with no cycle.
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let mk = |n: &str| {
|
|
tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), LocalName::from(n)),
|
|
attrs: vec![],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
})
|
|
};
|
|
let parent = mk("div");
|
|
let a = mk("a");
|
|
let b = mk("b");
|
|
tree.append_child(doc, parent);
|
|
tree.append_child(parent, a);
|
|
tree.append_child(parent, b); // parent -> [a, b]
|
|
|
|
// a is already b's previous sibling; this reorder must not create a cycle.
|
|
tree.insert_before(b, a);
|
|
|
|
let kids = tree.descendants(parent);
|
|
assert_eq!(kids, vec![a, b], "order preserved, no cycle");
|
|
}
|
|
|
|
#[test]
|
|
fn test_append_text_merges() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
tree.append_text(doc, "Hello ");
|
|
tree.append_text(doc, "World");
|
|
|
|
assert_eq!(tree.children(doc).len(), 1);
|
|
assert_eq!(tree.text_content(doc), "Hello World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_subtree() {
|
|
let tree = DomTree::new();
|
|
let doc = tree.document();
|
|
let div = tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), local_name!("div")),
|
|
attrs: vec![],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
});
|
|
tree.append_child(doc, div);
|
|
let text = tree.new_node(NodeData::Text { contents: "hi".into() });
|
|
tree.append_child(div, text);
|
|
|
|
assert_eq!(tree.len(), 3);
|
|
tree.remove(div);
|
|
assert_eq!(tree.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_next_in_subtree_follows_document_order_and_stays_within_root() {
|
|
let tree = DomTree::new();
|
|
let root = tree.new_node(NodeData::Text { contents: "root".into() });
|
|
let first = tree.new_node(NodeData::Text { contents: "first".into() });
|
|
let nested = tree.new_node(NodeData::Text { contents: "nested".into() });
|
|
let second = tree.new_node(NodeData::Text { contents: "second".into() });
|
|
tree.append_child(tree.document(), root);
|
|
tree.append_child(root, first);
|
|
tree.append_child(first, nested);
|
|
tree.append_child(root, second);
|
|
|
|
assert_eq!(tree.next_in_subtree(root, root), Some(first));
|
|
assert_eq!(tree.next_in_subtree(root, first), Some(nested));
|
|
assert_eq!(tree.next_in_subtree(root, nested), Some(second));
|
|
assert_eq!(tree.next_in_subtree(root, second), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_next_after_subtree_skips_descendants() {
|
|
// Same shape as above: root > [first > nested, second]. Stepping past
|
|
// `first` must land on `second`, not descend into `nested` — that is
|
|
// what NodeFilter.FILTER_REJECT needs.
|
|
let tree = DomTree::new();
|
|
let root = tree.new_node(NodeData::Text { contents: "root".into() });
|
|
let first = tree.new_node(NodeData::Text { contents: "first".into() });
|
|
let nested = tree.new_node(NodeData::Text { contents: "nested".into() });
|
|
let second = tree.new_node(NodeData::Text { contents: "second".into() });
|
|
tree.append_child(tree.document(), root);
|
|
tree.append_child(root, first);
|
|
tree.append_child(first, nested);
|
|
tree.append_child(root, second);
|
|
|
|
assert_eq!(tree.next_after_subtree(root, first), Some(second));
|
|
// A leaf behaves identically to next_in_subtree: there is no subtree.
|
|
assert_eq!(tree.next_after_subtree(root, nested), Some(second));
|
|
assert_eq!(tree.next_after_subtree(root, second), None);
|
|
// Rejecting the root itself exhausts the walk rather than escaping it.
|
|
assert_eq!(tree.next_after_subtree(root, root), None);
|
|
}
|
|
|
|
// Builds a chain of `depth` nested <div> elements under the document and
|
|
// returns (outermost_div, innermost_div). Depth this large overflows a
|
|
// recursive tree walk and aborts the process, so it guards the iterative
|
|
// serialize / text_content / import paths against regressing to recursion.
|
|
fn build_deep_chain(tree: &DomTree, depth: usize) -> (NodeId, NodeId) {
|
|
let mk_div = || {
|
|
tree.new_node(NodeData::Element {
|
|
name: QualName::new(None, ns!(html), local_name!("div")),
|
|
attrs: vec![],
|
|
template_contents: None,
|
|
mathml_annotation_xml_integration_point: false,
|
|
})
|
|
};
|
|
let root = mk_div();
|
|
tree.append_child(tree.document(), root);
|
|
let mut cur = root;
|
|
for _ in 1..depth {
|
|
let next = mk_div();
|
|
tree.append_child(cur, next);
|
|
cur = next;
|
|
}
|
|
(root, cur)
|
|
}
|
|
|
|
#[test]
|
|
fn test_outer_html_deeply_nested_does_not_overflow() {
|
|
let tree = DomTree::new();
|
|
let (root, leaf) = build_deep_chain(&tree, 100_000);
|
|
let marker = tree.new_node(NodeData::Text {
|
|
contents: "leaf".into(),
|
|
});
|
|
tree.append_child(leaf, marker);
|
|
|
|
let html = tree.outer_html(root);
|
|
assert!(html.starts_with("<div>"));
|
|
assert!(html.contains("leaf"));
|
|
assert!(html.ends_with("</div>"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_content_deeply_nested_does_not_overflow() {
|
|
let tree = DomTree::new();
|
|
let (root, leaf) = build_deep_chain(&tree, 100_000);
|
|
let marker = tree.new_node(NodeData::Text {
|
|
contents: "deep".into(),
|
|
});
|
|
tree.append_child(leaf, marker);
|
|
|
|
assert_eq!(tree.text_content(root), "deep");
|
|
}
|
|
|
|
#[test]
|
|
fn test_import_deeply_nested_does_not_overflow() {
|
|
let source = DomTree::new();
|
|
build_deep_chain(&source, 100_000);
|
|
|
|
let dest = DomTree::new();
|
|
let dest_doc = dest.document();
|
|
dest.import_children_from(dest_doc, &source, source.document());
|
|
|
|
assert!(dest.len() >= 100_000);
|
|
}
|
|
}
|