avm2: Implement XML.insertChildAfter and XML.insertChildBefore

This commit is contained in:
sleepycatcoding 2023-08-27 00:42:43 +03:00 committed by Nathan Adams
parent aa28e51a94
commit fbaa02e295
6 changed files with 255 additions and 4 deletions

View File

@ -13,7 +13,10 @@ use quick_xml::{
use crate::{avm2::TObject, xml::custom_unescape}; use crate::{avm2::TObject, xml::custom_unescape};
use super::{ use super::{
error::type_error, object::E4XOrXml, string::AvmString, Activation, Error, Multiname, Value, error::{make_error_1118, type_error},
object::E4XOrXml,
string::AvmString,
Activation, Error, Multiname, Value,
}; };
use crate::string::{WStr, WString}; use crate::string::{WStr, WString};
@ -295,6 +298,147 @@ impl<'gc> E4XNode<'gc> {
Ok(()) Ok(())
} }
// ECMA-357 9.1.1.4 [[DeleteByIndex]] (P)
pub fn delete_by_index(&self, index: usize, activation: &mut Activation<'_, 'gc>) {
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else {
return;
};
// 2.a. If i is less than x.[[Length]]
if index < children.len() {
// 2.a.i. If x has a property with name P
// 2.a.i.2. Remove the property with the name P from x
let element = children.remove(index);
// 2.a.i.1. Let x[P].[[Parent]] = null
element.set_parent(None, activation.gc());
}
}
// ECMA-357 9.1.1.11 [[Insert]] (P, V)
pub fn insert(
&self,
index: usize,
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
// 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return
if !matches!(*self.kind(), E4XNodeKind::Element { .. }) {
return Ok(());
}
// 4. If Type(V) is XML and (V is x or an ancestor of x) throw an Error exception
if let Some(xml) = value.as_object().and_then(|x| x.as_xml_object()) {
if self.ancestors().any(|x| E4XNode::ptr_eq(x, *xml.node())) {
return Err(make_error_1118(activation));
}
}
// 10. If Type(V) is XMLList
if let Some(list) = value.as_object().and_then(|x| x.as_xml_list_object()) {
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else {
unreachable!("E4XNode should be of element kind");
};
// 10.a. For j = 0 to V.[[Length-1]]
for (child_index, child) in list.children().iter().enumerate() {
let child = child.node();
// 10.a.i. V[j].[[Parent]] = x
child.set_parent(Some(*self), activation.gc());
// 10.a.ii. x[i + j] = V[j]
children.insert(index + child_index, *child);
}
// 11. Else
} else {
// 11.a. Call the [[Replace]] method of x with arguments i and V
if let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) {
// NOTE: Make room for the replace operation.
children.insert(index, E4XNode::dummy(activation.gc()))
}
self.replace(index, value, activation)?;
}
// 12. Return
Ok(())
}
// ECMA-357 9.1.1.12 [[Replace]] (P, V)
pub fn replace(
&self,
index: usize,
value: Value<'gc>,
activation: &mut Activation<'_, 'gc>,
) -> Result<(), Error<'gc>> {
// 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return
if !matches!(*self.kind(), E4XNodeKind::Element { .. }) {
return Ok(());
}
// 5. If Type(V) is XML and V.[[Class]] ∈ {"element", "comment", "processing-instruction", "text"}
if let Some(xml) = value.as_object().and_then(|x| x.as_xml_object()) {
if matches!(*xml.node().kind(), E4XNodeKind::Attribute(_)) {
return Ok(());
}
// 5.a. If V.[[Class]] is “element” and (V is x or an ancestor of x) throw an Error exception
if matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) {
if self.ancestors().any(|x| E4XNode::ptr_eq(x, *xml.node())) {
return Err(make_error_1118(activation));
}
}
// 5.b. Let V.[[Parent]] = x
xml.node().set_parent(Some(*self), activation.gc());
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else {
unreachable!("E4XNode should be of element kind");
};
// 5.c. If x has a property with name P
if let Some(node) = children.get(index) {
// 5.c.i. Let x[P].[[Parent]] = null
node.set_parent(None, activation.gc());
}
// 5.d. Let x[P] = V
if index >= children.len() {
children.push(*xml.node());
} else {
children[index] = *xml.node();
}
// 6. Else if Type(V) is XMLList
} else if let Some(_) = value.as_object().and_then(|x| x.as_xml_list_object()) {
// 6.a. Call the [[DeleteByIndex]] method of x with argument P
self.delete_by_index(index, activation);
// 6.b. Call the [[Insert]] method of x with arguments P and V
self.insert(index, value, activation)?;
// 7. Else
} else {
// 7.a. Let s = ToString(V)
let s: AvmString<'_> = value.coerce_to_string(activation)?;
// 7.b. Create a new XML object t with t.[[Class]] = "text", t.[[Parent]] = x and t.[[Value]] = s
let text_node = E4XNode::text(activation.gc(), s, Some(*self));
let E4XNodeKind::Element { children, .. } = &mut *self.kind_mut(activation.gc()) else {
unreachable!("E4XNode should be of element kind");
};
// 7.c. If x has a property with name P
if let Some(node) = children.get(index) {
// 7.c.i. Let x[P].[[Parent]] = null
node.set_parent(None, activation.gc());
}
// 7.d. Let the value of property P of x be t
if index >= children.len() {
children.push(text_node);
} else {
children[index] = text_node;
}
}
Ok(())
}
/// Parses a value provided to `XML`/`XMLList` into a list of nodes. /// Parses a value provided to `XML`/`XMLList` into a list of nodes.
/// The caller is responsible for validating that the number of top-level nodes /// The caller is responsible for validating that the number of top-level nodes
/// is correct (for XML, there should be exactly one.) /// is correct (for XML, there should be exactly one.)

View File

@ -78,6 +78,8 @@ package {
AS3 native function length():int; AS3 native function length():int;
AS3 native function comments():XMLList; AS3 native function comments():XMLList;
AS3 native function processingInstructions(name:String = "*"):XMLList; AS3 native function processingInstructions(name:String = "*"):XMLList;
AS3 native function insertChildAfter(child1:Object, child2:Object):*;
AS3 native function insertChildBefore(child1:Object, child2:Object):*;
AS3 function valueOf():XML { AS3 function valueOf():XML {
return this; return this;
@ -219,6 +221,16 @@ package {
return self.AS3::processingInstructions(name); return self.AS3::processingInstructions(name);
} }
prototype.insertChildAfter = function(child1:Object, child2:Object):* {
var self:XML = this;
return self.AS3::insertChildAfter(child1, child2);
}
prototype.insertChildBefore = function(child1:Object, child2:Object):* {
var self:XML = this;
return self.AS3::insertChildBefore(child1, child2);
}
public static const length:int = 1; public static const length:int = 1;
} }
} }

View File

@ -485,3 +485,101 @@ pub fn processing_instructions<'gc>(
Ok(XmlListObject::new(activation, nodes, Some(xml.into())).into()) Ok(XmlListObject::new(activation, nodes, Some(xml.into())).into())
} }
// ECMA-357 13.4.4.18 XML.prototype.insertChildAfter (child1, child2)
pub fn insert_child_after<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let xml = this.as_xml_object().unwrap();
let child1 = args.try_get_object(activation, 0);
let child2 = args.get_object(activation, 1, "child2")?;
// 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return
if !matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) {
return Ok(Value::Undefined);
}
// 3. Else if Type(child1) is XML
if let Some(child1) = child1.and_then(|x| x.as_xml_object()) {
// NOTE: We fetch the index separately to avoid borrowing errors.
let index = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() {
// 3.a. For i = 0 to x.[[Length]]-1
// 3.a.i. If x[i] is the same object as child1
children
.iter()
.position(|x| E4XNode::ptr_eq(*x, *child1.node()))
} else {
None
};
if let Some(index) = index {
// 3.a.i.1. Call the [[Insert]] method of x with arguments ToString(i + 1) and child2
xml.node().insert(index + 1, child2.into(), activation)?;
// 3.a.i.2. Return x
return Ok(xml.into());
}
// 2. If (child1 == null)
} else {
// 2.a. Call the [[Insert]] method of x with arguments "0" and child2
xml.node().insert(0, child2.into(), activation)?;
// 2.b. Return x
return Ok(xml.into());
}
// 4. Return
return Ok(Value::Undefined);
}
// ECMA-357 13.4.4.19 XML.prototype.insertChildBefore (child1, child2)
pub fn insert_child_before<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let xml = this.as_xml_object().unwrap();
let child1 = args.try_get_object(activation, 0);
let child2 = args.get_object(activation, 1, "child2")?;
// 1. If x.[[Class]] ∈ {"text", "comment", "processing-instruction", "attribute"}, return
if !matches!(*xml.node().kind(), E4XNodeKind::Element { .. }) {
return Ok(Value::Undefined);
}
// 3. Else if Type(child1) is XML
if let Some(child1) = child1.and_then(|x| x.as_xml_object()) {
// NOTE: We fetch the index separately to avoid borrowing errors.
let index = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() {
// 3.a. For i = 0 to x.[[Length]]-1
// 3.a.i. If x[i] is the same object as child1
children
.iter()
.position(|x| E4XNode::ptr_eq(*x, *child1.node()))
} else {
None
};
if let Some(index) = index {
// 3.a.i.1. Call the [[Insert]] method of x with arguments ToString(i) and child2
xml.node().insert(index, child2.into(), activation)?;
// 3.a.i.2. Return x
return Ok(xml.into());
}
// 2. If (child1 == null)
} else {
let length = if let E4XNodeKind::Element { children, .. } = &*xml.node().kind() {
children.len()
} else {
0
};
// 2.a. Call the [[Insert]] method of x with arguments ToString(x.[[Length]]) and child2
xml.node().insert(length, child2.into(), activation)?;
// 2.b. Return x
return Ok(xml.into());
}
// 4. Return
return Ok(Value::Undefined);
}

View File

@ -1,2 +1 @@
num_ticks = 1 num_ticks = 1
known_failure = true

View File

@ -1,2 +1 @@
num_ticks = 1 num_ticks = 1
known_failure = true

View File

@ -1,2 +1 @@
num_ticks = 1 num_ticks = 1
known_failure = true