JavaScript: an attribute placed among XML children goes in as text

ECMA-357 sends a single attribute given to [[Insert]] through [[Replace]], which makes a text node. A list holding an attribute went into the children as the attribute itself, changed its parent and stayed on its old owner. Each attribute now goes in as a new text node with its value, and it stays on its owner. A list assigned such a value holds the new text nodes, and an attribute does not count against the depth limit.
This commit is contained in:
dotCooCoo
2026-11-09 07:58:49 -08:00
parent 01673695af
commit 33f04522f8
2 changed files with 98 additions and 25 deletions
+54 -25
View File
@@ -907,7 +907,9 @@ impl Interp {
Content::List(l) => {
for at in 0..self.list_len(*l)? {
let Some(item) = self.list_at(*l, at)? else { break };
self.check_attach_node(x, item)?;
if self.xml_kind(item)? != XmlKind::Attribute {
self.check_attach_node(x, item)?;
}
}
Ok(())
}
@@ -940,9 +942,6 @@ impl Interp {
/// position in `x` to use after the removal. The caller has taken [`Self::place_fuel`].
fn detach(&mut self, x: ObjId, v: ObjId, at: usize) -> JsResult<usize> {
let Some(parent) = self.xml_node(v)?.parent else { return Ok(at) };
if self.xml_kind(v)? == XmlKind::Attribute {
return Ok(at);
}
let Some(q) = self.xml_node(parent)?.children.iter().position(|&c| c == v) else { return Ok(at) };
self.remove_child(parent, q)?;
Ok(if parent == x && q < at { at - 1 } else { at })
@@ -952,10 +951,7 @@ impl Interp {
/// the scan and removal that take it out.
fn place_fuel(&mut self, len: usize, v: ObjId) -> JsResult<()> {
self.xml_text_fuel(len + 1)?;
let node = self.xml_node(v)?;
if node.kind != XmlKind::Attribute
&& let Some(parent) = node.parent
{
if let Some(parent) = self.xml_node(v)?.parent {
let old = self.children_len(parent)?;
self.xml_text_fuel(old)?;
self.xml_text_fuel(old)?;
@@ -987,15 +983,30 @@ impl Interp {
self.place_child(x, at, v)
}
/// Takes the fuel and heap to put every item of `l` under `x` and returns the items. The tree
/// does not change here, so a limit leaves it as it was.
/// The node to place as a child for `item`: `item` itself, or a new text node holding the
/// value when it is an attribute. `roots` are held across any collection.
fn child_for_item(&mut self, item: ObjId, roots: &[Value]) -> JsResult<ObjId> {
if self.xml_kind(item)? != XmlKind::Attribute {
return Ok(item);
}
let value = self.xml_node(item)?.value.clone();
let t = self.rooted(roots, |i| i.new_text_node(value, None))?;
self.xml_keep(t);
Ok(t)
}
/// Takes the fuel and heap to put every item of `l` under `x` and returns the nodes to place,
/// from [`Self::child_for_item`]. The tree does not change here, so a limit leaves it as it
/// was.
fn reserve_list_insert(&mut self, x: ObjId, l: ObjId) -> JsResult<Vec<ObjId>> {
let items = self.xml_list(l)?.items.clone();
let mut items = self.xml_list(l)?.items.clone();
let bytes = XML_REF_BYTES.saturating_mul(items.len());
self.rooted(&[Value::Object(x), Value::Object(l)], |i| i.xml_charge(bytes))?;
let roots = [Value::Object(x), Value::Object(l)];
self.rooted(&roots, |i| i.xml_charge(bytes))?;
let len = self.children_len(x)?;
for (j, &item) in items.iter().enumerate() {
self.place_fuel(len + j, item)?;
for (j, item) in items.iter_mut().enumerate() {
*item = self.child_for_item(*item, &roots)?;
self.place_fuel(len + j, *item)?;
}
Ok(items)
}
@@ -1009,6 +1020,17 @@ impl Interp {
Ok(())
}
/// Puts a new text node holding `s` at position `at` of `x`.
fn insert_text(&mut self, x: ObjId, at: usize, s: &Str) -> JsResult<()> {
let t = self.new_text_node(s.clone(), Some(x))?;
self.xml_keep(t);
self.xml_charge(XML_REF_BYTES)?;
let node = self.xml_node_mut(x)?;
let at = at.min(node.children.len());
node.children.insert(at, t);
Ok(())
}
/// [[Insert]] (ECMA-357 section 9.1.1.11).
pub(crate) fn xml_insert(&mut self, x: ObjId, at: usize, c: &Content) -> JsResult<()> {
if self.xml_kind(x)? != XmlKind::Element {
@@ -1016,13 +1038,10 @@ impl Interp {
}
self.check_attach(x, c)?;
match c {
Content::Text(s) => {
let t = self.new_text_node(s.clone(), Some(x))?;
self.xml_keep(t);
self.xml_charge(XML_REF_BYTES)?;
let node = self.xml_node_mut(x)?;
let at = at.min(node.children.len());
node.children.insert(at, t);
Content::Text(s) => self.insert_text(x, at, s)?,
Content::Node(v) if self.xml_kind(*v)? == XmlKind::Attribute => {
let value = self.xml_node(*v)?.value.clone();
self.insert_text(x, at, &value)?;
}
Content::Node(v) => {
self.insert_node(x, at, *v)?;
@@ -1473,11 +1492,21 @@ impl Interp {
Content::List(l) => {
let c = self.list_shallow_copy(l)?;
self.xml_keep(c);
let items = self.xml_list(c)?.items.clone();
let place = match parent {
Some(parent) => self.index_in_parent(parent, item)?.map(|q| (parent, q)),
None => None,
};
let mut items = self.xml_list(c)?.items.clone();
if place.is_some() {
let roots = [Value::Object(x), Value::Object(c)];
for item in &mut items {
self.use_fuel()?;
*item = self.child_for_item(*item, &roots)?;
}
self.xml_list_mut(c)?.items.clone_from(&items);
}
self.xml_charge(XML_REF_BYTES.saturating_mul(items.len()))?;
if let Some(parent) = parent
&& let Some(q) = self.index_in_parent(parent, item)?
{
if let Some((parent, q)) = place {
self.xml_replace(parent, q, &Content::List(c))?;
}
self.xml_list_mut(x)?.items.splice(i..=i, items);
+44
View File
@@ -911,6 +911,50 @@ fn matches_are_replaced_in_document_order() {
truth("var x = <a><c/></a>; x.b = 'v'; x.b == 'v' && x.*.length() === 2 && x.b[0].parent() === x");
}
#[test]
fn an_attribute_put_among_children_goes_in_as_text_and_stays_on_its_owner() {
let setup = "var y = <y p='v'/>; var x = <x><b/></x>;";
let check = "x.b.length() === 0 && x.children().length() === 1 && x.children()[0].nodeKind() === 'text' && x.toString() === 'v' \
&& y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y";
for edit in ["x.replace(0, y.@p)", "x.replace(0, y.@p[0])", "x.b = y.@p", "x.setChildren(y.@p)", "x.*[0] = y.@p"] {
truth(&format!("{setup} {edit}; {check}"));
}
let owner = "y.@p.length() === 1 && y.@p.parent() === y && y.@p[0].parent() === y";
for (edit, k) in [
("x.prependChild(y.@p)", "0"),
("x.prependChild(y.@p[0])", "0"),
("x.appendChild(y.@p)", "1"),
("x.appendChild(y.@p[0])", "1"),
("x.insertChildAfter(x.b[0], y.@p)", "1"),
("x.insertChildBefore(x.b[0], y.@p[0])", "0"),
] {
truth(&format!(
"{setup} {edit}; x.children().length() === 2 && x.children()[{k}].nodeKind() === 'text' && x.children()[{k}] == 'v' && {owner}"
));
}
}
#[test]
fn a_list_assigned_attributes_holds_the_text_nodes_in_the_tree() {
truth(
"var y = <y p='v' q='w'/>; var x = <x><b/></x>; var l = x.*; l[0] = y.@*; \
l.length() === 2 && l[0].nodeKind() === 'text' && l[0].parent() === x && l[1].nodeKind() === 'text' && l[1].parent() === x \
&& x.toString() === 'vw' && y.@*.length() === 2",
);
}
#[test]
fn an_attribute_list_goes_in_as_text_at_the_depth_limit() {
let open = "<a>".repeat(7);
let close = "</a>".repeat(7);
let src = format!(
"var x = new XML('{open}<a/>{close}'); var c = x; while (c.a.length()) c = c.a[0]; \
var y = <y p='v'/>; c.prependChild(y.@p); c.toString() === 'v'"
);
let mut realm = Realm::with_options(Options { max_depth: 8, ..e4x() }, Limits::default());
assert_eq!(realm.run(&src), Ok(JsValue::Bool(true)));
}
#[test]
fn a_limit_during_normalize_does_not_repeat_text() {
let setup = "var x = <a/>; x.appendChild('p'); x.appendChild('q'); x.appendChild('r');";