mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 22:21:59 +00:00
Scripts: namespace scopes and edits count against the heap limit
The namespace list that inScopeNamespaces(), namespace(), namespaceDeclarations(), the XPath namespace axis, XPath prefixes and XPath name() collect from a node and its ancestors was built in Rust buffers the heap limit did not see, so a node with many namespaces in scope let a script grow past its limit. The scope now holds the bytes of its list and prefix table until it is dropped, and namespaceDeclarations() also holds its ancestor table and the declarations it returns. The queried node stays rooted while the scope is built, and each declaration is read in place instead of copying every node's namespace list. Other copies in the same files are held against the limit too: - addNamespace(), setNamespace(), setName() and removeNamespace() read an element's attributes one at a time instead of copying the list, and removeNamespace() holds its walk stack. - child(index), comments(), text(), elements(), processingInstructions() and normalize() on an XMLList hold the copy of its items, and replace() holds the copy of the children it matched. - XPath name() holds the units of a prefixed name it builds. - The XML, XMLList, Namespace and QName constructors, isXMLName(), attribute(), namespace(), propertyIsEnumerable(), setName(), setLocalName(), addNamespace(), removeNamespace() and setNamespace() reserve the bytes of the flat copy of a long (rope) string argument before they make it, and attribute() reserves the copy of a name without its leading @. - QName.prototype.toString() reserves its result before it builds it.
This commit is contained in:
+185
-86
@@ -8,8 +8,8 @@ use crate::builtins::{arg, constructor, data, method, native};
|
||||
use crate::heap::{
|
||||
Class, ENUMERABLE, Key, NativeFn, NativeKind, OBJECT_BYTES, ObjId, Object, PROP_BYTES, Prop, WRITABLE, XmlKind, XmlName, XmlNs, key, string_bytes,
|
||||
};
|
||||
use crate::interp::{Abrupt, Interp, JsResult};
|
||||
use crate::string_builtins::{new_array, set_element};
|
||||
use crate::interp::{Abrupt, HeldBytes, Interp, JsResult};
|
||||
use crate::string_builtins::{flat_charged, new_array, push_held, reserve_units, set_element};
|
||||
use crate::value::{Str, Value};
|
||||
use crate::xml::{
|
||||
Access, Content, NameRef, RESERVED_NS_BINDING, STAR, Xv, canonical_index, is_ncname, is_script_xml_name, ns_binding_allowed, xml_name_writable,
|
||||
@@ -184,6 +184,13 @@ fn as_qname(interp: &Interp, v: &Value) -> Option<(XmlName, bool)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The code units of `v` as a string. A rope is flattened only after the bytes of its copy are
|
||||
/// reserved.
|
||||
fn str_arg(interp: &mut Interp, v: &Value) -> JsResult<Key> {
|
||||
let s = interp.to_str(v)?;
|
||||
flat_charged(interp, &s)
|
||||
}
|
||||
|
||||
/// The namespace `new Namespace(...)` makes from its arguments (ECMA-357 section 13.2.2).
|
||||
fn build_namespace(interp: &mut Interp, args: &[Value]) -> JsResult<XmlNs> {
|
||||
match args {
|
||||
@@ -197,7 +204,7 @@ fn build_namespace(interp: &mut Interp, args: &[Value]) -> JsResult<XmlNs> {
|
||||
{
|
||||
return Ok(XmlNs { prefix: name.prefix, uri: u });
|
||||
}
|
||||
let u = interp.to_str(uri)?.flat();
|
||||
let u = str_arg(interp, uri)?;
|
||||
let prefix = if u.is_empty() { Some(empty()) } else { None };
|
||||
Ok(XmlNs { prefix, uri: u })
|
||||
}
|
||||
@@ -205,7 +212,7 @@ fn build_namespace(interp: &mut Interp, args: &[Value]) -> JsResult<XmlNs> {
|
||||
let qname_uri = as_qname(interp, uri).and_then(|(n, _)| n.uri);
|
||||
let u = match qname_uri {
|
||||
Some(u) => u,
|
||||
None => interp.to_str(uri)?.flat(),
|
||||
None => str_arg(interp, uri)?,
|
||||
};
|
||||
if u.is_empty() {
|
||||
if matches!(prefix_value, Value::Undefined) || interp.to_str(prefix_value)?.len() == 0 {
|
||||
@@ -216,7 +223,7 @@ fn build_namespace(interp: &mut Interp, args: &[Value]) -> JsResult<XmlNs> {
|
||||
if matches!(prefix_value, Value::Undefined) {
|
||||
return Ok(XmlNs { prefix: None, uri: u });
|
||||
}
|
||||
let p = interp.to_str(prefix_value)?.flat();
|
||||
let p = str_arg(interp, prefix_value)?;
|
||||
Ok(XmlNs { prefix: is_ncname(&p).then_some(p), uri: u })
|
||||
}
|
||||
}
|
||||
@@ -256,7 +263,7 @@ fn build_qname(interp: &mut Interp, args: &[Value]) -> JsResult<(XmlName, bool)>
|
||||
} else if matches!(name_value, Value::Undefined) {
|
||||
empty()
|
||||
} else {
|
||||
interp.to_str(&name_value)?.flat()
|
||||
str_arg(interp, &name_value)?
|
||||
};
|
||||
let (uri, prefix) = match namespace {
|
||||
Some(Value::Undefined) | None => {
|
||||
@@ -290,7 +297,16 @@ fn qname_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> Js
|
||||
let Some((name, attribute)) = as_qname(interp, &this) else {
|
||||
return Err(interp.type_error("QName.prototype.toString needs a QName"));
|
||||
};
|
||||
let mut units: Vec<u16> = Vec::new();
|
||||
let uri_len = match &name.uri {
|
||||
None => 3,
|
||||
Some(u) if !u.is_empty() => u.len().saturating_add(2),
|
||||
_ => 0,
|
||||
};
|
||||
let len = usize::from(attribute).saturating_add(uri_len).saturating_add(name.local.len());
|
||||
let bytes = string_bytes(len);
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
let mut units: Vec<u16> = Vec::with_capacity(len);
|
||||
if attribute {
|
||||
units.push(0x40);
|
||||
}
|
||||
@@ -303,9 +319,6 @@ fn qname_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> Js
|
||||
_ => {}
|
||||
}
|
||||
units.extend_from_slice(&name.local);
|
||||
let bytes = string_bytes(units.len());
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
Ok(Value::String(Str::from_units(units)))
|
||||
}
|
||||
|
||||
@@ -324,7 +337,7 @@ fn to_xml(interp: &mut Interp, v: &Value) -> JsResult<ObjId> {
|
||||
Err(interp.type_error("an XMLList converts to XML only when it has one item"))
|
||||
}
|
||||
Xv::Other => {
|
||||
let text = if v.is_nullish() { empty() } else { interp.to_str(v)?.flat() };
|
||||
let text = if v.is_nullish() { empty() } else { str_arg(interp, v)? };
|
||||
interp.xml_from_text(&text)
|
||||
}
|
||||
}
|
||||
@@ -355,7 +368,7 @@ fn xml_list_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool)
|
||||
Xv::Node(n) => i.list_push(out, n)?,
|
||||
_ if v.is_nullish() => {}
|
||||
_ => {
|
||||
let text = i.to_str(&v)?.flat();
|
||||
let text = str_arg(i, &v)?;
|
||||
for node in i.xml_parse(&text)? {
|
||||
i.list_push(out, node)?;
|
||||
}
|
||||
@@ -366,7 +379,7 @@ fn xml_list_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool)
|
||||
}
|
||||
|
||||
fn is_xml_name(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = interp.to_str(&arg(args, 0))?.flat();
|
||||
let s = str_arg(interp, &arg(args, 0))?;
|
||||
Ok(Value::Bool(is_script_xml_name(&s)))
|
||||
}
|
||||
|
||||
@@ -440,10 +453,16 @@ fn this_id(interp: &mut Interp, this: &Value) -> JsResult<ObjId> {
|
||||
}
|
||||
}
|
||||
|
||||
fn nodes_of(interp: &mut Interp, this: &Value) -> JsResult<Vec<ObjId>> {
|
||||
/// The nodes of `this`: the node itself, or a copy of the items of an XMLList, whose bytes `held`
|
||||
/// holds against the heap limit first.
|
||||
fn nodes_of(interp: &mut Interp, held: &mut HeldBytes, this: &Value) -> JsResult<Vec<ObjId>> {
|
||||
match interp.classify(this) {
|
||||
Xv::Node(n) => Ok(vec![n]),
|
||||
Xv::List(l) => Ok(interp.xml_list(l)?.items.clone()),
|
||||
Xv::List(l) => interp.rooted(std::slice::from_ref(this), |interp| {
|
||||
let count = interp.xml_list(l)?.items.len();
|
||||
held.add(interp, count.saturating_mul(size_of::<ObjId>()))?;
|
||||
Ok(interp.xml_list(l)?.items.clone())
|
||||
}),
|
||||
Xv::Other => Err(not_xml_method(interp)),
|
||||
}
|
||||
}
|
||||
@@ -464,8 +483,14 @@ fn attribute_name(interp: &mut Interp, v: &Value) -> JsResult<NameRef> {
|
||||
if matches!(v, Value::Undefined | Value::Null | Value::Bool(_) | Value::Number(_)) {
|
||||
return Err(interp.type_error("the value is not an XML attribute name"));
|
||||
}
|
||||
let local = interp.to_str(v)?.flat();
|
||||
let local = local.strip_prefix(&[0x40u16]).map_or_else(|| local.clone(), Rc::from);
|
||||
let local = str_arg(interp, v)?;
|
||||
let local = match local.strip_prefix(&[0x40u16]) {
|
||||
Some(rest) => {
|
||||
reserve_units(interp, rest.len())?;
|
||||
Rc::from(rest)
|
||||
}
|
||||
None => local,
|
||||
};
|
||||
let uri = if *local == *STAR { None } else { Some(empty()) };
|
||||
Ok(NameRef { uri, local, attribute: true })
|
||||
}
|
||||
@@ -510,7 +535,8 @@ fn child(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<
|
||||
match interp.xml_access(&p)? {
|
||||
Access::Name(n) => Ok(Value::Object(interp.xml_select(id, &n)?)),
|
||||
Access::Index(i) => {
|
||||
let nodes = nodes_of(interp, &this)?;
|
||||
let mut held = HeldBytes::new(interp);
|
||||
let nodes = nodes_of(interp, &mut held, &this)?;
|
||||
interp.rooted(std::slice::from_ref(&this), |it| {
|
||||
let out = it.new_xml_list(Some(id), None)?;
|
||||
it.xml_keep(out);
|
||||
@@ -535,7 +561,8 @@ fn select_children(
|
||||
keep: impl Fn(&Interp, ObjId) -> JsResult<bool>,
|
||||
) -> JsResult<Value> {
|
||||
let id = this_id(interp, this)?;
|
||||
let nodes = nodes_of(interp, this)?;
|
||||
let mut held = HeldBytes::new(interp);
|
||||
let nodes = nodes_of(interp, &mut held, this)?;
|
||||
interp.rooted(std::slice::from_ref(this), |it| {
|
||||
let out = it.new_xml_list(Some(id), target_name)?;
|
||||
it.xml_keep(out);
|
||||
@@ -666,34 +693,64 @@ fn has_simple_content(interp: &mut Interp, this: Value, _: &[Value], _: bool) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// The namespaces in scope at `x`, nearest declarations first, one for each prefix. Takes one unit
|
||||
/// of fuel for each node it climbs through and for each declaration it reads.
|
||||
pub(crate) fn scope_of(interp: &mut Interp, x: Option<ObjId>) -> JsResult<Vec<XmlNs>> {
|
||||
let mut out: Vec<XmlNs> = Vec::new();
|
||||
let mut seen: HashSet<Option<Key>> = HashSet::new();
|
||||
let mut cur = x;
|
||||
while let Some(y) = cur {
|
||||
interp.use_fuel()?;
|
||||
let (list, parent) = {
|
||||
let node = interp.xml_node(y)?;
|
||||
(node.ns.clone(), node.parent)
|
||||
};
|
||||
for ns in list {
|
||||
interp.use_fuel()?;
|
||||
if seen.insert(ns.prefix.clone()) {
|
||||
out.push(ns);
|
||||
}
|
||||
}
|
||||
cur = parent;
|
||||
}
|
||||
Ok(out)
|
||||
/// The namespaces in scope at a node, nearest declarations first, one for each prefix. The bytes of
|
||||
/// the list and of the table of prefixes that built it stay held against the heap limit until the
|
||||
/// scope is dropped.
|
||||
pub(crate) struct Scope {
|
||||
pub(crate) list: Vec<XmlNs>,
|
||||
_held: HeldBytes,
|
||||
}
|
||||
|
||||
fn namespaces_array(interp: &mut Interp, list: Vec<XmlNs>) -> JsResult<Value> {
|
||||
/// Holds the bytes of a full hash table of `len` entries of `entry` bytes each, and returns how
|
||||
/// many more entries to reserve room for. Each new entry is counted four times with a control
|
||||
/// byte, which covers the empty slots the table keeps and the old table while it is rehashed.
|
||||
fn grow_table(interp: &mut Interp, held: &mut HeldBytes, len: usize, entry: usize) -> JsResult<usize> {
|
||||
let more = len.max(4);
|
||||
held.add(interp, more.saturating_mul(4).saturating_mul(entry.saturating_add(1)).saturating_add(16))?;
|
||||
Ok(more)
|
||||
}
|
||||
|
||||
/// The namespace `y` declares at index `i`.
|
||||
fn declared_at(interp: &mut Interp, y: ObjId, i: usize) -> JsResult<Option<XmlNs>> {
|
||||
Ok(interp.xml_node(y)?.ns.get(i).cloned())
|
||||
}
|
||||
|
||||
/// The namespaces in scope at `x`, nearest declarations first, one for each prefix. Takes one unit
|
||||
/// of fuel for each node it climbs through and for each declaration it reads.
|
||||
pub(crate) fn scope_of(interp: &mut Interp, x: Option<ObjId>) -> JsResult<Scope> {
|
||||
let root: Vec<Value> = x.map(Value::Object).into_iter().collect();
|
||||
interp.rooted(&root, |interp| {
|
||||
let mut held = HeldBytes::new(interp);
|
||||
let mut out: Vec<XmlNs> = Vec::new();
|
||||
let mut seen: HashSet<Option<Key>> = HashSet::new();
|
||||
let mut cur = x;
|
||||
while let Some(y) = cur {
|
||||
interp.use_fuel()?;
|
||||
let mut i = 0;
|
||||
while let Some(ns) = declared_at(interp, y, i)? {
|
||||
interp.use_fuel()?;
|
||||
i += 1;
|
||||
if seen.contains(&ns.prefix) {
|
||||
continue;
|
||||
}
|
||||
if seen.len() == seen.capacity() {
|
||||
let more = grow_table(interp, &mut held, seen.len(), size_of::<Option<Key>>())?;
|
||||
seen.reserve(more);
|
||||
}
|
||||
seen.insert(ns.prefix.clone());
|
||||
push_held(interp, &mut held, &mut out, ns)?;
|
||||
}
|
||||
cur = interp.xml_node(y)?.parent;
|
||||
}
|
||||
Ok(Scope { list: out, _held: held })
|
||||
})
|
||||
}
|
||||
|
||||
fn namespaces_array(interp: &mut Interp, list: &[XmlNs]) -> JsResult<Value> {
|
||||
interp.rooted(&[], |i| {
|
||||
let a = new_array(i);
|
||||
for (at, ns) in list.into_iter().enumerate() {
|
||||
let v = new_namespace(i, ns)?;
|
||||
for (at, ns) in list.iter().enumerate() {
|
||||
let v = new_namespace(i, ns.clone())?;
|
||||
set_element(i, a, at, v)?;
|
||||
}
|
||||
Ok(Value::Object(a))
|
||||
@@ -702,28 +759,46 @@ fn namespaces_array(interp: &mut Interp, list: Vec<XmlNs>) -> JsResult<Value> {
|
||||
|
||||
fn in_scope_namespaces(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_node(interp, &this)?;
|
||||
let list = scope_of(interp, Some(x))?;
|
||||
namespaces_array(interp, list)
|
||||
let scope = scope_of(interp, Some(x))?;
|
||||
namespaces_array(interp, &scope.list)
|
||||
}
|
||||
|
||||
fn namespace_declarations(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_node(interp, &this)?;
|
||||
if interp.xml_kind(x)? != XmlKind::Element {
|
||||
return namespaces_array(interp, Vec::new());
|
||||
return namespaces_array(interp, &[]);
|
||||
}
|
||||
let parent = interp.xml_node(x)?.parent;
|
||||
let ancestors: HashMap<Option<Key>, Key> = scope_of(interp, parent)?.into_iter().map(|a| (a.prefix, a.uri)).collect();
|
||||
let declared: Vec<XmlNs> = interp.xml_node(x)?.ns.iter().filter(|ns| ancestors.get(&ns.prefix) != Some(&ns.uri)).cloned().collect();
|
||||
namespaces_array(interp, declared)
|
||||
interp.rooted(&[Value::Object(x)], |interp| {
|
||||
let parent = interp.xml_node(x)?.parent;
|
||||
let scope = scope_of(interp, parent)?;
|
||||
let mut held = HeldBytes::new(interp);
|
||||
let mut ancestors: HashMap<Option<Key>, Key> = HashMap::new();
|
||||
for a in &scope.list {
|
||||
if ancestors.len() == ancestors.capacity() {
|
||||
let more = grow_table(interp, &mut held, ancestors.len(), size_of::<(Option<Key>, Key)>())?;
|
||||
ancestors.reserve(more);
|
||||
}
|
||||
ancestors.insert(a.prefix.clone(), a.uri.clone());
|
||||
}
|
||||
let mut declared: Vec<XmlNs> = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(ns) = declared_at(interp, x, i)? {
|
||||
i += 1;
|
||||
if ancestors.get(&ns.prefix) != Some(&ns.uri) {
|
||||
push_held(interp, &mut held, &mut declared, ns)?;
|
||||
}
|
||||
}
|
||||
namespaces_array(interp, &declared)
|
||||
})
|
||||
}
|
||||
|
||||
fn namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_node(interp, &this)?;
|
||||
let scope = scope_of(interp, Some(x))?;
|
||||
if let Some(p) = args.first() {
|
||||
let p = interp.to_str(p)?.flat();
|
||||
return match scope.into_iter().find(|ns| ns.prefix.as_deref() == Some(&*p)) {
|
||||
Some(ns) => new_namespace(interp, ns),
|
||||
let p = str_arg(interp, p)?;
|
||||
return match scope.list.iter().find(|ns| ns.prefix.as_deref() == Some(&*p)) {
|
||||
Some(ns) => new_namespace(interp, ns.clone()),
|
||||
None => Ok(Value::Undefined),
|
||||
};
|
||||
}
|
||||
@@ -734,9 +809,10 @@ fn namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsRes
|
||||
let Some(name) = node.name.clone() else { return Ok(Value::Null) };
|
||||
let uri = name.uri.clone().unwrap_or_else(empty);
|
||||
let found = scope
|
||||
.list
|
||||
.iter()
|
||||
.find(|ns| ns.uri == uri && ns.prefix == name.prefix)
|
||||
.or_else(|| scope.iter().find(|ns| ns.uri == uri))
|
||||
.or_else(|| scope.list.iter().find(|ns| ns.uri == uri))
|
||||
.cloned()
|
||||
.unwrap_or(XmlNs { prefix: name.prefix, uri });
|
||||
new_namespace(interp, found)
|
||||
@@ -777,7 +853,8 @@ fn node_kind(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult
|
||||
}
|
||||
|
||||
fn normalize(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
for node in nodes_of(interp, &this)? {
|
||||
let mut held = HeldBytes::new(interp);
|
||||
for node in nodes_of(interp, &mut held, &this)? {
|
||||
if interp.xml_kind(node)? == XmlKind::Element {
|
||||
interp.node_normalize(node)?;
|
||||
}
|
||||
@@ -806,7 +883,7 @@ fn parent(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Va
|
||||
|
||||
fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let p = arg(args, 0);
|
||||
let s = interp.to_str(&p)?.flat();
|
||||
let s = str_arg(interp, &p)?;
|
||||
match interp.classify(&this) {
|
||||
Xv::Node(_) => Ok(Value::Bool(canonical_index(&s) == Some(0))),
|
||||
Xv::List(l) => Ok(Value::Bool(canonical_index(&s).is_some_and(|i| (i as usize) < interp.list_len(l).unwrap_or(0)))),
|
||||
@@ -814,6 +891,11 @@ fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: b
|
||||
}
|
||||
}
|
||||
|
||||
/// The attribute `x` holds at index `i`.
|
||||
fn attr_at(interp: &mut Interp, x: ObjId, i: usize) -> JsResult<Option<ObjId>> {
|
||||
Ok(interp.xml_node(x)?.attrs.get(i).copied())
|
||||
}
|
||||
|
||||
/// [[AddInScopeNamespace]] (ECMA-357 section 9.1.1.13).
|
||||
fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> {
|
||||
if interp.xml_kind(x)? != XmlKind::Element {
|
||||
@@ -823,16 +905,14 @@ fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> {
|
||||
if !ns_binding_allowed(&prefix, &ns.uri) {
|
||||
return Err(interp.type_error(RESERVED_NS_BINDING));
|
||||
}
|
||||
let node = interp.xml_node(x)?;
|
||||
if prefix.is_empty() && node.name.as_ref().and_then(|n| n.uri.as_ref()).is_some_and(|u| u.is_empty()) {
|
||||
if prefix.is_empty() && interp.xml_node(x)?.name.as_ref().and_then(|n| n.uri.as_ref()).is_some_and(|u| u.is_empty()) {
|
||||
return Ok(());
|
||||
}
|
||||
if !node.ns.iter().any(|m| m.prefix.as_ref() == Some(&prefix)) {
|
||||
if !interp.xml_node(x)?.ns.iter().any(|m| m.prefix.as_ref() == Some(&prefix)) {
|
||||
let bytes = crate::heap::XML_NS_BYTES;
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
}
|
||||
let attrs = interp.xml_node(x)?.attrs.clone();
|
||||
let node = interp.xml_node_mut(x)?;
|
||||
node.ns.retain(|m| m.prefix.as_ref() != Some(&prefix));
|
||||
node.ns.push(ns);
|
||||
@@ -841,8 +921,10 @@ fn add_in_scope(interp: &mut Interp, x: ObjId, ns: XmlNs) -> JsResult<()> {
|
||||
{
|
||||
name.prefix = None;
|
||||
}
|
||||
for a in attrs {
|
||||
let mut i = 0;
|
||||
while let Some(a) = attr_at(interp, x, i)? {
|
||||
interp.use_fuel()?;
|
||||
i += 1;
|
||||
if let Some(name) = &mut interp.xml_node_mut(a)?.name
|
||||
&& name.prefix.as_ref() == Some(&prefix)
|
||||
{
|
||||
@@ -915,25 +997,31 @@ fn insert_child_before(interp: &mut Interp, this: Value, args: &[Value], _: bool
|
||||
insert_relative(interp, this, args, false)
|
||||
}
|
||||
|
||||
/// One element of a `removeNamespace` walk; true when the walk goes on to its children.
|
||||
fn remove_ns_from(interp: &mut Interp, x: ObjId, ns: &XmlNs) -> JsResult<bool> {
|
||||
let node = interp.xml_node(x)?;
|
||||
let attrs = node.attrs.clone();
|
||||
if node.name.as_ref().and_then(|n| n.uri.as_ref()) == Some(&ns.uri) {
|
||||
return Ok(false);
|
||||
/// Whether the name of element `x` or of one of its attributes is in `ns`, which keeps `ns` on `x`
|
||||
/// and its descendants in a `removeNamespace` walk.
|
||||
fn ns_in_use(interp: &mut Interp, x: ObjId, ns: &XmlNs) -> JsResult<bool> {
|
||||
if interp.xml_node(x)?.name.as_ref().and_then(|n| n.uri.as_ref()) == Some(&ns.uri) {
|
||||
return Ok(true);
|
||||
}
|
||||
for a in attrs {
|
||||
let mut i = 0;
|
||||
while let Some(a) = attr_at(interp, x, i)? {
|
||||
interp.use_fuel()?;
|
||||
i += 1;
|
||||
if interp.xml_node(a)?.name.as_ref().and_then(|n| n.uri.as_ref()) == Some(&ns.uri) {
|
||||
return Ok(false);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Removes `ns` from the declarations of element `x`.
|
||||
fn remove_ns_from(interp: &mut Interp, x: ObjId, ns: &XmlNs) -> JsResult<()> {
|
||||
let list = &mut interp.xml_node_mut(x)?.ns;
|
||||
match &ns.prefix {
|
||||
None => list.retain(|m| m.uri != ns.uri),
|
||||
Some(p) => list.retain(|m| !(m.uri == ns.uri && m.prefix.as_ref() == Some(p))),
|
||||
}
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
@@ -942,23 +1030,31 @@ fn remove_namespace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -
|
||||
return Ok(this);
|
||||
}
|
||||
let ns = build_namespace(interp, std::slice::from_ref(&arg(args, 0)))?;
|
||||
if !remove_ns_from(interp, x, &ns)? {
|
||||
return Ok(this);
|
||||
}
|
||||
let mut stack = vec![(x, 0usize)];
|
||||
while let Some(&(node, next)) = stack.last() {
|
||||
interp.use_fuel()?;
|
||||
let Some(c) = interp.xml_node(node)?.children.get(next).copied() else {
|
||||
stack.pop();
|
||||
continue;
|
||||
};
|
||||
if let Some(frame) = stack.last_mut() {
|
||||
frame.1 += 1;
|
||||
interp.rooted(&[Value::Object(x)], |interp| {
|
||||
if ns_in_use(interp, x, &ns)? {
|
||||
return Ok(());
|
||||
}
|
||||
if interp.xml_kind(c)? == XmlKind::Element && remove_ns_from(interp, c, &ns)? {
|
||||
stack.push((c, 0));
|
||||
let mut held = HeldBytes::new(interp);
|
||||
let mut stack = Vec::new();
|
||||
push_held(interp, &mut held, &mut stack, (x, 0usize))?;
|
||||
remove_ns_from(interp, x, &ns)?;
|
||||
while let Some(&(node, next)) = stack.last() {
|
||||
interp.use_fuel()?;
|
||||
let Some(c) = interp.xml_node(node)?.children.get(next).copied() else {
|
||||
stack.pop();
|
||||
continue;
|
||||
};
|
||||
if let Some(frame) = stack.last_mut() {
|
||||
frame.1 += 1;
|
||||
}
|
||||
if interp.xml_kind(c)? != XmlKind::Element || ns_in_use(interp, c, &ns)? {
|
||||
continue;
|
||||
}
|
||||
push_held(interp, &mut held, &mut stack, (c, 0))?;
|
||||
remove_ns_from(interp, c, &ns)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
@@ -989,6 +1085,9 @@ fn replace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResul
|
||||
};
|
||||
let matched = i.xml_select(x, &NameRef { attribute: false, ..n })?;
|
||||
i.xml_keep(matched);
|
||||
let mut held = HeldBytes::new(i);
|
||||
let count = i.xml_list(matched)?.items.len();
|
||||
held.add(i, count.saturating_mul(size_of::<ObjId>()))?;
|
||||
let items = i.xml_list(matched)?.items.clone();
|
||||
if items.is_empty() {
|
||||
return Ok(());
|
||||
@@ -1020,7 +1119,7 @@ fn set_local_name(interp: &mut Interp, this: Value, args: &[Value], _: bool) ->
|
||||
let v = arg(args, 0);
|
||||
let local = match as_qname(interp, &v) {
|
||||
Some((n, _)) => n.local,
|
||||
None => interp.to_str(&v)?.flat(),
|
||||
None => str_arg(interp, &v)?,
|
||||
};
|
||||
checked_local(interp, &local)?;
|
||||
if kind == XmlKind::Attribute
|
||||
|
||||
+139
-1
@@ -351,6 +351,10 @@ fn the_xml_methods_behave_as_specified() {
|
||||
"var x = <a xmlns:p='u'><b/></a>; x.b[0].inScopeNamespaces().length === 1 && x.namespaceDeclarations()[0].prefix === 'p' && x.b[0].namespaceDeclarations().length === 0",
|
||||
);
|
||||
truth("var x = <a xmlns:p='u'/>; x.namespace('p').uri === 'u' && x.namespace('q') === undefined && x.namespace().uri === ''");
|
||||
truth(
|
||||
"var x = <a xmlns:p='u' xmlns:q='v'><b xmlns:p='w' xmlns:q='v'><c/></b></a>; var c = x.b.c[0]; var s = c.inScopeNamespaces(); var d = x.b[0].namespaceDeclarations(); \
|
||||
s.length === 2 && c.namespace('p').uri === 'w' && c.namespace('q').uri === 'v' && c.namespace('z') === undefined && d.length === 1 && d[0].prefix === 'p' && d[0].uri === 'w'",
|
||||
);
|
||||
truth("var x = <a/>; x.addNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
|
||||
truth("var x = <a xmlns:p='u'/>; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p') === undefined");
|
||||
truth("var x = <a xmlns:p='u' p:q='1'/>; x.removeNamespace(new Namespace('p', 'u')); x.namespace('p').uri === 'u'");
|
||||
@@ -1243,7 +1247,12 @@ fn comparing_xml_values_is_charged_to_the_heap() {
|
||||
|
||||
/// Runs `setup` with no limit, then `action` in the same realm, and returns how `action` ended.
|
||||
fn after_setup(setup: &str, action: &str) -> Result<JsValue, RunErrorKind> {
|
||||
let mut r = realm();
|
||||
after_setup_with(e4x(), setup, action)
|
||||
}
|
||||
|
||||
/// [`after_setup`] in a realm with `options`.
|
||||
fn after_setup_with(options: Options, setup: &str, action: &str) -> Result<JsValue, RunErrorKind> {
|
||||
let mut r = Realm::with_options(options, Limits::default());
|
||||
let interp = r.interp();
|
||||
let global = interp.global;
|
||||
assert!(crate::builtins::method(interp, global, "squeeze", squeeze, 1).is_ok());
|
||||
@@ -1267,6 +1276,135 @@ fn namespace_declarations_and_qname_strings_are_charged_to_the_heap() {
|
||||
assert_eq!(after_setup(long, action), heap);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_scopes_are_charged_to_the_heap() {
|
||||
let heap = Err(RunErrorKind::Limit(LimitKind::Heap));
|
||||
let setup = "var x = <r><b><c/></b></r>; for (var i = 0; i < 2000; i++) x.addNamespace(new Namespace('p' + i, 'u' + i)); \
|
||||
var b = x.b[0]; b.c[0].setNamespace(new Namespace('u7'));";
|
||||
for (action, ample) in [
|
||||
("b.namespace('p1999').uri", JsValue::String("u1999".into())),
|
||||
("b.namespaceDeclarations().length", JsValue::Number(0.0)),
|
||||
("b.inScopeNamespaces().length", JsValue::Number(2000.0)),
|
||||
("x.xpath('b[namespace::p1999]').length()", JsValue::Number(1.0)),
|
||||
("b.xpath('p5:c').length()", JsValue::Number(0.0)),
|
||||
("b.xpath(\"*[name() = 'p7:c']\").length()", JsValue::Number(1.0)),
|
||||
] {
|
||||
assert_eq!(after_setup(setup, &format!("squeeze(16384); {action}")), heap, "{action}");
|
||||
assert_eq!(after_setup(setup, action), Ok(ample.clone()), "{action}");
|
||||
// The scope reads the same bindings after the script has left garbage to collect.
|
||||
let churn = format!("squeeze(2000000); for (var i = 0; i < 3000; i++) [i]; {action}");
|
||||
assert_eq!(after_setup(setup, &churn), Ok(ample), "{action}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_edits_read_attributes_in_place_and_charge_their_walk() {
|
||||
let heap = Err(RunErrorKind::Limit(LimitKind::Heap));
|
||||
let wide = "var s = '<r xmlns:q=\"v\"'; for (var i = 0; i < 4000; i++) s += ' a' + i + '=\"v\"'; var x = new XML(s + '/>'); \
|
||||
var n = new Namespace('q', 'v'); var m = new Namespace('s', 'w');";
|
||||
let deep = "var s = '<r xmlns:q=\"v\">'; for (var i = 0; i < 2000; i++) s += '<d>'; for (var i = 0; i < 2000; i++) s += '</d>'; \
|
||||
var x = new XML(s + '</r>'); var n = new Namespace('q', 'v');";
|
||||
let removed = "x.removeNamespace(n); x.namespace('q') === undefined";
|
||||
// An element's attributes are read where they are, so 4,000 of them need no room; the walk of a
|
||||
// deep tree is held against the limit.
|
||||
for (tree, setup, action, ample, squeezed) in [
|
||||
("wide", wide, "x.addNamespace(m); x.namespace('s').uri", JsValue::String("w".into()), false),
|
||||
("wide", wide, removed, JsValue::Bool(true), false),
|
||||
("deep", deep, removed, JsValue::Bool(true), true),
|
||||
] {
|
||||
let options = Options { max_depth: 1_000_000, ..e4x() };
|
||||
let want = if squeezed { heap.clone() } else { Ok(ample.clone()) };
|
||||
assert_eq!(after_setup_with(options.clone(), setup, &format!("squeeze(4096); {action}")), want, "{tree}: {action}");
|
||||
assert_eq!(after_setup_with(options, setup, action), Ok(ample), "{tree}: {action}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xml_list_methods_charge_their_copies_to_the_heap() {
|
||||
let heap = Err(RunErrorKind::Limit(LimitKind::Heap));
|
||||
let setup = "var x = <r/>; for (var i = 0; i < 4000; i++) x.appendChild(<a/>); var l = x.a;";
|
||||
for (action, ample) in [
|
||||
("l.child(0).length()", JsValue::Number(0.0)),
|
||||
("l.comments().length()", JsValue::Number(0.0)),
|
||||
("l.normalize().length()", JsValue::Number(4000.0)),
|
||||
] {
|
||||
assert_eq!(after_setup(setup, &format!("squeeze(4096); {action}")), heap, "{action}");
|
||||
assert_eq!(after_setup(setup, action), Ok(ample), "{action}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The least spare heap, found by bisection, at which `action` ends without the heap limit after
|
||||
/// `setup`.
|
||||
fn least_spare(setup: &str, action: &str) -> usize {
|
||||
let (mut lo, mut hi) = (0usize, 1usize << 22);
|
||||
assert!(after_setup(setup, &format!("squeeze({hi}); {action}")).is_ok(), "{action}");
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
match after_setup(setup, &format!("squeeze({mid}); {action}")) {
|
||||
Ok(_) => hi = mid,
|
||||
Err(RunErrorKind::Limit(LimitKind::Heap)) => lo = mid + 1,
|
||||
Err(e) => panic!("{action}: {e:?}"),
|
||||
}
|
||||
}
|
||||
lo
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_charges_its_copy_of_the_matches_to_the_heap() {
|
||||
let setup = "var x = <r/>; for (var i = 0; i < 4000; i++) x.appendChild(<a/>);";
|
||||
let action = "x.replace('a', 'z'); x.children().length()";
|
||||
let select = least_spare(setup, "x.a.length()");
|
||||
let replace = least_spare(setup, action);
|
||||
assert!(replace >= select + 4000 * 4, "select {select}, replace {replace}");
|
||||
assert_eq!(after_setup(setup, action), Ok(JsValue::Number(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xpath_qualified_names_are_charged_to_the_heap() {
|
||||
let setup = "var s = 'a'; for (var i = 0; i < 14; i++) s += s; var x = <r xmlns:p='u'><c/></r>; var c = x.c[0]; \
|
||||
c.setLocalName(s); c.setNamespace(new Namespace('u'));";
|
||||
let action = "x.xpath('*[string-length(name()) = 16386]').length()";
|
||||
assert_eq!(after_setup(setup, &format!("squeeze(16384); {action}")), Err(RunErrorKind::Limit(LimitKind::Heap)));
|
||||
assert_eq!(after_setup(setup, action), Ok(JsValue::Number(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xml_methods_charge_the_strings_they_flatten() {
|
||||
let heap = Err(RunErrorKind::Limit(LimitKind::Heap));
|
||||
// `s` and `t` are ropes of 32,768 and 49,152 units; `f` is a flat string that starts with `@`.
|
||||
let setup = "var s = 'a'; for (var i = 0; i < 15; i++) s += s; var t = 'a b'; for (var i = 0; i < 14; i++) t += t; \
|
||||
var f = '@' + s; isXMLName(f); var x = <r><c/></r>; var c = x.c[0];";
|
||||
let mut wrong = Vec::new();
|
||||
for (action, ample) in [
|
||||
("x.addNamespace(s); 1", JsValue::Number(1.0)),
|
||||
("new Namespace(t, 'u').prefix", JsValue::Undefined),
|
||||
("c.setName(s); 1", JsValue::Number(1.0)),
|
||||
("isXMLName(s)", JsValue::Bool(true)),
|
||||
("x.attribute(s).length()", JsValue::Number(0.0)),
|
||||
("x.attribute(f).length()", JsValue::Number(0.0)),
|
||||
("x.namespace(s) === undefined", JsValue::Bool(true)),
|
||||
("x.propertyIsEnumerable(s)", JsValue::Bool(false)),
|
||||
("c.setLocalName(s); 1", JsValue::Number(1.0)),
|
||||
] {
|
||||
let squeezed = after_setup(setup, &format!("squeeze(16384); {action}"));
|
||||
if squeezed != heap {
|
||||
wrong.push(format!("{action} with 16384 spare: {squeezed:?}"));
|
||||
}
|
||||
assert_eq!(after_setup(setup, action), Ok(ample), "{action}");
|
||||
}
|
||||
// Parsing a rope needs room for its flat copy on top of what parsing the same flat text needs.
|
||||
let text = "var k = 'x'; for (var i = 0; i < 15; i++) k += k; var s = '<a><!--' + k + '--></a>'; \
|
||||
var g = '<a><!--' + k + '--></a>'; isXMLName(g);";
|
||||
for ctor in ["XML", "XMLList"] {
|
||||
let rope = least_spare(text, &format!("new {ctor}(s); 1"));
|
||||
let flat = least_spare(text, &format!("new {ctor}(g); 1"));
|
||||
if rope < flat + 32768 {
|
||||
wrong.push(format!("new {ctor} needs {rope} spare for a rope and {flat} for flat text"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{wrong:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_interrupt_stops_serialization() {
|
||||
let mut r = realm();
|
||||
|
||||
+27
-23
@@ -1472,13 +1472,14 @@ impl<'p> Ev<'p> {
|
||||
let xml = key("xml");
|
||||
let mut at = 0u32;
|
||||
let mut has_xml = false;
|
||||
for ns in scope_of(interp, Some(*x))? {
|
||||
let Some(prefix) = ns.prefix else { continue };
|
||||
let scope = scope_of(interp, Some(*x))?;
|
||||
for ns in &scope.list {
|
||||
let Some(prefix) = ns.prefix.clone() else { continue };
|
||||
if prefix.is_empty() && ns.uri.is_empty() {
|
||||
continue;
|
||||
}
|
||||
has_xml |= prefix == xml;
|
||||
self.add(interp, out, axis, test, XNode::Ns { owner: *x, at, prefix, uri: ns.uri })?;
|
||||
self.add(interp, out, axis, test, XNode::Ns { owner: *x, at, prefix, uri: ns.uri.clone() })?;
|
||||
at += 1;
|
||||
}
|
||||
if !has_xml {
|
||||
@@ -1867,7 +1868,7 @@ impl<'p> Ev<'p> {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
Some(XNode::Node(x)) => node_name(interp, f, x)?,
|
||||
Some(XNode::Node(x)) => return node_name(interp, f, x),
|
||||
};
|
||||
Ok(Text::plain(interp, units))
|
||||
}
|
||||
@@ -2179,7 +2180,7 @@ impl<'p> Ev<'p> {
|
||||
let uri = if units_are(p, "xml") {
|
||||
key(XML_URI)
|
||||
} else {
|
||||
match scope.iter().find(|ns| ns.prefix.as_ref() == Some(p)) {
|
||||
match scope.list.iter().find(|ns| ns.prefix.as_ref() == Some(p)) {
|
||||
Some(ns) if !ns.uri.is_empty() => ns.uri.clone(),
|
||||
_ => {
|
||||
let shown = String::from_utf16_lossy(p);
|
||||
@@ -2213,24 +2214,26 @@ impl<'p> Ev<'p> {
|
||||
}
|
||||
}
|
||||
|
||||
fn node_name(interp: &mut Interp, f: Func, x: ObjId) -> JsResult<Key> {
|
||||
fn node_name(interp: &mut Interp, f: Func, x: ObjId) -> JsResult<Text> {
|
||||
let node = interp.xml_node(x)?;
|
||||
let (kind, parent) = (node.kind, node.parent);
|
||||
let Some(name) = node.name.clone() else { return Ok(empty()) };
|
||||
match (f, kind) {
|
||||
(_, XmlKind::Text | XmlKind::Comment) => Ok(empty()),
|
||||
(Func::NamespaceUri, XmlKind::Pi) => Ok(empty()),
|
||||
(Func::NamespaceUri, _) => Ok(Rc::from(name_uri(&name))),
|
||||
(Func::LocalName, _) | (_, XmlKind::Pi) => Ok(name.local),
|
||||
_ => qualified_name(interp, x, parent, &name, kind == XmlKind::Attribute),
|
||||
}
|
||||
let Some(name) = node.name.clone() else { return Ok(Text::plain(interp, empty())) };
|
||||
let units = match (f, kind) {
|
||||
(_, XmlKind::Text | XmlKind::Comment) => empty(),
|
||||
(Func::NamespaceUri, XmlKind::Pi) => empty(),
|
||||
(Func::NamespaceUri, _) => Rc::from(name_uri(&name)),
|
||||
(Func::LocalName, _) | (_, XmlKind::Pi) => name.local,
|
||||
_ => return qualified_name(interp, x, parent, &name, kind == XmlKind::Attribute),
|
||||
};
|
||||
Ok(Text::plain(interp, units))
|
||||
}
|
||||
|
||||
/// An element's or attribute's name as a QName, with a prefix declared for its namespace.
|
||||
fn qualified_name(interp: &mut Interp, x: ObjId, parent: Option<ObjId>, name: &XmlName, attribute: bool) -> JsResult<Key> {
|
||||
/// An element's or attribute's name as a QName, with a prefix declared for its namespace. The units
|
||||
/// of a prefixed name are held against the heap limit while the text lives.
|
||||
fn qualified_name(interp: &mut Interp, x: ObjId, parent: Option<ObjId>, name: &XmlName, attribute: bool) -> JsResult<Text> {
|
||||
let uri = name_uri(name);
|
||||
if uri.is_empty() {
|
||||
return Ok(name.local.clone());
|
||||
return Ok(Text::plain(interp, name.local.clone()));
|
||||
}
|
||||
let prefix = if units_are(uri, XML_URI) {
|
||||
key("xml")
|
||||
@@ -2240,17 +2243,18 @@ fn qualified_name(interp: &mut Interp, x: ObjId, parent: Option<ObjId>, name: &X
|
||||
_ => {
|
||||
let scope = scope_of(interp, if attribute { parent } else { Some(x) })?;
|
||||
let usable = |ns: &XmlNs| *ns.uri == *uri && ns.prefix.as_ref().is_some_and(|p| !(attribute && p.is_empty()));
|
||||
scope.into_iter().find(usable).and_then(|ns| ns.prefix).unwrap_or_else(empty)
|
||||
scope.list.iter().find(|ns| usable(ns)).and_then(|ns| ns.prefix.clone()).unwrap_or_else(empty)
|
||||
}
|
||||
}
|
||||
};
|
||||
if prefix.is_empty() {
|
||||
return Ok(name.local.clone());
|
||||
return Ok(Text::plain(interp, name.local.clone()));
|
||||
}
|
||||
let mut units = prefix.to_vec();
|
||||
units.push(COLON);
|
||||
units.extend_from_slice(&name.local);
|
||||
Ok(Rc::from(units))
|
||||
let mut buf = TextBuf::new(interp);
|
||||
buf.push(interp, &prefix)?;
|
||||
buf.push(interp, &[COLON])?;
|
||||
buf.push(interp, &name.local)?;
|
||||
Ok(buf.finish())
|
||||
}
|
||||
|
||||
/// The first index where `needle` occurs in `hay`. Every position tried takes fuel.
|
||||
|
||||
@@ -811,6 +811,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,379 of 1,411 pass for `String`, 298 of 298 for `Number`, 954 of 988 for `RegExp` and 8 of 8 for `Date`. The others expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- With `Options::e4x` set, an E4X name standing alone (`@a`, `@ns::a`, `@[expr]`, `ns::a`, `*`) inside `with` or a filter predicate can be assigned, with `=` or a compound operator, incremented, decremented, deleted and used as a `for` or `for each` variable, as well as read. Each of these uses the nearest XML or XMLList value in scope that holds the name and evaluates the name once. When no XML value in scope holds the name, the read or write throws a `ReferenceError` before the right-hand side runs and creates no child or attribute, as ECMA-357 section 11.1 (https://ecma-international.org/publications-and-standards/standards/ecma-357/) specifies, so a filter such as `list.(@id == '1')` throws when an item has no `@id` and no outer XML value holds one. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- `vectorcraft-js` runs ECMAScript for XML (ECMA-357, 2nd edition, https://ecma-international.org/publications-and-standards/standards/ecma-357/) when `Options::e4x` is set, which it is not by default: XML and XMLList literals with embedded `{}` expressions, the `..`, `.@`, `.*`, `.()` and `::` selectors, assignment and `delete` on selected nodes, `for each`, `default xml namespace`, and the `XML`, `XMLList`, `Namespace` and `QName` objects with their methods and settings. `xml.xpath(expression[, variables])` evaluates an XPath 1.0 expression (https://www.w3.org/TR/1999/REC-xpath-19991116/) with all 13 axes and the 27 core functions, and returns the selected nodes as an XMLList in document order without repeats. Each property of `variables` binds the variable of its name, an XMLList runs the expression on each of its elements in turn, and a prefix in a name test resolves through the context element's in-scope namespaces. An expression that breaks the grammar gives a `SyntaxError`; an unknown function, a wrong argument count, an unbound prefix or variable, and a result that is not a node-set or that holds the root or a namespace node give a `TypeError`. `id()` selects nothing, and evaluation uses no native stack, takes fuel and counts against the heap limit. `<!DOCTYPE>` in an XML string gives a `SyntaxError`. An XML declaration at the start of an XML string, after optional white space, is checked against XML 1.0's grammar (https://www.w3.org/TR/xml/#NT-XMLDecl): a version of `1.` and digits, then an optional encoding name and an optional standalone `yes` or `no`, in that order; any other declaration, or one with a character XML does not allow, gives a `SyntaxError`. The parser takes fuel for the length of every name, white space run and declaration it scans, and for the text it searched when a quote, CDATA section, comment or processing instruction is not closed. Parsing, serializing, comparing and copying trees use no native stack, and each takes fuel and counts against the heap limit. Inserting a node into itself or its descendants throws a `TypeError` before the tree changes, a literal or XML string nested past `Options::max_depth` gives a `SyntaxError`, and a tree built past that depth stops at the heap limit. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- Scripts: `inScopeNamespaces()`, `namespace()`, `namespaceDeclarations()`, the XPath `namespace` axis, prefixes in an XPath expression and the qualified names `name()` gives count the namespaces they collect from a node and its ancestors against the heap limit, so a node with many namespaces in scope stops the script at the heap limit instead of growing past it. `addNamespace()`, `setNamespace()`, `setName()` and `removeNamespace()` read an element's attributes in place instead of copying them, and `removeNamespace()` counts its walk of the tree; `child()` by index, `comments()`, `text()`, `elements()`, `processingInstructions()` and `normalize()` on an XMLList count their copy of its items; `replace()` counts its copy of the children it matched; and `name()` counts the prefixed name it builds. The `XML`, `XMLList`, `Namespace` and `QName` constructors, `isXMLName()`, `attribute()`, `namespace()`, `propertyIsEnumerable()`, `setName()`, `setLocalName()`, `addNamespace()`, `removeNamespace()` and `setNamespace()` count the copy they make when they flatten a long string argument, and `QName.prototype.toString()` counts its result before it builds it. Tests: `crates/js/src/xml_tests.rs`.
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
|
||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||
|
||||
Reference in New Issue
Block a user