avm1: Merge `XmlDocument` into `XmlObject`
As `XmlDocument` and `XmlObject` had 1-to-1 relation, and `XmlDocument` is already tightly coupled with AVM1, there's no good reason for them being separate objects. This brings us one step closer towards an XML implementation hosted completely in AVM1. A future PR will merge `XmlNode` into `XmlNodeObject` in a similar manner.
This commit is contained in:
parent
aee98d81c8
commit
ad2b8ea007
|
@ -28,7 +28,7 @@ use crate::avm1::object::xml_node_object::XmlNodeObject;
|
|||
use crate::avm1::object::xml_object::XmlObject;
|
||||
use crate::avm1::{AvmString, ScriptObject, SoundObject, StageObject, Value};
|
||||
use crate::display_object::DisplayObject;
|
||||
use crate::xml::{XmlDocument, XmlNode};
|
||||
use crate::xml::XmlNode;
|
||||
use gc_arena::{Collect, MutationContext};
|
||||
use ruffle_macros::enum_trait_object;
|
||||
use std::fmt::Debug;
|
||||
|
@ -532,7 +532,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
}
|
||||
|
||||
/// Get the underlying XML document for this object, if it exists.
|
||||
fn as_xml(&self) -> Option<XmlDocument<'gc>> {
|
||||
fn as_xml(&self) -> Option<XmlObject<'gc>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::error::Error;
|
||||
use crate::avm1::object::TObject;
|
||||
use crate::avm1::{Object, ScriptObject};
|
||||
use crate::avm1::property::Attribute;
|
||||
use crate::avm1::{Object, ScriptObject, TObject};
|
||||
use crate::impl_custom_object;
|
||||
use crate::xml::{XmlDocument, XmlNode};
|
||||
use crate::string::{AvmString, WStr, WString};
|
||||
use crate::xml::{Error as XmlError, ParseError, XmlNode};
|
||||
use gc_arena::{Collect, GcCell, MutationContext};
|
||||
use quick_xml::events::{BytesDecl, Event};
|
||||
use quick_xml::{Error as QXError, Reader, Writer};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// A ScriptObject that is inherently tied to an XML document.
|
||||
#[derive(Clone, Copy, Collect)]
|
||||
|
@ -18,27 +22,241 @@ pub struct XmlObject<'gc>(GcCell<'gc, XmlObjectData<'gc>>);
|
|||
#[collect(no_drop)]
|
||||
pub struct XmlObjectData<'gc> {
|
||||
base: ScriptObject<'gc>,
|
||||
document: XmlDocument<'gc>,
|
||||
|
||||
/// The root node of the XML document.
|
||||
root: XmlNode<'gc>,
|
||||
|
||||
/// Whether or not the document has a document declaration.
|
||||
has_xmldecl: bool,
|
||||
|
||||
/// The XML version string, if set.
|
||||
version: String,
|
||||
|
||||
/// The XML document encoding, if set.
|
||||
encoding: Option<String>,
|
||||
|
||||
/// The XML standalone flag, if set.
|
||||
standalone: Option<String>,
|
||||
|
||||
/// The XML doctype, if set.
|
||||
doctype: Option<AvmString<'gc>>,
|
||||
|
||||
/// The document's ID map.
|
||||
///
|
||||
/// When nodes are parsed into the document by way of `parseXML` or the
|
||||
/// document constructor, they get put into this object, which is accessible
|
||||
/// through the document's `idMap`.
|
||||
id_map: ScriptObject<'gc>,
|
||||
|
||||
/// The last parse error encountered, if any.
|
||||
last_parse_error: Option<ParseError>,
|
||||
}
|
||||
|
||||
impl<'gc> XmlObject<'gc> {
|
||||
/// Construct a new XML document and object pair.
|
||||
pub fn empty(gc_context: MutationContext<'gc, '_>, proto: Option<Object<'gc>>) -> Self {
|
||||
let document = XmlDocument::new(gc_context);
|
||||
let mut root = XmlNode::new_document_root(gc_context);
|
||||
let object = Self(GcCell::allocate(
|
||||
gc_context,
|
||||
XmlObjectData {
|
||||
base: ScriptObject::object(gc_context, proto),
|
||||
document,
|
||||
root,
|
||||
has_xmldecl: false,
|
||||
version: "1.0".to_string(),
|
||||
encoding: None,
|
||||
standalone: None,
|
||||
doctype: None,
|
||||
id_map: ScriptObject::bare_object(gc_context),
|
||||
last_parse_error: None,
|
||||
},
|
||||
));
|
||||
|
||||
document
|
||||
.as_node()
|
||||
.introduce_script_object(gc_context, object.into());
|
||||
|
||||
root.introduce_script_object(gc_context, object.into());
|
||||
object
|
||||
}
|
||||
|
||||
/// Yield the document in node form.
|
||||
pub fn as_node(self) -> XmlNode<'gc> {
|
||||
self.0.read().root
|
||||
}
|
||||
|
||||
/// Retrieve the first DocType node in the document.
|
||||
pub fn doctype(self) -> Option<AvmString<'gc>> {
|
||||
self.0.read().doctype
|
||||
}
|
||||
|
||||
/// Replace the contents of this document with the result of parsing a string.
|
||||
///
|
||||
/// This method does not yet actually remove existing node contents.
|
||||
///
|
||||
/// If `process_entity` is `true`, then entities will be processed by this
|
||||
/// function. Invalid or unrecognized entities will cause parsing to fail
|
||||
/// with an `Err`.
|
||||
pub fn replace_with_str(
|
||||
&mut self,
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
data: &WStr,
|
||||
process_entity: bool,
|
||||
ignore_white: bool,
|
||||
) -> Result<(), XmlError> {
|
||||
let data_utf8 = data.to_utf8_lossy();
|
||||
let mut parser = Reader::from_str(&data_utf8);
|
||||
let mut buf = Vec::new();
|
||||
let mut open_tags = vec![self.as_node()];
|
||||
|
||||
self.clear_parse_error(activation.context.gc_context);
|
||||
|
||||
loop {
|
||||
let event =
|
||||
self.log_parse_result(activation.context.gc_context, parser.read_event(&mut buf))?;
|
||||
|
||||
match event {
|
||||
Event::Start(bs) => {
|
||||
let child = XmlNode::from_start_event(
|
||||
activation.context.gc_context,
|
||||
bs,
|
||||
process_entity,
|
||||
)?;
|
||||
self.update_id_map(activation, child);
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
open_tags.push(child);
|
||||
}
|
||||
Event::Empty(bs) => {
|
||||
let child = XmlNode::from_start_event(
|
||||
activation.context.gc_context,
|
||||
bs,
|
||||
process_entity,
|
||||
)?;
|
||||
self.update_id_map(activation, child);
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
}
|
||||
Event::End(_) => {
|
||||
open_tags.pop();
|
||||
}
|
||||
Event::Text(bt) | Event::CData(bt) => {
|
||||
let child = XmlNode::text_from_text_event(
|
||||
activation.context.gc_context,
|
||||
bt,
|
||||
process_entity,
|
||||
)?;
|
||||
if child.node_value() != Some(AvmString::default())
|
||||
&& (!ignore_white || !child.is_whitespace_text())
|
||||
{
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
}
|
||||
}
|
||||
Event::DocType(bt) => {
|
||||
// TODO: `quick-xml` is case-insensitive for DOCTYPE declarations,
|
||||
// but it doesn't expose the whole tag, only the inner portion of it.
|
||||
// Flash is also case-insensitive for DOCTYPE declarations. However,
|
||||
// the `.docTypeDecl` property preserves the original case.
|
||||
let mut doctype = WString::from_buf(b"<!DOCTYPE".to_vec());
|
||||
doctype.push_str(WStr::from_units(bt.escaped()));
|
||||
doctype.push_byte(b'>');
|
||||
self.0.write(activation.context.gc_context).doctype =
|
||||
Some(AvmString::new(activation.context.gc_context, doctype));
|
||||
}
|
||||
Event::Decl(bd) => {
|
||||
let mut self_write = self.0.write(activation.context.gc_context);
|
||||
|
||||
self_write.has_xmldecl = true;
|
||||
self_write.version = String::from_utf8(bd.version()?.into_owned())?;
|
||||
self_write.encoding = if let Some(encoding) = bd.encoding() {
|
||||
Some(String::from_utf8(encoding?.into_owned())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self_write.standalone = if let Some(standalone) = bd.standalone() {
|
||||
Some(String::from_utf8(standalone?.into_owned())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a string matching the XML document declaration, if there is
|
||||
/// one.
|
||||
pub fn xmldecl_string(self) -> Result<Option<String>, XmlError> {
|
||||
let self_read = self.0.read();
|
||||
|
||||
if self_read.has_xmldecl {
|
||||
let mut result = Vec::new();
|
||||
let mut writer = Writer::new(Cursor::new(&mut result));
|
||||
let bd = BytesDecl::new(
|
||||
self_read.version.as_bytes(),
|
||||
self_read.encoding.as_ref().map(|s| s.as_bytes()),
|
||||
self_read.standalone.as_ref().map(|s| s.as_bytes()),
|
||||
);
|
||||
writer.write_event(Event::Decl(bd))?;
|
||||
|
||||
Ok(Some(String::from_utf8(result)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the script object for the document's `idMap` property.
|
||||
pub fn id_map(self) -> ScriptObject<'gc> {
|
||||
self.0.read().id_map
|
||||
}
|
||||
|
||||
/// Update the ID map object with a given new node.
|
||||
fn update_id_map(&mut self, activation: &mut Activation<'_, 'gc, '_>, mut node: XmlNode<'gc>) {
|
||||
if let Some(id) = node.attribute_value(WStr::from_units(b"id")) {
|
||||
self.0
|
||||
.write(activation.context.gc_context)
|
||||
.id_map
|
||||
.define_value(
|
||||
activation.context.gc_context,
|
||||
id,
|
||||
node.script_object(activation).into(),
|
||||
Attribute::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log the result of an XML parse, saving the error for later inspection
|
||||
/// if necessary.
|
||||
fn log_parse_result<O>(
|
||||
self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
maybe_error: Result<O, QXError>,
|
||||
) -> Result<O, ParseError> {
|
||||
match maybe_error {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
let new_error = ParseError::from_quickxml_error(e);
|
||||
|
||||
self.0.write(gc_context).last_parse_error = Some(new_error.clone());
|
||||
|
||||
Err(new_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last parse error within this document, if any.
|
||||
pub fn last_parse_error(self) -> Option<ParseError> {
|
||||
self.0.read().last_parse_error.clone()
|
||||
}
|
||||
|
||||
/// Clear the previous parse error.
|
||||
fn clear_parse_error(self, gc_context: MutationContext<'gc, '_>) {
|
||||
self.0.write(gc_context).last_parse_error = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for XmlObject<'_> {
|
||||
|
@ -46,7 +264,7 @@ impl fmt::Debug for XmlObject<'_> {
|
|||
let this = self.0.read();
|
||||
f.debug_struct("XmlObject")
|
||||
.field("base", &this.base)
|
||||
.field("document", &this.document)
|
||||
.field("root", &self.0.read().root)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
@ -62,11 +280,11 @@ impl<'gc> TObject<'gc> for XmlObject<'gc> {
|
|||
Ok(Self::empty(activation.context.gc_context, Some(this)).into())
|
||||
}
|
||||
|
||||
fn as_xml(&self) -> Option<XmlDocument<'gc>> {
|
||||
Some(self.0.read().document)
|
||||
fn as_xml(&self) -> Option<XmlObject<'gc>> {
|
||||
Some(*self)
|
||||
}
|
||||
|
||||
fn as_xml_node(&self) -> Option<XmlNode<'gc>> {
|
||||
Some(self.0.read().document.as_node())
|
||||
Some(self.as_node())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
//! Garbage-collectable XML DOM impl
|
||||
|
||||
mod document;
|
||||
mod error;
|
||||
mod iterators;
|
||||
mod tree;
|
||||
|
||||
pub use document::XmlDocument;
|
||||
pub use error::Error;
|
||||
pub use error::ParseError;
|
||||
pub use tree::XmlNode;
|
||||
|
|
|
@ -1,266 +0,0 @@
|
|||
//! XML Document
|
||||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::property::Attribute;
|
||||
use crate::avm1::{ScriptObject, TObject};
|
||||
use crate::string::{AvmString, WStr, WString};
|
||||
use crate::xml::{Error, ParseError, XmlNode};
|
||||
use gc_arena::{Collect, GcCell, MutationContext};
|
||||
use quick_xml::events::{BytesDecl, Event};
|
||||
use quick_xml::{Error as QXError, Reader, Writer};
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// The entirety of an XML document.
|
||||
#[derive(Copy, Clone, Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct XmlDocument<'gc>(GcCell<'gc, XmlDocumentData<'gc>>);
|
||||
|
||||
#[derive(Clone, Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct XmlDocumentData<'gc> {
|
||||
/// The root node of the XML document.
|
||||
root: XmlNode<'gc>,
|
||||
|
||||
/// Whether or not the document has a document declaration.
|
||||
has_xmldecl: bool,
|
||||
|
||||
/// The XML version string, if set.
|
||||
version: String,
|
||||
|
||||
/// The XML document encoding, if set.
|
||||
encoding: Option<String>,
|
||||
|
||||
/// The XML standalone flag, if set.
|
||||
standalone: Option<String>,
|
||||
|
||||
/// The XML doctype, if set.
|
||||
doctype: Option<AvmString<'gc>>,
|
||||
|
||||
/// The document's ID map.
|
||||
///
|
||||
/// When nodes are parsed into the document by way of `parseXML` or the
|
||||
/// document constructor, they get put into this object, which is accessible
|
||||
/// through the document's `idMap`.
|
||||
id_map: ScriptObject<'gc>,
|
||||
|
||||
/// The last parse error encountered, if any.
|
||||
last_parse_error: Option<ParseError>,
|
||||
}
|
||||
|
||||
impl<'gc> XmlDocument<'gc> {
|
||||
/// Construct a new, empty XML document.
|
||||
pub fn new(mc: MutationContext<'gc, '_>) -> Self {
|
||||
Self(GcCell::allocate(
|
||||
mc,
|
||||
XmlDocumentData {
|
||||
root: XmlNode::new_document_root(mc),
|
||||
has_xmldecl: false,
|
||||
version: "1.0".to_string(),
|
||||
encoding: None,
|
||||
standalone: None,
|
||||
doctype: None,
|
||||
id_map: ScriptObject::bare_object(mc),
|
||||
last_parse_error: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Yield the document in node form.
|
||||
///
|
||||
/// If the document does not have a node, then this function will panic.
|
||||
pub fn as_node(self) -> XmlNode<'gc> {
|
||||
self.0.read().root
|
||||
}
|
||||
|
||||
/// Retrieve the first DocType node in the document.
|
||||
pub fn doctype(self) -> Option<AvmString<'gc>> {
|
||||
self.0.read().doctype
|
||||
}
|
||||
|
||||
/// Replace the contents of this document with the result of parsing a string.
|
||||
///
|
||||
/// This method does not yet actually remove existing node contents.
|
||||
///
|
||||
/// If `process_entity` is `true`, then entities will be processed by this
|
||||
/// function. Invalid or unrecognized entities will cause parsing to fail
|
||||
/// with an `Err`.
|
||||
pub fn replace_with_str(
|
||||
&mut self,
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
data: &WStr,
|
||||
process_entity: bool,
|
||||
ignore_white: bool,
|
||||
) -> Result<(), Error> {
|
||||
let data_utf8 = data.to_utf8_lossy();
|
||||
let mut parser = Reader::from_str(&data_utf8);
|
||||
let mut buf = Vec::new();
|
||||
let mut open_tags = vec![self.as_node()];
|
||||
|
||||
self.clear_parse_error(activation.context.gc_context);
|
||||
|
||||
loop {
|
||||
let event =
|
||||
self.log_parse_result(activation.context.gc_context, parser.read_event(&mut buf))?;
|
||||
|
||||
match event {
|
||||
Event::Start(bs) => {
|
||||
let child = XmlNode::from_start_event(
|
||||
activation.context.gc_context,
|
||||
bs,
|
||||
process_entity,
|
||||
)?;
|
||||
self.update_idmap(activation, child);
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
open_tags.push(child);
|
||||
}
|
||||
Event::Empty(bs) => {
|
||||
let child = XmlNode::from_start_event(
|
||||
activation.context.gc_context,
|
||||
bs,
|
||||
process_entity,
|
||||
)?;
|
||||
self.update_idmap(activation, child);
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
}
|
||||
Event::End(_) => {
|
||||
open_tags.pop();
|
||||
}
|
||||
Event::Text(bt) | Event::CData(bt) => {
|
||||
let child = XmlNode::text_from_text_event(
|
||||
activation.context.gc_context,
|
||||
bt,
|
||||
process_entity,
|
||||
)?;
|
||||
if child.node_value() != Some(AvmString::default())
|
||||
&& (!ignore_white || !child.is_whitespace_text())
|
||||
{
|
||||
open_tags
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.append_child(activation.context.gc_context, child)?;
|
||||
}
|
||||
}
|
||||
Event::DocType(bt) => {
|
||||
// TODO: `quick-xml` is case-insensitive for DOCTYPE declarations,
|
||||
// but it doesn't expose the whole tag, only the inner portion of it.
|
||||
// Flash is also case-insensitive for DOCTYPE declarations. However,
|
||||
// the `.docTypeDecl` property preserves the original case.
|
||||
let mut doctype = WString::from_buf(b"<!DOCTYPE".to_vec());
|
||||
doctype.push_str(WStr::from_units(bt.escaped()));
|
||||
doctype.push_byte(b'>');
|
||||
self.0.write(activation.context.gc_context).doctype =
|
||||
Some(AvmString::new(activation.context.gc_context, doctype));
|
||||
}
|
||||
Event::Decl(bd) => {
|
||||
let mut self_write = self.0.write(activation.context.gc_context);
|
||||
|
||||
self_write.has_xmldecl = true;
|
||||
self_write.version = String::from_utf8(bd.version()?.into_owned())?;
|
||||
self_write.encoding = if let Some(encoding) = bd.encoding() {
|
||||
Some(String::from_utf8(encoding?.into_owned())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self_write.standalone = if let Some(standalone) = bd.standalone() {
|
||||
Some(String::from_utf8(standalone?.into_owned())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a string matching the XML document declaration, if there is
|
||||
/// one.
|
||||
pub fn xmldecl_string(self) -> Result<Option<String>, Error> {
|
||||
let self_read = self.0.read();
|
||||
|
||||
if self_read.has_xmldecl {
|
||||
let mut result = Vec::new();
|
||||
let mut writer = Writer::new(Cursor::new(&mut result));
|
||||
let bd = BytesDecl::new(
|
||||
self_read.version.as_bytes(),
|
||||
self_read.encoding.as_ref().map(|s| s.as_bytes()),
|
||||
self_read.standalone.as_ref().map(|s| s.as_bytes()),
|
||||
);
|
||||
writer.write_event(Event::Decl(bd))?;
|
||||
|
||||
Ok(Some(String::from_utf8(result)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the script object for the document's `idMap` property.
|
||||
pub fn id_map(self) -> ScriptObject<'gc> {
|
||||
self.0.read().id_map
|
||||
}
|
||||
|
||||
/// Update the idmap object with a given new node.
|
||||
pub fn update_idmap(
|
||||
&mut self,
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
mut node: XmlNode<'gc>,
|
||||
) {
|
||||
if let Some(id) = node.attribute_value(WStr::from_units(b"id")) {
|
||||
self.0
|
||||
.write(activation.context.gc_context)
|
||||
.id_map
|
||||
.define_value(
|
||||
activation.context.gc_context,
|
||||
id,
|
||||
node.script_object(activation).into(),
|
||||
Attribute::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log the result of an XML parse, saving the error for later inspection
|
||||
/// if necessary.
|
||||
pub fn log_parse_result<O>(
|
||||
self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
maybe_error: Result<O, QXError>,
|
||||
) -> Result<O, ParseError> {
|
||||
match maybe_error {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
let new_error = ParseError::from_quickxml_error(e);
|
||||
|
||||
self.0.write(gc_context).last_parse_error = Some(new_error.clone());
|
||||
|
||||
Err(new_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last parse error within this document, if any.
|
||||
pub fn last_parse_error(self) -> Option<ParseError> {
|
||||
self.0.read().last_parse_error.clone()
|
||||
}
|
||||
|
||||
/// Clear the previous parse error.
|
||||
pub fn clear_parse_error(self, gc_context: MutationContext<'gc, '_>) {
|
||||
self.0.write(gc_context).last_parse_error = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> fmt::Debug for XmlDocument<'gc> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("XmlDocument")
|
||||
.field("root", &self.0.read().root)
|
||||
.finish()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue