diff options
author | Ville Lindholm <ville@lindholm.dev> | 2024-12-08 04:01:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-08 02:01:50 +0000 |
commit | bc7fe41a02b044b52a54c7347f347557a4bd0e8d (patch) | |
tree | 328133ab7eea5f406965c24a3fd2c6cc4858c55c | |
parent | 264c0f972fd1a732ee1d1490d78a1d26a65c6f5f (diff) | |
download | servo-bc7fe41a02b044b52a54c7347f347557a4bd0e8d.tar.gz servo-bc7fe41a02b044b52a54c7347f347557a4bd0e8d.zip |
Add XPath parser/evaluator (#34463)
* Add XPath parser/evaluator
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* Correctly annotate XPathEvaluator IDL
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: have bindings pass in `can_gc`
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: add docstrings
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: implement PartialEq for Value for readability
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: add docstrings for CoreFunctions
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: simplify node test code
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: add unit tests for string handling xpath functions
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* put xpath features behind dom.xpath.enabled pref
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review] remove rstest and insta dev-deps
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* update wpt test expectations
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* [PR review]: tweak metadata files
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
* update wpt test expectations AGAIN
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
---------
Signed-off-by: Ville Lindholm <ville@lindholm.dev>
36 files changed, 6426 insertions, 314 deletions
diff --git a/Cargo.lock b/Cargo.lock index 89f1b5fb3f3..96519101499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6042,6 +6042,7 @@ dependencies = [ "mozangle", "mozjs", "net_traits", + "nom", "num-traits", "num_cpus", "parking_lot", diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 6fb0b0342f6..adf0f910ca1 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -436,6 +436,9 @@ mod gen { }, timeout_ms: i64, }, + xpath: { + enabled: bool, + } }, gfx: { subpixel_text_antialiasing: { diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 1d4df4d67f3..5975d14b678 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -130,6 +130,7 @@ webrender_api = { workspace = true } webrender_traits = { workspace = true } webxr-api = { workspace = true, features = ["ipc"], optional = true } xml5ever = { workspace = true } +nom = "7.1.3" [target.'cfg(not(target_os = "ios"))'.dependencies] mozangle = { workspace = true } diff --git a/components/script/dom/bindings/codegen/Bindings.conf b/components/script/dom/bindings/codegen/Bindings.conf index a1f3de7e46a..0a15dde41d7 100644 --- a/components/script/dom/bindings/codegen/Bindings.conf +++ b/components/script/dom/bindings/codegen/Bindings.conf @@ -84,7 +84,7 @@ DOMInterfaces = { }, 'Document': { - 'canGc': ['Close', 'CreateElement', 'CreateElementNS', 'ImportNode', 'SetTitle', 'Write', 'Writeln', 'CreateEvent', 'CreateRange', 'Open', 'Open_', 'CreateComment', 'CreateAttribute', 'CreateAttributeNS', 'CreateDocumentFragment', 'CreateTextNode', 'CreateCDATASection', 'CreateProcessingInstruction', 'Prepend', 'Append', 'ReplaceChildren', 'SetBgColor', 'SetFgColor', 'Fonts', 'ElementFromPoint', 'ElementsFromPoint', 'ExitFullscreen'], + 'canGc': ['Close', 'CreateElement', 'CreateElementNS', 'ImportNode', 'SetTitle', 'Write', 'Writeln', 'CreateEvent', 'CreateRange', 'Open', 'Open_', 'CreateComment', 'CreateAttribute', 'CreateAttributeNS', 'CreateDocumentFragment', 'CreateTextNode', 'CreateCDATASection', 'CreateProcessingInstruction', 'Prepend', 'Append', 'ReplaceChildren', 'SetBgColor', 'SetFgColor', 'Fonts', 'ElementFromPoint', 'ElementsFromPoint', 'ExitFullscreen', 'CreateExpression', 'CreateNSResolver', 'Evaluate'], }, 'DocumentFragment': { @@ -488,6 +488,14 @@ DOMInterfaces = { 'canGc': ['Abort', 'GetResponseXML', 'Response', 'Send'], }, +'XPathEvaluator': { + 'canGc': ['CreateExpression', 'Evaluate'], +}, + +'XPathExpression': { + 'canGc': ['Evaluate'], +}, + 'XRBoundedReferenceSpace': { 'canGc': ['BoundsGeometry'], }, diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 7021cbe62d5..99bbedf9bb1 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -71,6 +71,7 @@ use uuid::Uuid; use webgpu::swapchain::WebGPUContextId; use webrender_api::units::DeviceIntRect; +use super::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use crate::animation_timeline::AnimationTimeline; use crate::animations::Animations; use crate::document_loader::{DocumentLoader, LoadType}; @@ -95,6 +96,7 @@ use crate::dom::bindings::codegen::Bindings::TouchBinding::TouchMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ FrameRequestCallback, ScrollBehavior, WindowMethods, }; +use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::codegen::UnionTypes::{NodeOrString, StringOrElementCreationOptions}; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; @@ -180,6 +182,7 @@ use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext; use crate::dom::wheelevent::WheelEvent; use crate::dom::window::{ReflowReason, Window}; use crate::dom::windowproxy::WindowProxy; +use crate::dom::xpathevaluator::XPathEvaluator; use crate::fetch::FetchCanceller; use crate::network_listener::{NetworkListener, PreInvoke}; use crate::realms::{enter_realm, AlreadyInRealm, InRealm}; @@ -682,6 +685,12 @@ impl Document { self.is_html_document } + pub fn is_xhtml_document(&self) -> bool { + self.content_type.type_() == mime::APPLICATION && + self.content_type.subtype().as_str() == "xhtml" && + self.content_type.suffix() == Some(mime::XML) + } + pub fn set_https_state(&self, https_state: HttpsState) { self.https_state.set(https_state); } @@ -4519,11 +4528,7 @@ impl DocumentMethods<crate::DomTypeHolder> for Document { local_name.make_ascii_lowercase(); } - let is_xhtml = self.content_type.type_() == mime::APPLICATION && - self.content_type.subtype().as_str() == "xhtml" && - self.content_type.suffix() == Some(mime::XML); - - let ns = if self.is_html_document || is_xhtml { + let ns = if self.is_html_document || self.is_xhtml_document() { ns!(html) } else { ns!() @@ -5613,6 +5618,53 @@ impl DocumentMethods<crate::DomTypeHolder> for Document { fn VisibilityState(&self) -> DocumentVisibilityState { self.visibility_state.get() } + + fn CreateExpression( + &self, + expression: DOMString, + resolver: Option<Rc<XPathNSResolver>>, + can_gc: CanGc, + ) -> Fallible<DomRoot<super::types::XPathExpression>> { + let global = self.global(); + let window = global.as_window(); + let evaluator = XPathEvaluator::new(window, None, can_gc); + XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateExpression( + &*evaluator, + expression, + resolver, + can_gc, + ) + } + + fn CreateNSResolver(&self, node_resolver: &Node, can_gc: CanGc) -> DomRoot<Node> { + let global = self.global(); + let window = global.as_window(); + let evaluator = XPathEvaluator::new(window, None, can_gc); + XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateNSResolver(&*evaluator, node_resolver) + } + + fn Evaluate( + &self, + expression: DOMString, + context_node: &Node, + resolver: Option<Rc<XPathNSResolver>>, + type_: u16, + result: Option<&super::types::XPathResult>, + can_gc: CanGc, + ) -> Fallible<DomRoot<super::types::XPathResult>> { + let global = self.global(); + let window = global.as_window(); + let evaluator = XPathEvaluator::new(window, None, can_gc); + XPathEvaluatorMethods::<crate::DomTypeHolder>::Evaluate( + &*evaluator, + expression, + context_node, + resolver, + type_, + result, + can_gc, + ) + } } fn update_with_current_instant(marker: &Cell<Option<CrossProcessInstant>>) { diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 3253cf6b39d..52a8559628a 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -601,4 +601,7 @@ pub mod xmlhttprequest; pub mod xmlhttprequesteventtarget; pub mod xmlhttprequestupload; pub mod xmlserializer; +pub mod xpathevaluator; +pub mod xpathexpression; +pub mod xpathresult; pub use self::webgl_extensions::ext::*; diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 3c7df12b633..b8e3531508e 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -10,7 +10,7 @@ use std::default::Default; use std::ops::Range; use std::slice::from_ref; use std::sync::Arc as StdArc; -use std::{cmp, iter}; +use std::{cmp, fmt, iter}; use app_units::Au; use base::id::{BrowsingContextId, PipelineId}; @@ -168,6 +168,23 @@ pub struct Node { layout_data: DomRefCell<Option<Box<GenericLayoutData>>>, } +impl fmt::Debug for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if matches!(self.type_id(), NodeTypeId::Element(_)) { + let el = self.downcast::<Element>().unwrap(); + el.fmt(f) + } else { + write!(f, "[Node({:?})]", self.type_id()) + } + } +} + +impl fmt::Debug for DomRoot<Node> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + (**self).fmt(f) + } +} + /// Flags for node items #[derive(Clone, Copy, JSTraceable, MallocSizeOf)] pub struct NodeFlags(u16); diff --git a/components/script/dom/webidls/XPathEvaluator.webidl b/components/script/dom/webidls/XPathEvaluator.webidl new file mode 100644 index 00000000000..8586b19232b --- /dev/null +++ b/components/script/dom/webidls/XPathEvaluator.webidl @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// https://dom.spec.whatwg.org/#mixin-xpathevaluatorbase +interface mixin XPathEvaluatorBase { + [NewObject, Throws, Pref="dom.xpath.enabled"] XPathExpression createExpression( + DOMString expression, + optional XPathNSResolver? resolver = null + ); + Node createNSResolver(Node nodeResolver); // legacy + // XPathResult.ANY_TYPE = 0 + [Throws, Pref="dom.xpath.enabled"] XPathResult evaluate( + DOMString expression, + Node contextNode, + optional XPathNSResolver? resolver = null, + optional unsigned short type = 0, + optional XPathResult? result = null + ); +}; + +Document includes XPathEvaluatorBase; + +// https://dom.spec.whatwg.org/#interface-xpathevaluator +[Exposed=Window, Pref="dom.xpath.enabled"] +interface XPathEvaluator { + constructor(); +}; + +XPathEvaluator includes XPathEvaluatorBase; diff --git a/components/script/dom/webidls/XPathExpression.webidl b/components/script/dom/webidls/XPathExpression.webidl new file mode 100644 index 00000000000..bf5ba2b83aa --- /dev/null +++ b/components/script/dom/webidls/XPathExpression.webidl @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// https://dom.spec.whatwg.org/#interface-xpathexpression +[Exposed=Window, Pref="dom.xpath.enabled"] +interface XPathExpression { + // XPathResult.ANY_TYPE = 0 + [Throws] XPathResult evaluate( + Node contextNode, + optional unsigned short type = 0, + optional XPathResult? result = null + ); +}; diff --git a/components/script/dom/webidls/XPathNSResolver.webidl b/components/script/dom/webidls/XPathNSResolver.webidl new file mode 100644 index 00000000000..099f6afd105 --- /dev/null +++ b/components/script/dom/webidls/XPathNSResolver.webidl @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// https://dom.spec.whatwg.org/#mixin-xpathevaluatorbase +[Exposed=Window] +callback interface XPathNSResolver { + DOMString? lookupNamespaceURI(DOMString? prefix); +}; diff --git a/components/script/dom/webidls/XPathResult.webidl b/components/script/dom/webidls/XPathResult.webidl new file mode 100644 index 00000000000..25e87e3024a --- /dev/null +++ b/components/script/dom/webidls/XPathResult.webidl @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// https://dom.spec.whatwg.org/#interface-xpathresult +[Exposed=Window, Pref="dom.xpath.enabled"] +interface XPathResult { + const unsigned short ANY_TYPE = 0; + const unsigned short NUMBER_TYPE = 1; + const unsigned short STRING_TYPE = 2; + const unsigned short BOOLEAN_TYPE = 3; + const unsigned short UNORDERED_NODE_ITERATOR_TYPE = 4; + const unsigned short ORDERED_NODE_ITERATOR_TYPE = 5; + const unsigned short UNORDERED_NODE_SNAPSHOT_TYPE = 6; + const unsigned short ORDERED_NODE_SNAPSHOT_TYPE = 7; + const unsigned short ANY_UNORDERED_NODE_TYPE = 8; + const unsigned short FIRST_ORDERED_NODE_TYPE = 9; + + readonly attribute unsigned short resultType; + [Throws] readonly attribute unrestricted double numberValue; + [Throws] readonly attribute DOMString stringValue; + [Throws] readonly attribute boolean booleanValue; + [Throws] readonly attribute Node? singleNodeValue; + [Throws] readonly attribute boolean invalidIteratorState; + [Throws] readonly attribute unsigned long snapshotLength; + + [Throws] Node? iterateNext(); + [Throws] Node? snapshotItem(unsigned long index); +}; diff --git a/components/script/dom/xpathevaluator.rs b/components/script/dom/xpathevaluator.rs new file mode 100644 index 00000000000..967f7ef6076 --- /dev/null +++ b/components/script/dom/xpathevaluator.rs @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::rc::Rc; + +use dom_struct::dom_struct; +use js::rust::HandleObject; + +use super::bindings::error::Error; +use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; +use crate::dom::bindings::codegen::Bindings::XPathExpressionBinding::XPathExpression_Binding::XPathExpressionMethods; +use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; +use crate::dom::bindings::error::Fallible; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector}; +use crate::dom::bindings::root::{Dom, DomRoot}; +use crate::dom::bindings::str::DOMString; +use crate::dom::node::Node; +use crate::dom::window::Window; +use crate::dom::xpathexpression::XPathExpression; +use crate::dom::xpathresult::XPathResult; +use crate::script_runtime::CanGc; + +#[dom_struct] +pub struct XPathEvaluator { + reflector_: Reflector, + window: Dom<Window>, +} + +impl XPathEvaluator { + fn new_inherited(window: &Window) -> XPathEvaluator { + XPathEvaluator { + reflector_: Reflector::new(), + window: Dom::from_ref(window), + } + } + + pub fn new( + window: &Window, + proto: Option<HandleObject>, + can_gc: CanGc, + ) -> DomRoot<XPathEvaluator> { + reflect_dom_object_with_proto( + Box::new(XPathEvaluator::new_inherited(window)), + window, + proto, + can_gc, + ) + } +} + +impl XPathEvaluatorMethods<crate::DomTypeHolder> for XPathEvaluator { + /// <https://dom.spec.whatwg.org/#dom-xpathevaluator-xpathevaluator> + fn Constructor( + window: &Window, + proto: Option<HandleObject>, + can_gc: CanGc, + ) -> DomRoot<XPathEvaluator> { + XPathEvaluator::new(window, proto, can_gc) + } + + /// <https://dom.spec.whatwg.org/#dom-xpathevaluatorbase-createexpression> + fn CreateExpression( + &self, + expression: DOMString, + _resolver: Option<Rc<XPathNSResolver>>, + can_gc: CanGc, + ) -> Fallible<DomRoot<XPathExpression>> { + let global = self.global(); + let window = global.as_window(); + // NB: this function is *not* Fallible according to the spec, so we swallow any parsing errors and + // just pass a None as the expression... it's not great. + let parsed_expression = crate::xpath::parse(&expression).map_err(|_e| Error::Syntax)?; + Ok(XPathExpression::new( + window, + None, + can_gc, + parsed_expression, + )) + } + + /// <https://dom.spec.whatwg.org/#dom-xpathevaluatorbase-creatensresolver> + fn CreateNSResolver(&self, node_resolver: &Node) -> DomRoot<Node> { + // Legacy: the spec tells us to just return `node_resolver` as-is + DomRoot::from_ref(node_resolver) + } + + /// <https://dom.spec.whatwg.org/#dom-xpathevaluatorbase-evaluate> + fn Evaluate( + &self, + expression_str: DOMString, + context_node: &Node, + _resolver: Option<Rc<XPathNSResolver>>, + result_type: u16, + result: Option<&XPathResult>, + can_gc: CanGc, + ) -> Fallible<DomRoot<XPathResult>> { + let global = self.global(); + let window = global.as_window(); + let parsed_expression = crate::xpath::parse(&expression_str).map_err(|_| Error::Syntax)?; + let expression = XPathExpression::new(window, None, can_gc, parsed_expression); + XPathExpressionMethods::<crate::DomTypeHolder>::Evaluate( + &*expression, + context_node, + result_type, + result, + can_gc, + ) + } +} diff --git a/components/script/dom/xpathexpression.rs b/components/script/dom/xpathexpression.rs new file mode 100644 index 00000000000..16e898c2e72 --- /dev/null +++ b/components/script/dom/xpathexpression.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use dom_struct::dom_struct; +use js::rust::HandleObject; + +use crate::dom::bindings::codegen::Bindings::XPathExpressionBinding::XPathExpressionMethods; +use crate::dom::bindings::error::{Error, Fallible}; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector}; +use crate::dom::bindings::root::{Dom, DomRoot}; +use crate::dom::node::Node; +use crate::dom::window::Window; +use crate::dom::xpathresult::{XPathResult, XPathResultType}; +use crate::script_runtime::CanGc; +use crate::xpath::{evaluate_parsed_xpath, Expr}; + +#[dom_struct] +pub struct XPathExpression { + reflector_: Reflector, + window: Dom<Window>, + #[no_trace] + parsed_expression: Expr, +} + +impl XPathExpression { + fn new_inherited(window: &Window, parsed_expression: Expr) -> XPathExpression { + XPathExpression { + reflector_: Reflector::new(), + window: Dom::from_ref(window), + parsed_expression, + } + } + + pub fn new( + window: &Window, + proto: Option<HandleObject>, + can_gc: CanGc, + parsed_expression: Expr, + ) -> DomRoot<XPathExpression> { + reflect_dom_object_with_proto( + Box::new(XPathExpression::new_inherited(window, parsed_expression)), + window, + proto, + can_gc, + ) + } +} + +impl XPathExpressionMethods<crate::DomTypeHolder> for XPathExpression { + /// <https://dom.spec.whatwg.org/#dom-xpathexpression-evaluate> + fn Evaluate( + &self, + context_node: &Node, + result_type_num: u16, + _result: Option<&XPathResult>, + can_gc: CanGc, + ) -> Fallible<DomRoot<XPathResult>> { + let result_type = XPathResultType::try_from(result_type_num) + .map_err(|()| Error::Type("Invalid XPath result type".to_string()))?; + + let global = self.global(); + let window = global.as_window(); + + let result_value = evaluate_parsed_xpath(&self.parsed_expression, context_node) + .map_err(|_e| Error::Operation)?; + + // TODO(vlindhol): support putting results into mutable `_result` as per the spec + Ok(XPathResult::new( + window, + None, + can_gc, + result_type, + result_value.into(), + )) + } +} diff --git a/components/script/dom/xpathresult.rs b/components/script/dom/xpathresult.rs new file mode 100644 index 00000000000..93905b05ff7 --- /dev/null +++ b/components/script/dom/xpathresult.rs @@ -0,0 +1,261 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::Cell; + +use dom_struct::dom_struct; +use js::rust::HandleObject; + +use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{ + XPathResultConstants, XPathResultMethods, +}; +use crate::dom::bindings::error::{Error, Fallible}; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, Reflector}; +use crate::dom::bindings::root::{Dom, DomRoot}; +use crate::dom::bindings::str::DOMString; +use crate::dom::node::Node; +use crate::dom::window::Window; +use crate::script_runtime::CanGc; +use crate::xpath::{NodesetHelpers, Value}; + +#[repr(u16)] +#[derive(Clone, Copy, Debug, Eq, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)] +pub enum XPathResultType { + Any = XPathResultConstants::ANY_TYPE, + Number = XPathResultConstants::NUMBER_TYPE, + String = XPathResultConstants::STRING_TYPE, + Boolean = XPathResultConstants::BOOLEAN_TYPE, + UnorderedNodeIterator = XPathResultConstants::UNORDERED_NODE_ITERATOR_TYPE, + OrderedNodeIterator = XPathResultConstants::ORDERED_NODE_ITERATOR_TYPE, + UnorderedNodeSnapshot = XPathResultConstants::UNORDERED_NODE_SNAPSHOT_TYPE, + OrderedNodeSnapshot = XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE, + AnyUnorderedNode = XPathResultConstants::ANY_UNORDERED_NODE_TYPE, + FirstOrderedNode = XPathResultConstants::FIRST_ORDERED_NODE_TYPE, +} + +impl TryFrom<u16> for XPathResultType { + type Error = (); + + fn try_from(value: u16) -> Result<Self, Self::Error> { + match value { + XPathResultConstants::ANY_TYPE => Ok(Self::Any), + XPathResultConstants::NUMBER_TYPE => Ok(Self::Number), + XPathResultConstants::STRING_TYPE => Ok(Self::String), + XPathResultConstants::BOOLEAN_TYPE => Ok(Self::Boolean), + XPathResultConstants::UNORDERED_NODE_ITERATOR_TYPE => Ok(Self::UnorderedNodeIterator), + XPathResultConstants::ORDERED_NODE_ITERATOR_TYPE => Ok(Self::OrderedNodeIterator), + XPathResultConstants::UNORDERED_NODE_SNAPSHOT_TYPE => Ok(Self::UnorderedNodeSnapshot), + XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE => Ok(Self::OrderedNodeSnapshot), + XPathResultConstants::ANY_UNORDERED_NODE_TYPE => Ok(Self::AnyUnorderedNode), + XPathResultConstants::FIRST_ORDERED_NODE_TYPE => Ok(Self::FirstOrderedNode), + _ => Err(()), + } + } +} + +#[derive(JSTraceable, MallocSizeOf)] +pub enum XPathResultValue { + Boolean(bool), + /// A IEEE-754 double-precision floating point number + Number(f64), + String(DOMString), + /// A collection of unique nodes + Nodeset(Vec<DomRoot<Node>>), +} + +impl From<Value> for XPathResultValue { + fn from(value: Value) -> Self { + match value { + Value::Boolean(b) => XPathResultValue::Boolean(b), + Value::Number(n) => XPathResultValue::Number(n), + Value::String(s) => XPathResultValue::String(s.into()), + Value::Nodeset(nodes) => { + // Put the evaluation result into (unique) document order. This also re-roots them + // so that we are sure we can hold them for the lifetime of this XPathResult. + let rooted_nodes = nodes.document_order_unique(); + XPathResultValue::Nodeset(rooted_nodes) + }, + } + } +} + +#[dom_struct] +pub struct XPathResult { + reflector_: Reflector, + window: Dom<Window>, + result_type: XPathResultType, + value: XPathResultValue, + iterator_invalid: Cell<bool>, + iterator_pos: Cell<usize>, +} + +impl XPathResult { + fn new_inherited( + window: &Window, + result_type: XPathResultType, + value: XPathResultValue, + ) -> XPathResult { + // TODO(vlindhol): if the wanted result type is AnyUnorderedNode | FirstOrderedNode, + // we could drop all nodes except one to save memory. + let inferred_result_type = if result_type == XPathResultType::Any { + match value { + XPathResultValue::Boolean(_) => XPathResultType::Boolean, + XPathResultValue::Number(_) => XPathResultType::Number, + XPathResultValue::String(_) => XPathResultType::String, + XPathResultValue::Nodeset(_) => XPathResultType::UnorderedNodeIterator, + } + } else { + result_type + }; + + XPathResult { + reflector_: Reflector::new(), + window: Dom::from_ref(window), + result_type: inferred_result_type, + iterator_invalid: Cell::new(false), + iterator_pos: Cell::new(0), + value, + } + } + + /// NB: Blindly trusts `result_type` and constructs an object regardless of the contents + /// of `value`. The exception is `XPathResultType::Any`, for which we look at the value + /// to determine the type. + pub fn new( + window: &Window, + proto: Option<HandleObject>, + can_gc: CanGc, + result_type: XPathResultType, + value: XPathResultValue, + ) -> DomRoot<XPathResult> { + reflect_dom_object_with_proto( + Box::new(XPathResult::new_inherited(window, result_type, value)), + window, + proto, + can_gc, + ) + } +} + +impl XPathResultMethods<crate::DomTypeHolder> for XPathResult { + /// <https://dom.spec.whatwg.org/#dom-xpathresult-resulttype> + fn ResultType(&self) -> u16 { + self.result_type as u16 + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-numbervalue> + fn GetNumberValue(&self) -> Fallible<f64> { + match (&self.value, self.result_type) { + (XPathResultValue::Number(n), XPathResultType::Number) => Ok(*n), + _ => Err(Error::Type( + "Can't get number value for non-number XPathResult".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-stringvalue> + fn GetStringValue(&self) -> Fallible<DOMString> { + match (&self.value, self.result_type) { + (XPathResultValue::String(s), XPathResultType::String) => Ok(s.clone()), + _ => Err(Error::Type( + "Can't get string value for non-string XPathResult".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-booleanvalue> + fn GetBooleanValue(&self) -> Fallible<bool> { + match (&self.value, self.result_type) { + (XPathResultValue::Boolean(b), XPathResultType::Boolean) => Ok(*b), + _ => Err(Error::Type( + "Can't get boolean value for non-boolean XPathResult".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-iteratenext> + fn IterateNext(&self) -> Fallible<Option<DomRoot<Node>>> { + // TODO(vlindhol): actually set `iterator_invalid` somewhere + if self.iterator_invalid.get() { + return Err(Error::Range( + "Invalidated iterator for XPathResult, the DOM has mutated.".to_string(), + )); + } + + match (&self.value, self.result_type) { + ( + XPathResultValue::Nodeset(nodes), + XPathResultType::OrderedNodeIterator | XPathResultType::UnorderedNodeIterator, + ) => { + let pos = self.iterator_pos.get(); + if pos >= nodes.len() { + Ok(None) + } else { + let node = nodes[pos].clone(); + self.iterator_pos.set(pos + 1); + Ok(Some(node)) + } + }, + _ => Err(Error::Type( + "Can't iterate on XPathResult that is not a node-set".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-invaliditeratorstate> + fn GetInvalidIteratorState(&self) -> Fallible<bool> { + let is_iterator_invalid = self.iterator_invalid.get(); + if is_iterator_invalid || + matches!( + self.result_type, + XPathResultType::OrderedNodeIterator | XPathResultType::UnorderedNodeIterator + ) + { + Ok(is_iterator_invalid) + } else { + Err(Error::Type( + "Can't iterate on XPathResult that is not a node-set".to_string(), + )) + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-snapshotlength> + fn GetSnapshotLength(&self) -> Fallible<u32> { + match (&self.value, self.result_type) { + ( + XPathResultValue::Nodeset(nodes), + XPathResultType::OrderedNodeSnapshot | XPathResultType::UnorderedNodeSnapshot, + ) => Ok(nodes.len() as u32), + _ => Err(Error::Type( + "Can't get snapshot length of XPathResult that is not a snapshot".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-snapshotitem> + fn SnapshotItem(&self, index: u32) -> Fallible<Option<DomRoot<Node>>> { + match (&self.value, self.result_type) { + ( + XPathResultValue::Nodeset(nodes), + XPathResultType::OrderedNodeSnapshot | XPathResultType::UnorderedNodeSnapshot, + ) => Ok(nodes.get(index as usize).cloned()), + _ => Err(Error::Type( + "Can't get snapshot item of XPathResult that is not a snapshot".to_string(), + )), + } + } + + /// <https://dom.spec.whatwg.org/#dom-xpathresult-singlenodevalue> + fn GetSingleNodeValue(&self) -> Fallible<Option<DomRoot<Node>>> { + match (&self.value, self.result_type) { + ( + XPathResultValue::Nodeset(nodes), + XPathResultType::AnyUnorderedNode | XPathResultType::FirstOrderedNode, + ) => Ok(nodes.first().cloned()), + _ => Err(Error::Type( + "Getting single value requires result type 'any unordered node' or 'first ordered node'".to_string(), + )), + } + } +} diff --git a/components/script/lib.rs b/components/script/lib.rs index 63f0147d421..7cb227a53e1 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -98,6 +98,7 @@ mod unminify; mod drag_data_store; mod links; +mod xpath; pub use init::init; pub use script_runtime::JSEngineSetup; diff --git a/components/script/xpath/context.rs b/components/script/xpath/context.rs new file mode 100644 index 00000000000..90873a89776 --- /dev/null +++ b/components/script/xpath/context.rs @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::iter::Enumerate; +use std::vec::IntoIter; + +use super::Node; +use crate::dom::bindings::root::DomRoot; + +/// The context during evaluation of an XPath expression. +pub struct EvaluationCtx { + /// Where we started at + pub starting_node: DomRoot<Node>, + /// The "current" node in the evaluation + pub context_node: DomRoot<Node>, + /// Details needed for evaluating a predicate list + pub predicate_ctx: Option<PredicateCtx>, + /// The nodes we're currently matching against + pub predicate_nodes: Option<Vec<DomRoot<Node>>>, +} + +#[derive(Clone, Copy)] +pub struct PredicateCtx { + pub index: usize, + pub size: usize, +} + +impl EvaluationCtx { + /// Prepares the context used while evaluating the XPath expression + pub fn new(context_node: &Node) -> EvaluationCtx { + EvaluationCtx { + starting_node: DomRoot::from_ref(context_node), + context_node: DomRoot::from_ref(context_node), + predicate_ctx: None, + predicate_nodes: None, + } + } + + /// Creates a new context using the provided node as the context node + pub fn subcontext_for_node(&self, node: &Node) -> EvaluationCtx { + EvaluationCtx { + starting_node: self.starting_node.clone(), + context_node: DomRoot::from_ref(node), + predicate_ctx: self.predicate_ctx, + predicate_nodes: self.predicate_nodes.clone(), + } + } + + pub fn update_predicate_nodes(&self, nodes: Vec<&Node>) -> EvaluationCtx { + EvaluationCtx { + starting_node: self.starting_node.clone(), + context_node: self.context_node.clone(), + predicate_ctx: None, + predicate_nodes: Some(nodes.into_iter().map(DomRoot::from_ref).collect()), + } + } + + pub fn subcontext_iter_for_nodes(&self) -> EvalNodesetIter { + let size = self.predicate_nodes.as_ref().map_or(0, |v| v.len()); + EvalNodesetIter { + ctx: self, + nodes_iter: self + .predicate_nodes + .as_ref() + .map_or_else(|| Vec::new().into_iter(), |v| v.clone().into_iter()) + .enumerate(), + size, + } + } +} + +/// When evaluating predicates, we need to keep track of the current node being evaluated and +/// the index of that node in the nodeset we're operating on. +pub struct EvalNodesetIter<'a> { + ctx: &'a EvaluationCtx, + nodes_iter: Enumerate<IntoIter<DomRoot<Node>>>, + size: usize, +} + +impl<'a> Iterator for EvalNodesetIter<'a> { + type Item = EvaluationCtx; + + fn next(&mut self) -> Option<EvaluationCtx> { + self.nodes_iter.next().map(|(idx, node)| EvaluationCtx { + starting_node: self.ctx.starting_node.clone(), + context_node: node.clone(), + predicate_nodes: self.ctx.predicate_nodes.clone(), + predicate_ctx: Some(PredicateCtx { + index: idx + 1, + size: self.size, + }), + }) + } +} diff --git a/components/script/xpath/eval.rs b/components/script/xpath/eval.rs new file mode 100644 index 00000000000..d880e2f3930 --- /dev/null +++ b/components/script/xpath/eval.rs @@ -0,0 +1,589 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::fmt; + +use html5ever::{local_name, namespace_prefix, namespace_url, ns, QualName}; + +use super::parser::{ + AdditiveOp, Axis, EqualityOp, Expr, FilterExpr, KindTest, Literal, MultiplicativeOp, NodeTest, + NumericLiteral, PathExpr, PredicateExpr, PredicateListExpr, PrimaryExpr, + QName as ParserQualName, RelationalOp, StepExpr, UnaryOp, +}; +use super::{EvaluationCtx, Value}; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; +use crate::dom::bindings::inheritance::{Castable, CharacterDataTypeId, NodeTypeId}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::xmlname::validate_and_extract; +use crate::dom::element::Element; +use crate::dom::node::{Node, ShadowIncluding}; +use crate::dom::processinginstruction::ProcessingInstruction; + +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + NotANodeset, + InvalidPath, + UnknownFunction { name: QualName }, + UnknownVariable { name: QualName }, + UnknownNamespace { prefix: String }, + InvalidQName { qname: ParserQualName }, + FunctionEvaluation { fname: String }, + Internal { msg: String }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::NotANodeset => write!(f, "expression did not evaluate to a nodeset"), + Error::InvalidPath => write!(f, "invalid path expression"), + Error::UnknownFunction { name } => write!(f, "unknown function {:?}", name), + Error::UnknownVariable { name } => write!(f, "unknown variable {:?}", name), + Error::UnknownNamespace { prefix } => { + write!(f, "unknown namespace prefix {:?}", prefix) + }, + Error::InvalidQName { qname } => { + write!(f, "invalid QName {:?}", qname) + }, + Error::FunctionEvaluation { fname } => { + write!(f, "error while evaluating function: {}", fname) + }, + Error::Internal { msg } => { + write!(f, "internal error: {}", msg) + }, + } + } +} + +impl std::error::Error for Error {} + +pub fn try_extract_nodeset(v: Value) -> Result<Vec<DomRoot<Node>>, Error> { + match v { + Value::Nodeset(ns) => Ok(ns), + _ => Err(Error::NotANodeset), + } +} + +pub trait Evaluatable: fmt::Debug { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error>; + /// Returns true if this expression evaluates to a primitive value, without needing to touch the DOM + fn is_primitive(&self) -> bool; +} + +impl<T: ?Sized> Evaluatable for Box<T> +where + T: Evaluatable, +{ + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + (**self).evaluate(context) + } + + fn is_primitive(&self) -> bool { + (**self).is_primitive() + } +} + +impl Evaluatable for Expr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + Expr::And(left, right) => { + let left_bool = left.evaluate(context)?.boolean(); + let v = left_bool && right.evaluate(context)?.boolean(); + Ok(Value::Boolean(v)) + }, + Expr::Or(left, right) => { + let left_bool = left.evaluate(context)?.boolean(); + let v = left_bool || right.evaluate(context)?.boolean(); + Ok(Value::Boolean(v)) + }, + Expr::Equality(left, equality_op, right) => { + let left_val = left.evaluate(context)?; + let right_val = right.evaluate(context)?; + + let v = match equality_op { + EqualityOp::Eq => left_val == right_val, + EqualityOp::NotEq => left_val != right_val, + }; + + Ok(Value::Boolean(v)) + }, + Expr::Relational(left, relational_op, right) => { + let left_val = left.evaluate(context)?.number(); + let right_val = right.evaluate(context)?.number(); + + let v = match relational_op { + RelationalOp::Lt => left_val < right_val, + RelationalOp::Gt => left_val > right_val, + RelationalOp::LtEq => left_val <= right_val, + RelationalOp::GtEq => left_val >= right_val, + }; + Ok(Value::Boolean(v)) + }, + Expr::Additive(left, additive_op, right) => { + let left_val = left.evaluate(context)?.number(); + let right_val = right.evaluate(context)?.number(); + + let v = match additive_op { + AdditiveOp::Add => left_val + right_val, + AdditiveOp::Sub => left_val - right_val, + }; + Ok(Value::Number(v)) + }, + Expr::Multiplicative(left, multiplicative_op, right) => { + let left_val = left.evaluate(context)?.number(); + let right_val = right.evaluate(context)?.number(); + + let v = match multiplicative_op { + MultiplicativeOp::Mul => left_val * right_val, + MultiplicativeOp::Div => left_val / right_val, + MultiplicativeOp::Mod => left_val % right_val, + }; + Ok(Value::Number(v)) + }, + Expr::Unary(unary_op, expr) => { + let v = expr.evaluate(context)?.number(); + + match unary_op { + UnaryOp::Minus => Ok(Value::Number(-v)), + } + }, + Expr::Union(left, right) => { + let as_nodes = |e: &Expr| e.evaluate(context).and_then(try_extract_nodeset); + + let mut left_nodes = as_nodes(left)?; + let right_nodes = as_nodes(right)?; + + left_nodes.extend(right_nodes); + Ok(Value::Nodeset(left_nodes)) + }, + Expr::Path(path_expr) => path_expr.evaluate(context), + } + } + + fn is_primitive(&self) -> bool { + match self { + Expr::Or(left, right) => left.is_primitive() && right.is_primitive(), + Expr::And(left, right) => left.is_primitive() && right.is_primitive(), + Expr::Equality(left, _, right) => left.is_primitive() && right.is_primitive(), + Expr::Relational(left, _, right) => left.is_primitive() && right.is_primitive(), + Expr::Additive(left, _, right) => left.is_primitive() && right.is_primitive(), + Expr::Multiplicative(left, _, right) => left.is_primitive() && right.is_primitive(), + Expr::Unary(_, expr) => expr.is_primitive(), + Expr::Union(_, _) => false, + Expr::Path(path_expr) => path_expr.is_primitive(), + } + } +} + +impl Evaluatable for PathExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + let mut current_nodes = vec![context.context_node.clone()]; + + // If path starts with '//', add an implicit descendant-or-self::node() step + if self.is_descendant { + current_nodes = current_nodes + .iter() + .flat_map(|n| n.traverse_preorder(ShadowIncluding::No)) + .collect(); + } + + trace!("[PathExpr] Evaluating path expr: {:?}", self); + + let have_multiple_steps = self.steps.len() > 1; + + for step in &self.steps { + let mut next_nodes = Vec::new(); + for node in current_nodes { + let step_context = context.subcontext_for_node(&node); + let step_result = step.evaluate(&step_context)?; + match (have_multiple_steps, step_result) { + (_, Value::Nodeset(mut nodes)) => { + // as long as we evaluate to nodesets, keep going + next_nodes.append(&mut nodes); + }, + (false, value) => { + trace!("[PathExpr] Got single primitive value: {:?}", value); + return Ok(value); + }, + (true, value) => { + error!( + "Expected nodeset from step evaluation, got: {:?} node: {:?}, step: {:?}", + value, node, step + ); + return Ok(value); + }, + } + } + current_nodes = next_nodes; + } + + trace!("[PathExpr] Got nodes: {:?}", current_nodes); + + Ok(Value::Nodeset(current_nodes)) + } + + fn is_primitive(&self) -> bool { + !self.is_absolute && + !self.is_descendant && + self.steps.len() == 1 && + self.steps[0].is_primitive() + } +} + +impl TryFrom<&ParserQualName> for QualName { + type Error = Error; + + fn try_from(qname: &ParserQualName) -> Result<Self, Self::Error> { + let qname_as_str = qname.to_string(); + if let Ok((ns, prefix, local)) = validate_and_extract(None, &qname_as_str) { + Ok(QualName { prefix, ns, local }) + } else { + Err(Error::InvalidQName { + qname: qname.clone(), + }) + } + } +} + +pub enum NameTestComparisonMode { + /// Namespaces must match exactly + XHtml, + /// Missing namespace information is treated as the HTML namespace + Html, +} + +pub fn element_name_test( + expected_name: QualName, + element_qualname: QualName, + comparison_mode: NameTestComparisonMode, +) -> bool { + let is_wildcard = expected_name.local == local_name!("*"); + + let test_prefix = expected_name + .prefix + .clone() + .unwrap_or(namespace_prefix!("")); + let test_ns_uri = match test_prefix { + namespace_prefix!("*") => ns!(*), + namespace_prefix!("html") => ns!(html), + namespace_prefix!("xml") => ns!(xml), + namespace_prefix!("xlink") => ns!(xlink), + namespace_prefix!("svg") => ns!(svg), + namespace_prefix!("mathml") => ns!(mathml), + namespace_prefix!("") => { + if matches!(comparison_mode, NameTestComparisonMode::XHtml) { + ns!() + } else { + ns!(html) + } + }, + _ => { + // We don't support custom namespaces, use fallback or panic depending on strictness + if matches!(comparison_mode, NameTestComparisonMode::XHtml) { + panic!("Unrecognized namespace prefix: {}", test_prefix) + } else { + ns!(html) + } + }, + }; + + if is_wildcard { + test_ns_uri == element_qualname.ns + } else { + test_ns_uri == element_qualname.ns && expected_name.local == element_qualname.local + } +} + +fn apply_node_test(test: &NodeTest, node: &Node) -> Result<bool, Error> { + let result = match test { + NodeTest::Name(qname) => { + // Convert the unvalidated "parser QualName" into the proper QualName structure + let wanted_name: QualName = qname.try_into()?; + if matches!(node.type_id(), NodeTypeId::Element(_)) { + let element = node.downcast::<Element>().unwrap(); + let comparison_mode = if node.owner_doc().is_xhtml_document() { + NameTestComparisonMode::XHtml + } else { + NameTestComparisonMode::Html + }; + let element_qualname = QualName::new( + element.prefix().as_ref().cloned(), + element.namespace().clone(), + element.local_name().clone(), + ); + element_name_test(wanted_name, element_qualname, comparison_mode) + } else { + false + } + }, + NodeTest::Wildcard => true, + NodeTest::Kind(kind) => match kind { + KindTest::PI(target) => { + if NodeTypeId::CharacterData(CharacterDataTypeId::ProcessingInstruction) == + node.type_id() + { + let pi = node.downcast::<ProcessingInstruction>().unwrap(); + match (target, pi.target()) { + (Some(target_name), node_target_name) + if target_name == &node_target_name.to_string() => + { + true + }, + (Some(_), _) => false, + (None, _) => true, + } + } else { + false + } + }, + KindTest::Comment => matches!( + node.type_id(), + NodeTypeId::CharacterData(CharacterDataTypeId::Comment) + ), + KindTest::Text => matches!( + node.type_id(), + NodeTypeId::CharacterData(CharacterDataTypeId::Text(_)) + ), + KindTest::Node => true, + }, + }; + Ok(result) +} + +impl Evaluatable for StepExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + StepExpr::Filter(filter_expr) => filter_expr.evaluate(context), + StepExpr::Axis(axis_step) => { + let nodes: Vec<DomRoot<Node>> = match axis_step.axis { + Axis::Child => context.context_node.children().collect(), + Axis::Descendant => context + .context_node + .traverse_preorder(ShadowIncluding::No) + .skip(1) + .collect(), + Axis::Parent => vec![context.context_node.GetParentNode()] + .into_iter() + .flatten() + .collect(), + Axis::Ancestor => context.context_node.ancestors().collect(), + Axis::Following => context + .context_node + .following_nodes(&context.context_node) + .skip(1) + .collect(), + Axis::Preceding => context + .context_node + .preceding_nodes(&context.context_node) + .skip(1) + .collect(), + Axis::FollowingSibling => context.context_node.following_siblings().collect(), + Axis::PrecedingSibling => context.context_node.preceding_siblings().collect(), + Axis::Attribute => { + if matches!(Node::type_id(&context.context_node), NodeTypeId::Element(_)) { + let element = context.context_node.downcast::<Element>().unwrap(); + element + .attrs() + .iter() + .map(|attr| attr.upcast::<Node>()) + .map(DomRoot::from_ref) + .collect() + } else { + vec![] + } + }, + Axis::Self_ => vec![context.context_node.clone()], + Axis::DescendantOrSelf => context + .context_node + .traverse_preorder(ShadowIncluding::No) + .collect(), + Axis::AncestorOrSelf => context + .context_node + .inclusive_ancestors(ShadowIncluding::No) + .collect(), + Axis::Namespace => Vec::new(), // Namespace axis is not commonly implemented + }; + + trace!("[StepExpr] Axis {:?} got nodes {:?}", axis_step.axis, nodes); + + // Filter nodes according to the step's node_test. Will error out if any NodeTest + // application errors out. + let filtered_nodes: Vec<DomRoot<Node>> = nodes + .into_iter() + .map(|node| { + apply_node_test(&axis_step.node_test, &node) + .map(|matches| matches.then_some(node)) + }) + .collect::<Result<Vec<_>, _>>()? + .into_iter() + .flatten() + .collect(); + + trace!("[StepExpr] Filtering got nodes {:?}", filtered_nodes); + + if axis_step.predicates.predicates.is_empty() { + trace!( + "[StepExpr] No predicates, returning nodes {:?}", + filtered_nodes + ); + Ok(Value::Nodeset(filtered_nodes)) + } else { + // Apply predicates + let predicate_list_subcontext = context + .update_predicate_nodes(filtered_nodes.iter().map(|n| &**n).collect()); + axis_step.predicates.evaluate(&predicate_list_subcontext) + } + }, + } + } + + fn is_primitive(&self) -> bool { + match self { + StepExpr::Filter(filter_expr) => filter_expr.is_primitive(), + StepExpr::Axis(_) => false, + } + } +} + +impl Evaluatable for PredicateListExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + if let Some(ref predicate_nodes) = context.predicate_nodes { + // Initializing: every node the predicates act on is matched + let mut matched_nodes: Vec<DomRoot<Node>> = predicate_nodes.clone(); + + // apply each predicate to the nodes matched by the previous predicate + for predicate_expr in &self.predicates { + let context_for_predicate = + context.update_predicate_nodes(matched_nodes.iter().map(|n| &**n).collect()); + + let narrowed_nodes = predicate_expr + .evaluate(&context_for_predicate) + .and_then(try_extract_nodeset)?; + matched_nodes = narrowed_nodes; + trace!( + "[PredicateListExpr] Predicate {:?} matched nodes {:?}", + predicate_expr, + matched_nodes + ); + } + Ok(Value::Nodeset(matched_nodes)) + } else { + Err(Error::Internal { + msg: "[PredicateListExpr] No nodes on stack for predicate to operate on" + .to_string(), + }) + } + } + + fn is_primitive(&self) -> bool { + self.predicates.len() == 1 && self.predicates[0].is_primitive() + } +} + +impl Evaluatable for PredicateExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + let narrowed_nodes: Result<Vec<DomRoot<Node>>, Error> = context + .subcontext_iter_for_nodes() + .filter_map(|ctx| { + if let Some(predicate_ctx) = ctx.predicate_ctx { + let eval_result = self.expr.evaluate(&ctx); + + let v = match eval_result { + Ok(Value::Number(v)) => Ok(predicate_ctx.index == v as usize), + Ok(v) => Ok(v.boolean()), + Err(e) => Err(e), + }; + + match v { + Ok(true) => Some(Ok(ctx.context_node)), + Ok(false) => None, + Err(e) => Some(Err(e)), + } + } else { + Some(Err(Error::Internal { + msg: "[PredicateExpr] No predicate context set".to_string(), + })) + } + }) + .collect(); + + Ok(Value::Nodeset(narrowed_nodes?)) + } + + fn is_primitive(&self) -> bool { + self.expr.is_primitive() + } +} + +impl Evaluatable for FilterExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + let primary_result = self.primary.evaluate(context)?; + let have_predicates = !self.predicates.predicates.is_empty(); + + match (have_predicates, &primary_result) { + (false, _) => { + trace!( + "[FilterExpr] No predicates, returning primary result: {:?}", + primary_result + ); + Ok(primary_result) + }, + (true, Value::Nodeset(vec)) => { + let predicate_list_subcontext = + context.update_predicate_nodes(vec.iter().map(|n| &**n).collect()); + let result_filtered_by_predicates = + self.predicates.evaluate(&predicate_list_subcontext); + trace!( + "[FilterExpr] Result filtered by predicates: {:?}", + result_filtered_by_predicates + ); + result_filtered_by_predicates + }, + // You can't use filtering expressions `[]` on other than node-sets + (true, _) => Err(Error::NotANodeset), + } + } + + fn is_primitive(&self) -> bool { + self.predicates.predicates.is_empty() && self.primary.is_primitive() + } +} + +impl Evaluatable for PrimaryExpr { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + PrimaryExpr::Literal(literal) => literal.evaluate(context), + PrimaryExpr::Variable(_qname) => todo!(), + PrimaryExpr::Parenthesized(expr) => expr.evaluate(context), + PrimaryExpr::ContextItem => Ok(Value::Nodeset(vec![context.context_node.clone()])), + PrimaryExpr::Function(core_function) => core_function.evaluate(context), + } + } + + fn is_primitive(&self) -> bool { + match self { + PrimaryExpr::Literal(_) => true, + PrimaryExpr::Variable(_qname) => false, + PrimaryExpr::Parenthesized(expr) => expr.is_primitive(), + PrimaryExpr::ContextItem => false, + PrimaryExpr::Function(_) => false, + } + } +} + +impl Evaluatable for Literal { + fn evaluate(&self, _context: &EvaluationCtx) -> Result<Value, Error> { + match self { + Literal::Numeric(numeric_literal) => match numeric_literal { + // We currently make no difference between ints and floats + NumericLiteral::Integer(v) => Ok(Value::Number(*v as f64)), + NumericLiteral::Decimal(v) => Ok(Value::Number(*v)), + }, + Literal::String(s) => Ok(Value::String(s.into())), + } + } + + fn is_primitive(&self) -> bool { + true + } +} diff --git a/components/script/xpath/eval_function.rs b/components/script/xpath/eval_function.rs new file mode 100644 index 00000000000..dbf77c503ea --- /dev/null +++ b/components/script/xpath/eval_function.rs @@ -0,0 +1,357 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use super::context::EvaluationCtx; +use super::eval::{try_extract_nodeset, Error, Evaluatable}; +use super::parser::CoreFunction; +use super::Value; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; +use crate::dom::bindings::inheritance::{Castable, NodeTypeId}; +use crate::dom::element::Element; +use crate::dom::node::Node; + +/// Returns e.g. "rect" for `<svg:rect>` +fn local_name(node: &Node) -> Option<String> { + if matches!(Node::type_id(node), NodeTypeId::Element(_)) { + let element = node.downcast::<Element>().unwrap(); + Some(element.local_name().to_string()) + } else { + None + } +} + +/// Returns e.g. "svg:rect" for `<svg:rect>` +fn name(node: &Node) -> Option<String> { + if matches!(Node::type_id(node), NodeTypeId::Element(_)) { + let element = node.downcast::<Element>().unwrap(); + if let Some(prefix) = element.prefix().as_ref() { + Some(format!("{}:{}", prefix, element.local_name())) + } else { + Some(element.local_name().to_string()) + } + } else { + None + } +} + +/// Returns e.g. the SVG namespace URI for `<svg:rect>` +fn namespace_uri(node: &Node) -> Option<String> { + if matches!(Node::type_id(node), NodeTypeId::Element(_)) { + let element = node.downcast::<Element>().unwrap(); + Some(element.namespace().to_string()) + } else { + None + } +} + +/// Returns the text contents of the Node, or empty string if none. +fn string_value(node: &Node) -> String { + node.GetTextContent().unwrap_or_default().to_string() +} + +/// If s2 is found inside s1, return everything *before* s2. Return all of s1 otherwise. +fn substring_before(s1: &str, s2: &str) -> String { + match s1.find(s2) { + Some(pos) => s1[..pos].to_string(), + None => String::new(), + } +} + +/// If s2 is found inside s1, return everything *after* s2. Return all of s1 otherwise. +fn substring_after(s1: &str, s2: &str) -> String { + match s1.find(s2) { + Some(pos) => s1[pos + s2.len()..].to_string(), + None => String::new(), + } +} + +fn substring(s: &str, start_idx: isize, len: Option<isize>) -> String { + let s_len = s.len(); + let len = len.unwrap_or(s_len as isize).max(0) as usize; + let start_idx = start_idx.max(0) as usize; + let end_idx = (start_idx + len.max(0)).min(s_len); + s[start_idx..end_idx].to_string() +} + +/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-normalize-space> +pub fn normalize_space(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut last_was_whitespace = true; // Handles leading whitespace + + for c in s.chars() { + match c { + '\x20' | '\x09' | '\x0D' | '\x0A' => { + if !last_was_whitespace { + result.push(' '); + last_was_whitespace = true; + } + }, + other => { + result.push(other); + last_was_whitespace = false; + }, + } + } + + if last_was_whitespace { + result.pop(); + } + + result +} + +impl Evaluatable for CoreFunction { + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + CoreFunction::Last => { + let predicate_ctx = context.predicate_ctx.ok_or_else(|| Error::Internal { + msg: "[CoreFunction] last() is only usable as a predicate".to_string(), + })?; + Ok(Value::Number(predicate_ctx.size as f64)) + }, + CoreFunction::Position => { + let predicate_ctx = context.predicate_ctx.ok_or_else(|| Error::Internal { + msg: "[CoreFunction] position() is only usable as a predicate".to_string(), + })?; + Ok(Value::Number(predicate_ctx.index as f64)) + }, + CoreFunction::Count(expr) => { + let nodes = expr.evaluate(context).and_then(try_extract_nodeset)?; + Ok(Value::Number(nodes.len() as f64)) + }, + CoreFunction::String(expr_opt) => match expr_opt { + Some(expr) => Ok(Value::String(expr.evaluate(context)?.string())), + None => Ok(Value::String(string_value(&context.context_node))), + }, + CoreFunction::Concat(exprs) => { + let strings: Result<Vec<_>, _> = exprs + .iter() + .map(|e| Ok(e.evaluate(context)?.string())) + .collect(); + Ok(Value::String(strings?.join(""))) + }, + CoreFunction::Id(_expr) => todo!(), + CoreFunction::LocalName(expr_opt) => { + let node = match expr_opt { + Some(expr) => expr + .evaluate(context) + .and_then(try_extract_nodeset)? + .first() + .cloned(), + None => Some(context.context_node.clone()), + }; + let name = node.and_then(|n| local_name(&n)).unwrap_or_default(); + Ok(Value::String(name.to_string())) + }, + CoreFunction::NamespaceUri(expr_opt) => { + let node = match expr_opt { + Some(expr) => expr + .evaluate(context) + .and_then(try_extract_nodeset)? + .first() + .cloned(), + None => Some(context.context_node.clone()), + }; + let ns = node.and_then(|n| namespace_uri(&n)).unwrap_or_default(); + Ok(Value::String(ns.to_string())) + }, + CoreFunction::Name(expr_opt) => { + let node = match expr_opt { + Some(expr) => expr + .evaluate(context) + .and_then(try_extract_nodeset)? + .first() + .cloned(), + None => Some(context.context_node.clone()), + }; + let name = node.and_then(|n| name(&n)).unwrap_or_default(); + Ok(Value::String(name)) + }, + CoreFunction::StartsWith(str1, str2) => { + let s1 = str1.evaluate(context)?.string(); + let s2 = str2.evaluate(context)?.string(); + Ok(Value::Boolean(s1.starts_with(&s2))) + }, + CoreFunction::Contains(str1, str2) => { + let s1 = str1.evaluate(context)?.string(); + let s2 = str2.evaluate(context)?.string(); + Ok(Value::Boolean(s1.contains(&s2))) + }, + CoreFunction::SubstringBefore(str1, str2) => { + let s1 = str1.evaluate(context)?.string(); + let s2 = str2.evaluate(context)?.string(); + Ok(Value::String(substring_before(&s1, &s2))) + }, + CoreFunction::SubstringAfter(str1, str2) => { + let s1 = str1.evaluate(context)?.string(); + let s2 = str2.evaluate(context)?.string(); + Ok(Value::String(substring_after(&s1, &s2))) + }, + CoreFunction::Substring(str1, start, length_opt) => { + let s = str1.evaluate(context)?.string(); + let start_idx = start.evaluate(context)?.number().round() as isize - 1; + let len = match length_opt { + Some(len_expr) => Some(len_expr.evaluate(context)?.number().round() as isize), + None => None, + }; + Ok(Value::String(substring(&s, start_idx, len))) + }, + CoreFunction::StringLength(expr_opt) => { + let s = match expr_opt { + Some(expr) => expr.evaluate(context)?.string(), + None => string_value(&context.context_node), + }; + Ok(Value::Number(s.chars().count() as f64)) + }, + CoreFunction::NormalizeSpace(expr_opt) => { + let s = match expr_opt { + Some(expr) => expr.evaluate(context)?.string(), + None => string_value(&context.context_node), + }; + + Ok(Value::String(normalize_space(&s))) + }, + CoreFunction::Translate(str1, str2, str3) => { + let s = str1.evaluate(context)?.string(); + let from = str2.evaluate(context)?.string(); + let to = str3.evaluate(context)?.string(); + let result = s + .chars() + .map(|c| match from.find(c) { + Some(i) if i < to.chars().count() => to.chars().nth(i).unwrap(), + _ => c, + }) + .collect(); + Ok(Value::String(result)) + }, + CoreFunction::Number(expr_opt) => { + let val = match expr_opt { + Some(expr) => expr.evaluate(context)?, + None => Value::String(string_value(&context.context_node)), + }; + Ok(Value::Number(val.number())) + }, + CoreFunction::Sum(expr) => { + let nodes = expr.evaluate(context).and_then(try_extract_nodeset)?; + let sum = nodes + .iter() + .map(|n| Value::String(string_value(n)).number()) + .sum(); + Ok(Value::Number(sum)) + }, + CoreFunction::Floor(expr) => { + let num = expr.evaluate(context)?.number(); + Ok(Value::Number(num.floor())) + }, + CoreFunction::Ceiling(expr) => { + let num = expr.evaluate(context)?.number(); + Ok(Value::Number(num.ceil())) + }, + CoreFunction::Round(expr) => { + let num = expr.evaluate(context)?.number(); + Ok(Value::Number(num.round())) + }, + CoreFunction::Boolean(expr) => Ok(Value::Boolean(expr.evaluate(context)?.boolean())), + CoreFunction::Not(expr) => Ok(Value::Boolean(!expr.evaluate(context)?.boolean())), + CoreFunction::True => Ok(Value::Boolean(true)), + CoreFunction::False => Ok(Value::Boolean(false)), + CoreFunction::Lang(_) => Ok(Value::Nodeset(vec![])), // Not commonly used in the DOM, short-circuit it + } + } + + fn is_primitive(&self) -> bool { + match self { + CoreFunction::Last => false, + CoreFunction::Position => false, + CoreFunction::Count(_) => false, + CoreFunction::Id(_) => false, + CoreFunction::LocalName(_) => false, + CoreFunction::NamespaceUri(_) => false, + CoreFunction::Name(_) => false, + CoreFunction::String(expr_opt) => expr_opt + .as_ref() + .map(|expr| expr.is_primitive()) + .unwrap_or(false), + CoreFunction::Concat(vec) => vec.iter().all(|expr| expr.is_primitive()), + CoreFunction::StartsWith(expr, substr) => expr.is_primitive() && substr.is_primitive(), + CoreFunction::Contains(expr, substr) => expr.is_primitive() && substr.is_primitive(), + CoreFunction::SubstringBefore(expr, substr) => { + expr.is_primitive() && substr.is_primitive() + }, + CoreFunction::SubstringAfter(expr, substr) => { + expr.is_primitive() && substr.is_primitive() + }, + CoreFunction::Substring(expr, start_pos, length_opt) => { + expr.is_primitive() && + start_pos.is_primitive() && + length_opt + .as_ref() + .map(|length| length.is_primitive()) + .unwrap_or(false) + }, + CoreFunction::StringLength(expr_opt) => expr_opt + .as_ref() + .map(|expr| expr.is_primitive()) + .unwrap_or(false), + CoreFunction::NormalizeSpace(expr_opt) => expr_opt + .as_ref() + .map(|expr| expr.is_primitive()) + .unwrap_or(false), + CoreFunction::Translate(expr, from_chars, to_chars) => { + expr.is_primitive() && from_chars.is_primitive() && to_chars.is_primitive() + }, + CoreFunction::Number(expr_opt) => expr_opt + .as_ref() + .map(|expr| expr.is_primitive()) + .unwrap_or(false), + CoreFunction::Sum(expr) => expr.is_primitive(), + CoreFunction::Floor(expr) => expr.is_primitive(), + CoreFunction::Ceiling(expr) => expr.is_primitive(), + CoreFunction::Round(expr) => expr.is_primitive(), + CoreFunction::Boolean(expr) => expr.is_primitive(), + CoreFunction::Not(expr) => expr.is_primitive(), + CoreFunction::True => true, + CoreFunction::False => true, + CoreFunction::Lang(_) => false, + } + } +} +#[cfg(test)] +mod tests { + use super::{substring, substring_after, substring_before}; + + #[test] + fn test_substring_before() { + assert_eq!(substring_before("hello world", "world"), "hello "); + assert_eq!(substring_before("prefix:name", ":"), "prefix"); + assert_eq!(substring_before("no-separator", "xyz"), ""); + assert_eq!(substring_before("", "anything"), ""); + assert_eq!(substring_before("multiple:colons:here", ":"), "multiple"); + assert_eq!(substring_before("start-match-test", "start"), ""); + } + + #[test] + fn test_substring_after() { + assert_eq!(substring_after("hello world", "hello "), "world"); + assert_eq!(substring_after("prefix:name", ":"), "name"); + assert_eq!(substring_after("no-separator", "xyz"), ""); + assert_eq!(substring_after("", "anything"), ""); + assert_eq!(substring_after("multiple:colons:here", ":"), "colons:here"); + assert_eq!(substring_after("test-end-match", "match"), ""); + } + + #[test] + fn test_substring() { + assert_eq!(substring("hello world", 0, Some(5)), "hello"); + assert_eq!(substring("hello world", 6, Some(5)), "world"); + assert_eq!(substring("hello", 1, Some(3)), "ell"); + assert_eq!(substring("hello", -5, Some(2)), "he"); + assert_eq!(substring("hello", 0, None), "hello"); + assert_eq!(substring("hello", 2, Some(10)), "llo"); + assert_eq!(substring("hello", 5, Some(1)), ""); + assert_eq!(substring("", 0, Some(5)), ""); + assert_eq!(substring("hello", 0, Some(0)), ""); + assert_eq!(substring("hello", 0, Some(-5)), ""); + } +} diff --git a/components/script/xpath/eval_value.rs b/components/script/xpath/eval_value.rs new file mode 100644 index 00000000000..de3f0d9d075 --- /dev/null +++ b/components/script/xpath/eval_value.rs @@ -0,0 +1,242 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::borrow::ToOwned; +use std::collections::HashSet; +use std::{fmt, string}; + +use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::utils::AsVoidPtr; +use crate::dom::node::Node; + +/// The primary types of values that an XPath expression returns as a result. +pub enum Value { + Boolean(bool), + /// A IEEE-754 double-precision floating point number + Number(f64), + String(String), + /// A collection of not-necessarily-unique nodes + Nodeset(Vec<DomRoot<Node>>), +} + +impl fmt::Debug for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Value::Boolean(val) => write!(f, "{}", val), + Value::Number(val) => write!(f, "{}", val), + Value::String(ref val) => write!(f, "{}", val), + Value::Nodeset(ref val) => write!(f, "Nodeset({:?})", val), + } + } +} + +pub fn str_to_num(s: &str) -> f64 { + s.trim().parse().unwrap_or(f64::NAN) +} + +/// Helper for PartialEq<Value> implementations +fn str_vals(nodes: &[DomRoot<Node>]) -> HashSet<String> { + nodes + .iter() + .map(|n| n.GetTextContent().unwrap_or_default().to_string()) + .collect() +} + +/// Helper for PartialEq<Value> implementations +fn num_vals(nodes: &[DomRoot<Node>]) -> Vec<f64> { + nodes + .iter() + .map(|n| Value::String(n.GetTextContent().unwrap_or_default().into()).number()) + .collect() +} + +impl PartialEq<Value> for Value { + fn eq(&self, other: &Value) -> bool { + match (self, other) { + (Value::Nodeset(left_nodes), Value::Nodeset(right_nodes)) => { + let left_strings = str_vals(left_nodes); + let right_strings = str_vals(right_nodes); + !left_strings.is_disjoint(&right_strings) + }, + (&Value::Nodeset(ref nodes), &Value::Number(val)) | + (&Value::Number(val), &Value::Nodeset(ref nodes)) => { + let numbers = num_vals(nodes); + numbers.iter().any(|n| *n == val) + }, + (&Value::Nodeset(ref nodes), &Value::String(ref val)) | + (&Value::String(ref val), &Value::Nodeset(ref nodes)) => { + let strings = str_vals(nodes); + strings.contains(val) + }, + (&Value::Boolean(_), _) | (_, &Value::Boolean(_)) => self.boolean() == other.boolean(), + (&Value::Number(_), _) | (_, &Value::Number(_)) => self.number() == other.number(), + _ => self.string() == other.string(), + } + } +} + +impl Value { + pub fn boolean(&self) -> bool { + match *self { + Value::Boolean(val) => val, + Value::Number(n) => n != 0.0 && !n.is_nan(), + Value::String(ref s) => !s.is_empty(), + Value::Nodeset(ref nodeset) => !nodeset.is_empty(), + } + } + + pub fn into_boolean(self) -> bool { + self.boolean() + } + + pub fn number(&self) -> f64 { + match *self { + Value::Boolean(val) => { + if val { + 1.0 + } else { + 0.0 + } + }, + Value::Number(val) => val, + Value::String(ref s) => str_to_num(s), + Value::Nodeset(..) => str_to_num(&self.string()), + } + } + + pub fn into_number(self) -> f64 { + self.number() + } + + pub fn string(&self) -> string::String { + match *self { + Value::Boolean(v) => v.to_string(), + Value::Number(n) => { + if n.is_infinite() { + if n.signum() < 0.0 { + "-Infinity".to_owned() + } else { + "Infinity".to_owned() + } + } else if n == 0.0 { + // catches -0.0 also + 0.0.to_string() + } else { + n.to_string() + } + }, + Value::String(ref val) => val.clone(), + Value::Nodeset(ref nodes) => match nodes.document_order_first() { + Some(n) => n.GetTextContent().unwrap_or_default().to_string(), + None => "".to_owned(), + }, + } + } + + pub fn into_string(self) -> string::String { + match self { + Value::String(val) => val, + other => other.string(), + } + } +} + +macro_rules! from_impl { + ($raw:ty, $variant:expr) => { + impl From<$raw> for Value { + fn from(other: $raw) -> Value { + $variant(other) + } + } + }; +} + +from_impl!(bool, Value::Boolean); +from_impl!(f64, Value::Number); +from_impl!(String, Value::String); +impl<'a> From<&'a str> for Value { + fn from(other: &'a str) -> Value { + Value::String(other.into()) + } +} +from_impl!(Vec<DomRoot<Node>>, Value::Nodeset); + +macro_rules! partial_eq_impl { + ($raw:ty, $variant:pat => $b:expr) => { + impl PartialEq<$raw> for Value { + fn eq(&self, other: &$raw) -> bool { + match *self { + $variant => $b == other, + _ => false, + } + } + } + + impl PartialEq<Value> for $raw { + fn eq(&self, other: &Value) -> bool { + match *other { + $variant => $b == self, + _ => false, + } + } + } + }; +} + +partial_eq_impl!(bool, Value::Boolean(ref v) => v); +partial_eq_impl!(f64, Value::Number(ref v) => v); +partial_eq_impl!(String, Value::String(ref v) => v); +partial_eq_impl!(&str, Value::String(ref v) => v); +partial_eq_impl!(Vec<DomRoot<Node>>, Value::Nodeset(ref v) => v); + +pub trait NodesetHelpers { + /// Returns the node that occurs first in [document order] + /// + /// [document order]: https://www.w3.org/TR/xpath/#dt-document-order + fn document_order_first(&self) -> Option<DomRoot<Node>>; + fn document_order(&self) -> Vec<DomRoot<Node>>; + fn document_order_unique(&self) -> Vec<DomRoot<Node>>; +} + +impl NodesetHelpers for Vec<DomRoot<Node>> { + fn document_order_first(&self) -> Option<DomRoot<Node>> { + self.iter() + .min_by(|a, b| { + if a == b { + std::cmp::Ordering::Equal + } else if a.is_before(b) { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + }) + .cloned() + } + fn document_order(&self) -> Vec<DomRoot<Node>> { + let mut nodes: Vec<DomRoot<Node>> = self.clone(); + if nodes.len() == 1 { + return nodes; + } + + nodes.sort_by(|a, b| { + if a == b { + std::cmp::Ordering::Equal + } else if a.is_before(b) { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + }); + + nodes + } + fn document_order_unique(&self) -> Vec<DomRoot<Node>> { + let mut nodes: Vec<DomRoot<Node>> = self.document_order(); + + nodes.dedup_by_key(|n| n.as_void_ptr()); + + nodes + } +} diff --git a/components/script/xpath/mod.rs b/components/script/xpath/mod.rs new file mode 100644 index 00000000000..b1bfa3b3089 --- /dev/null +++ b/components/script/xpath/mod.rs @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use context::EvaluationCtx; +use eval::Evaluatable; +pub use eval_value::{NodesetHelpers, Value}; +use parser::OwnedParserError; +pub use parser::{parse as parse_impl, Expr}; + +use super::dom::node::Node; + +mod context; +mod eval; +mod eval_function; +mod eval_value; +mod parser; + +/// The failure modes of executing an XPath. +#[derive(Debug, PartialEq)] +pub enum Error { + /// The XPath was syntactically invalid + Parsing { source: OwnedParserError }, + /// The XPath could not be executed + Evaluating { source: eval::Error }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::Parsing { source } => write!(f, "Unable to parse XPath: {}", source), + Error::Evaluating { source } => write!(f, "Unable to evaluate XPath: {}", source), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Parsing { source } => Some(source), + Error::Evaluating { source } => Some(source), + } + } +} + +/// Parse an XPath expression from a string +pub fn parse(xpath: &str) -> Result<Expr, Error> { + match parse_impl(xpath) { + Ok(expr) => { + debug!("Parsed XPath: {:?}", expr); + Ok(expr) + }, + Err(e) => { + debug!("Unable to parse XPath: {}", e); + Err(Error::Parsing { source: e }) + }, + } +} + +/// Evaluate an already-parsed XPath expression +pub fn evaluate_parsed_xpath(expr: &Expr, context_node: &Node) -> Result<Value, Error> { + let context = EvaluationCtx::new(context_node); + match expr.evaluate(&context) { + Ok(v) => { + debug!("Evaluated XPath: {:?}", v); + Ok(v) + }, + Err(e) => { + debug!("Unable to evaluate XPath: {}", e); + Err(Error::Evaluating { source: e }) + }, + } +} diff --git a/components/script/xpath/parser.rs b/components/script/xpath/parser.rs new file mode 100644 index 00000000000..3d439b8a4ca --- /dev/null +++ b/components/script/xpath/parser.rs @@ -0,0 +1,1209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use nom::branch::alt; +use nom::bytes::complete::{tag, take_while1}; +use nom::character::complete::{alpha1, alphanumeric1, char, digit1, multispace0}; +use nom::combinator::{map, opt, recognize, value}; +use nom::error::{Error as NomError, ErrorKind as NomErrorKind}; +use nom::multi::{many0, separated_list0}; +use nom::sequence::{delimited, pair, preceded, tuple}; +use nom::{Finish, IResult}; + +pub fn parse(input: &str) -> Result<Expr, OwnedParserError> { + let (_, ast) = expr(input).finish().map_err(OwnedParserError::from)?; + Ok(ast) +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum Expr { + Or(Box<Expr>, Box<Expr>), + And(Box<Expr>, Box<Expr>), + Equality(Box<Expr>, EqualityOp, Box<Expr>), + Relational(Box<Expr>, RelationalOp, Box<Expr>), + Additive(Box<Expr>, AdditiveOp, Box<Expr>), + Multiplicative(Box<Expr>, MultiplicativeOp, Box<Expr>), + Unary(UnaryOp, Box<Expr>), + Union(Box<Expr>, Box<Expr>), + Path(PathExpr), +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum EqualityOp { + Eq, + NotEq, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum RelationalOp { + Lt, + Gt, + LtEq, + GtEq, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum AdditiveOp { + Add, + Sub, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum MultiplicativeOp { + Mul, + Div, + Mod, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum UnaryOp { + Minus, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct PathExpr { + pub is_absolute: bool, + pub is_descendant: bool, + pub steps: Vec<StepExpr>, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct PredicateListExpr { + pub predicates: Vec<PredicateExpr>, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct PredicateExpr { + pub expr: Expr, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct FilterExpr { + pub primary: PrimaryExpr, + pub predicates: PredicateListExpr, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum StepExpr { + Filter(FilterExpr), + Axis(AxisStep), +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct AxisStep { + pub axis: Axis, + pub node_test: NodeTest, + pub predicates: PredicateListExpr, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum Axis { + Child, + Descendant, + Attribute, + Self_, + DescendantOrSelf, + FollowingSibling, + Following, + Namespace, + Parent, + Ancestor, + PrecedingSibling, + Preceding, + AncestorOrSelf, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum NodeTest { + Name(QName), + Wildcard, + Kind(KindTest), +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct QName { + pub prefix: Option<String>, + pub local_part: String, +} + +impl std::fmt::Display for QName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.prefix { + Some(prefix) => write!(f, "{}:{}", prefix, self.local_part), + None => write!(f, "{}", self.local_part), + } + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum KindTest { + PI(Option<String>), + Comment, + Text, + Node, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum PrimaryExpr { + Literal(Literal), + Variable(QName), + Parenthesized(Box<Expr>), + ContextItem, + /// We only support the built-in core functions + Function(CoreFunction), +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum Literal { + Numeric(NumericLiteral), + String(String), +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum NumericLiteral { + Integer(u64), + Decimal(f64), +} + +/// In the DOM we do not support custom functions, so we can enumerate the usable ones +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum CoreFunction { + // Node Set Functions + /// last() + Last, + /// position() + Position, + /// count(node-set) + Count(Box<Expr>), + /// id(object) + Id(Box<Expr>), + /// local-name(node-set?) + LocalName(Option<Box<Expr>>), + /// namespace-uri(node-set?) + NamespaceUri(Option<Box<Expr>>), + /// name(node-set?) + Name(Option<Box<Expr>>), + + // String Functions + /// string(object?) + String(Option<Box<Expr>>), + /// concat(string, string, ...) + Concat(Vec<Expr>), + /// starts-with(string, string) + StartsWith(Box<Expr>, Box<Expr>), + /// contains(string, string) + Contains(Box<Expr>, Box<Expr>), + /// substring-before(string, string) + SubstringBefore(Box<Expr>, Box<Expr>), + /// substring-after(string, string) + SubstringAfter(Box<Expr>, Box<Expr>), + /// substring(string, number, number?) + Substring(Box<Expr>, Box<Expr>, Option<Box<Expr>>), + /// string-length(string?) + StringLength(Option<Box<Expr>>), + /// normalize-space(string?) + NormalizeSpace(Option<Box<Expr>>), + /// translate(string, string, string) + Translate(Box<Expr>, Box<Expr>, Box<Expr>), + + // Number Functions + /// number(object?) + Number(Option<Box<Expr>>), + /// sum(node-set) + Sum(Box<Expr>), + /// floor(number) + Floor(Box<Expr>), + /// ceiling(number) + Ceiling(Box<Expr>), + /// round(number) + Round(Box<Expr>), + + // Boolean Functions + /// boolean(object) + Boolean(Box<Expr>), + /// not(boolean) + Not(Box<Expr>), + /// true() + True, + /// false() + False, + /// lang(string) + Lang(Box<Expr>), +} + +impl CoreFunction { + pub fn name(&self) -> &'static str { + match self { + CoreFunction::Last => "last", + CoreFunction::Position => "position", + CoreFunction::Count(_) => "count", + CoreFunction::Id(_) => "id", + CoreFunction::LocalName(_) => "local-name", + CoreFunction::NamespaceUri(_) => "namespace-uri", + CoreFunction::Name(_) => "name", + CoreFunction::String(_) => "string", + CoreFunction::Concat(_) => "concat", + CoreFunction::StartsWith(_, _) => "starts-with", + CoreFunction::Contains(_, _) => "contains", + CoreFunction::SubstringBefore(_, _) => "substring-before", + CoreFunction::SubstringAfter(_, _) => "substring-after", + CoreFunction::Substring(_, _, _) => "substring", + CoreFunction::StringLength(_) => "string-length", + CoreFunction::NormalizeSpace(_) => "normalize-space", + CoreFunction::Translate(_, _, _) => "translate", + CoreFunction::Number(_) => "number", + CoreFunction::Sum(_) => "sum", + CoreFunction::Floor(_) => "floor", + CoreFunction::Ceiling(_) => "ceiling", + CoreFunction::Round(_) => "round", + CoreFunction::Boolean(_) => "boolean", + CoreFunction::Not(_) => "not", + CoreFunction::True => "true", + CoreFunction::False => "false", + CoreFunction::Lang(_) => "lang", + } + } + + pub fn min_args(&self) -> usize { + match self { + // No args + CoreFunction::Last | + CoreFunction::Position | + CoreFunction::True | + CoreFunction::False => 0, + + // Optional single arg + CoreFunction::LocalName(_) | + CoreFunction::NamespaceUri(_) | + CoreFunction::Name(_) | + CoreFunction::String(_) | + CoreFunction::StringLength(_) | + CoreFunction::NormalizeSpace(_) | + CoreFunction::Number(_) => 0, + + // Required single arg + CoreFunction::Count(_) | + CoreFunction::Id(_) | + CoreFunction::Sum(_) | + CoreFunction::Floor(_) | + CoreFunction::Ceiling(_) | + CoreFunction::Round(_) | + CoreFunction::Boolean(_) | + CoreFunction::Not(_) | + CoreFunction::Lang(_) => 1, + + // Required two args + CoreFunction::StartsWith(_, _) | + CoreFunction::Contains(_, _) | + CoreFunction::SubstringBefore(_, _) | + CoreFunction::SubstringAfter(_, _) => 2, + + // Special cases + CoreFunction::Concat(_) => 2, // Minimum 2 args + CoreFunction::Substring(_, _, _) => 2, // 2 or 3 args + CoreFunction::Translate(_, _, _) => 3, // Exactly 3 args + } + } + + pub fn max_args(&self) -> Option<usize> { + match self { + // No args + CoreFunction::Last | + CoreFunction::Position | + CoreFunction::True | + CoreFunction::False => Some(0), + + // Optional single arg (0 or 1) + CoreFunction::LocalName(_) | + CoreFunction::NamespaceUri(_) | + CoreFunction::Name(_) | + CoreFunction::String(_) | + CoreFunction::StringLength(_) | + CoreFunction::NormalizeSpace(_) | + CoreFunction::Number(_) => Some(1), + + // Exactly one arg + CoreFunction::Count(_) | + CoreFunction::Id(_) | + CoreFunction::Sum(_) | + CoreFunction::Floor(_) | + CoreFunction::Ceiling(_) | + CoreFunction::Round(_) | + CoreFunction::Boolean(_) | + CoreFunction::Not(_) | + CoreFunction::Lang(_) => Some(1), + + // Exactly two args + CoreFunction::StartsWith(_, _) | + CoreFunction::Contains(_, _) | + CoreFunction::SubstringBefore(_, _) | + CoreFunction::SubstringAfter(_, _) => Some(2), + + // Special cases + CoreFunction::Concat(_) => None, // Unlimited args + CoreFunction::Substring(_, _, _) => Some(3), // 2 or 3 args + CoreFunction::Translate(_, _, _) => Some(3), // Exactly 3 args + } + } + + /// Returns true if the number of arguments is valid for this function + pub fn is_valid_arity(&self, num_args: usize) -> bool { + let min = self.min_args(); + let max = self.max_args(); + + num_args >= min && max.map_or(true, |max| num_args <= max) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct OwnedParserError { + input: String, + kind: NomErrorKind, +} + +impl<'a> From<NomError<&'a str>> for OwnedParserError { + fn from(err: NomError<&'a str>) -> Self { + OwnedParserError { + input: err.input.to_string(), + kind: err.code, + } + } +} + +impl std::fmt::Display for OwnedParserError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "error {:?} at: {}", self.kind, self.input) + } +} + +impl std::error::Error for OwnedParserError {} + +/// Top-level parser +fn expr(input: &str) -> IResult<&str, Expr> { + expr_single(input) +} + +fn expr_single(input: &str) -> IResult<&str, Expr> { + or_expr(input) +} + +fn or_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = and_expr(input)?; + let (input, rest) = many0(preceded(ws(tag("or")), and_expr))(input)?; + + Ok(( + input, + rest.into_iter() + .fold(first, |acc, expr| Expr::Or(Box::new(acc), Box::new(expr))), + )) +} + +fn and_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = equality_expr(input)?; + let (input, rest) = many0(preceded(ws(tag("and")), equality_expr))(input)?; + + Ok(( + input, + rest.into_iter() + .fold(first, |acc, expr| Expr::And(Box::new(acc), Box::new(expr))), + )) +} + +fn equality_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = relational_expr(input)?; + let (input, rest) = many0(tuple(( + ws(alt(( + map(tag("="), |_| EqualityOp::Eq), + map(tag("!="), |_| EqualityOp::NotEq), + ))), + relational_expr, + )))(input)?; + + Ok(( + input, + rest.into_iter().fold(first, |acc, (op, expr)| { + Expr::Equality(Box::new(acc), op, Box::new(expr)) + }), + )) +} + +fn relational_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = additive_expr(input)?; + let (input, rest) = many0(tuple(( + ws(alt(( + map(tag("<="), |_| RelationalOp::LtEq), + map(tag(">="), |_| RelationalOp::GtEq), + map(tag("<"), |_| RelationalOp::Lt), + map(tag(">"), |_| RelationalOp::Gt), + ))), + additive_expr, + )))(input)?; + + Ok(( + input, + rest.into_iter().fold(first, |acc, (op, expr)| { + Expr::Relational(Box::new(acc), op, Box::new(expr)) + }), + )) +} + +fn additive_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = multiplicative_expr(input)?; + let (input, rest) = many0(tuple(( + ws(alt(( + map(tag("+"), |_| AdditiveOp::Add), + map(tag("-"), |_| AdditiveOp::Sub), + ))), + multiplicative_expr, + )))(input)?; + + Ok(( + input, + rest.into_iter().fold(first, |acc, (op, expr)| { + Expr::Additive(Box::new(acc), op, Box::new(expr)) + }), + )) +} + +fn multiplicative_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = unary_expr(input)?; + let (input, rest) = many0(tuple(( + ws(alt(( + map(tag("*"), |_| MultiplicativeOp::Mul), + map(tag("div"), |_| MultiplicativeOp::Div), + map(tag("mod"), |_| MultiplicativeOp::Mod), + ))), + unary_expr, + )))(input)?; + + Ok(( + input, + rest.into_iter().fold(first, |acc, (op, expr)| { + Expr::Multiplicative(Box::new(acc), op, Box::new(expr)) + }), + )) +} + +fn unary_expr(input: &str) -> IResult<&str, Expr> { + let (input, minus_count) = many0(ws(char('-')))(input)?; + let (input, expr) = union_expr(input)?; + + Ok(( + input, + (0..minus_count.len()).fold(expr, |acc, _| Expr::Unary(UnaryOp::Minus, Box::new(acc))), + )) +} + +fn union_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = path_expr(input)?; + let (input, rest) = many0(preceded(ws(char('|')), path_expr))(input)?; + + Ok(( + input, + rest.into_iter().fold(first, |acc, expr| { + Expr::Union(Box::new(acc), Box::new(expr)) + }), + )) +} + +fn path_expr(input: &str) -> IResult<&str, Expr> { + alt(( + // "//" RelativePathExpr + map(pair(tag("//"), relative_path_expr), |(_, rel_path)| { + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: match rel_path { + Expr::Path(p) => p.steps, + _ => unreachable!(), + }, + }) + }), + // "/" RelativePathExpr? + map(pair(char('/'), opt(relative_path_expr)), |(_, rel_path)| { + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: false, + steps: rel_path + .map(|p| match p { + Expr::Path(p) => p.steps, + _ => unreachable!(), + }) + .unwrap_or_default(), + }) + }), + // RelativePathExpr + relative_path_expr, + ))(input) +} + +fn relative_path_expr(input: &str) -> IResult<&str, Expr> { + let (input, first) = step_expr(input)?; + let (input, steps) = many0(pair( + // ("/" | "//") + ws(alt((value(false, char('/')), value(true, tag("//"))))), + step_expr, + ))(input)?; + + let mut all_steps = vec![first]; + for (is_descendant, step) in steps { + if is_descendant { + // Insert an implicit descendant-or-self::node() step + all_steps.push(StepExpr::Axis(AxisStep { + axis: Axis::DescendantOrSelf, + node_test: NodeTest::Kind(KindTest::Node), + predicates: PredicateListExpr { predicates: vec![] }, + })); + } + all_steps.push(step); + } + + Ok(( + input, + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: all_steps, + }), + )) +} + +fn step_expr(input: &str) -> IResult<&str, StepExpr> { + alt(( + map(filter_expr, StepExpr::Filter), + map(axis_step, StepExpr::Axis), + ))(input) +} + +fn axis_step(input: &str) -> IResult<&str, AxisStep> { + let (input, (step, predicates)) = + pair(alt((forward_step, reverse_step)), predicate_list)(input)?; + + let (axis, node_test) = step; + Ok(( + input, + AxisStep { + axis, + node_test, + predicates, + }, + )) +} + +fn forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { + alt(( + // ForwardAxis NodeTest + pair(forward_axis, node_test), + // AbbrevForwardStep + abbrev_forward_step, + ))(input) +} + +fn forward_axis(input: &str) -> IResult<&str, Axis> { + let (input, axis) = alt(( + value(Axis::Child, tag("child::")), + value(Axis::Descendant, tag("descendant::")), + value(Axis::Attribute, tag("attribute::")), + value(Axis::Self_, tag("self::")), + value(Axis::DescendantOrSelf, tag("descendant-or-self::")), + value(Axis::FollowingSibling, tag("following-sibling::")), + value(Axis::Following, tag("following::")), + value(Axis::Namespace, tag("namespace::")), + ))(input)?; + + Ok((input, axis)) +} + +fn abbrev_forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { + let (input, attr) = opt(char('@'))(input)?; + let (input, test) = node_test(input)?; + + Ok(( + input, + ( + if attr.is_some() { + Axis::Attribute + } else { + Axis::Child + }, + test, + ), + )) +} + +fn reverse_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { + alt(( + // ReverseAxis NodeTest + pair(reverse_axis, node_test), + // AbbrevReverseStep + abbrev_reverse_step, + ))(input) +} + +fn reverse_axis(input: &str) -> IResult<&str, Axis> { + alt(( + value(Axis::Parent, tag("parent::")), + value(Axis::Ancestor, tag("ancestor::")), + value(Axis::PrecedingSibling, tag("preceding-sibling::")), + value(Axis::Preceding, tag("preceding::")), + value(Axis::AncestorOrSelf, tag("ancestor-or-self::")), + ))(input) +} + +fn abbrev_reverse_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { + map(tag(".."), |_| { + (Axis::Parent, NodeTest::Kind(KindTest::Node)) + })(input) +} + +fn node_test(input: &str) -> IResult<&str, NodeTest> { + alt(( + map(kind_test, NodeTest::Kind), + map(name_test, |name| match name { + NameTest::Wildcard => NodeTest::Wildcard, + NameTest::QName(qname) => NodeTest::Name(qname), + }), + ))(input) +} + +#[derive(Clone, Debug, PartialEq)] +enum NameTest { + QName(QName), + Wildcard, +} + +fn name_test(input: &str) -> IResult<&str, NameTest> { + alt(( + // NCName ":" "*" + map(tuple((ncname, char(':'), char('*'))), |(prefix, _, _)| { + NameTest::QName(QName { + prefix: Some(prefix.to_string()), + local_part: "*".to_string(), + }) + }), + // "*" + value(NameTest::Wildcard, char('*')), + // QName + map(qname, NameTest::QName), + ))(input) +} + +fn filter_expr(input: &str) -> IResult<&str, FilterExpr> { + let (input, primary) = primary_expr(input)?; + let (input, predicates) = predicate_list(input)?; + + Ok(( + input, + FilterExpr { + primary, + predicates, + }, + )) +} + +fn predicate_list(input: &str) -> IResult<&str, PredicateListExpr> { + let (input, predicates) = many0(predicate)(input)?; + Ok((input, PredicateListExpr { predicates })) +} + +fn predicate(input: &str) -> IResult<&str, PredicateExpr> { + let (input, expr) = delimited(ws(char('[')), expr, ws(char(']')))(input)?; + Ok((input, PredicateExpr { expr })) +} + +fn primary_expr(input: &str) -> IResult<&str, PrimaryExpr> { + alt(( + literal, + var_ref, + map(parenthesized_expr, |e| { + PrimaryExpr::Parenthesized(Box::new(e)) + }), + context_item_expr, + function_call, + ))(input) +} + +fn literal(input: &str) -> IResult<&str, PrimaryExpr> { + map(alt((numeric_literal, string_literal)), |lit| { + PrimaryExpr::Literal(lit) + })(input) +} + +fn numeric_literal(input: &str) -> IResult<&str, Literal> { + alt((decimal_literal, integer_literal))(input) +} + +fn var_ref(input: &str) -> IResult<&str, PrimaryExpr> { + let (input, _) = char('$')(input)?; + let (input, name) = qname(input)?; + Ok((input, PrimaryExpr::Variable(name))) +} + +fn parenthesized_expr(input: &str) -> IResult<&str, Expr> { + delimited(ws(char('(')), expr, ws(char(')')))(input) +} + +fn context_item_expr(input: &str) -> IResult<&str, PrimaryExpr> { + map(char('.'), |_| PrimaryExpr::ContextItem)(input) +} + +fn function_call(input: &str) -> IResult<&str, PrimaryExpr> { + let (input, name) = qname(input)?; + let (input, args) = delimited( + ws(char('(')), + separated_list0(ws(char(',')), expr_single), + ws(char(')')), + )(input)?; + + // Helper to create error + let arity_error = || nom::Err::Error(NomError::new(input, NomErrorKind::Verify)); + + let core_fn = match name.local_part.as_str() { + // Node Set Functions + "last" => CoreFunction::Last, + "position" => CoreFunction::Position, + "count" => CoreFunction::Count(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + "id" => CoreFunction::Id(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + "local-name" => CoreFunction::LocalName(args.into_iter().next().map(Box::new)), + "namespace-uri" => CoreFunction::NamespaceUri(args.into_iter().next().map(Box::new)), + "name" => CoreFunction::Name(args.into_iter().next().map(Box::new)), + + // String Functions + "string" => CoreFunction::String(args.into_iter().next().map(Box::new)), + "concat" => CoreFunction::Concat(args.into_iter().collect()), + "starts-with" => { + let mut args = args.into_iter(); + CoreFunction::StartsWith( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + ) + }, + "contains" => { + let mut args = args.into_iter(); + CoreFunction::Contains( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + ) + }, + "substring-before" => { + let mut args = args.into_iter(); + CoreFunction::SubstringBefore( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + ) + }, + "substring-after" => { + let mut args = args.into_iter(); + CoreFunction::SubstringAfter( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + ) + }, + "substring" => { + let mut args = args.into_iter(); + CoreFunction::Substring( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + args.next().map(Box::new), + ) + }, + "string-length" => CoreFunction::StringLength(args.into_iter().next().map(Box::new)), + "normalize-space" => CoreFunction::NormalizeSpace(args.into_iter().next().map(Box::new)), + "translate" => { + let mut args = args.into_iter(); + CoreFunction::Translate( + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + Box::new(args.next().ok_or_else(arity_error)?), + ) + }, + + // Number Functions + "number" => CoreFunction::Number(args.into_iter().next().map(Box::new)), + "sum" => CoreFunction::Sum(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + "floor" => CoreFunction::Floor(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + "ceiling" => { + CoreFunction::Ceiling(Box::new(args.into_iter().next().ok_or_else(arity_error)?)) + }, + "round" => CoreFunction::Round(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + + // Boolean Functions + "boolean" => { + CoreFunction::Boolean(Box::new(args.into_iter().next().ok_or_else(arity_error)?)) + }, + "not" => CoreFunction::Not(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + "true" => CoreFunction::True, + "false" => CoreFunction::False, + "lang" => CoreFunction::Lang(Box::new(args.into_iter().next().ok_or_else(arity_error)?)), + + // Unknown function + _ => return Err(nom::Err::Error(NomError::new(input, NomErrorKind::Verify))), + }; + + Ok((input, PrimaryExpr::Function(core_fn))) +} + +fn kind_test(input: &str) -> IResult<&str, KindTest> { + alt((pi_test, comment_test, text_test, any_kind_test))(input) +} + +fn any_kind_test(input: &str) -> IResult<&str, KindTest> { + map(tuple((tag("node"), ws(char('(')), ws(char(')')))), |_| { + KindTest::Node + })(input) +} + +fn text_test(input: &str) -> IResult<&str, KindTest> { + map(tuple((tag("text"), ws(char('(')), ws(char(')')))), |_| { + KindTest::Text + })(input) +} + +fn comment_test(input: &str) -> IResult<&str, KindTest> { + map( + tuple((tag("comment"), ws(char('(')), ws(char(')')))), + |_| KindTest::Comment, + )(input) +} + +fn pi_test(input: &str) -> IResult<&str, KindTest> { + map( + tuple(( + tag("processing-instruction"), + ws(char('(')), + opt(ws(string_literal)), + ws(char(')')), + )), + |(_, _, literal, _)| { + KindTest::PI(literal.map(|l| match l { + Literal::String(s) => s, + _ => unreachable!(), + })) + }, + )(input) +} + +fn ws<'a, F, O>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O> +where + F: FnMut(&'a str) -> IResult<&'a str, O>, +{ + delimited(multispace0, inner, multispace0) +} + +fn integer_literal(input: &str) -> IResult<&str, Literal> { + map(recognize(tuple((opt(char('-')), digit1))), |s: &str| { + Literal::Numeric(NumericLiteral::Integer(s.parse().unwrap())) + })(input) +} + +fn decimal_literal(input: &str) -> IResult<&str, Literal> { + map( + recognize(tuple((opt(char('-')), opt(digit1), char('.'), digit1))), + |s: &str| Literal::Numeric(NumericLiteral::Decimal(s.parse().unwrap())), + )(input) +} + +fn string_literal(input: &str) -> IResult<&str, Literal> { + alt(( + delimited( + char('"'), + map(take_while1(|c| c != '"'), |s: &str| { + Literal::String(s.to_string()) + }), + char('"'), + ), + delimited( + char('\''), + map(take_while1(|c| c != '\''), |s: &str| { + Literal::String(s.to_string()) + }), + char('\''), + ), + ))(input) +} + +// QName parser +fn qname(input: &str) -> IResult<&str, QName> { + let (input, prefix) = opt(tuple((ncname, char(':'))))(input)?; + let (input, local) = ncname(input)?; + + Ok(( + input, + QName { + prefix: prefix.map(|(p, _)| p.to_string()), + local_part: local.to_string(), + }, + )) +} + +// NCName parser +fn ncname(input: &str) -> IResult<&str, &str> { + recognize(pair( + alpha1, + many0(alt((alphanumeric1, tag("-"), tag("_")))), + ))(input) +} + +// Test functions to verify the parsers: +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_node_tests() { + let cases = vec![ + ("node()", NodeTest::Kind(KindTest::Node)), + ("text()", NodeTest::Kind(KindTest::Text)), + ("comment()", NodeTest::Kind(KindTest::Comment)), + ( + "processing-instruction()", + NodeTest::Kind(KindTest::PI(None)), + ), + ( + "processing-instruction('test')", + NodeTest::Kind(KindTest::PI(Some("test".to_string()))), + ), + ("*", NodeTest::Wildcard), + ( + "prefix:*", + NodeTest::Name(QName { + prefix: Some("prefix".to_string()), + local_part: "*".to_string(), + }), + ), + ( + "div", + NodeTest::Name(QName { + prefix: None, + local_part: "div".to_string(), + }), + ), + ( + "ns:div", + NodeTest::Name(QName { + prefix: Some("ns".to_string()), + local_part: "div".to_string(), + }), + ), + ]; + + for (input, expected) in cases { + match node_test(input) { + Ok((remaining, result)) => { + assert!(remaining.is_empty(), "Parser didn't consume all input"); + assert_eq!(result, expected); + }, + Err(e) => panic!("Failed to parse '{}': {:?}", input, e), + } + } + } + + #[test] + fn test_filter_expr() { + let cases = vec![ + ( + "processing-instruction('test')[2]", + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Kind(KindTest::PI(Some("test".to_string()))), + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::Numeric( + NumericLiteral::Integer(2), + )), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + }], + }, + })], + }), + ), + ( + "concat('hello', ' ', 'world')", + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Function(CoreFunction::Concat(vec![ + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "hello".to_string(), + )), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String(" ".to_string())), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "world".to_string(), + )), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + ])), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + ), + ]; + + for (input, expected) in cases { + match parse(input) { + Ok(result) => { + assert_eq!(result, expected); + }, + Err(e) => panic!("Failed to parse '{}': {:?}", input, e), + } + } + } + + #[test] + fn test_complex_paths() { + let cases = vec![ + ( + "//*[contains(@class, 'test')]", + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Wildcard, + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Function(CoreFunction::Contains( + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Axis(AxisStep { + axis: Axis::Attribute, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "class".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "test".to_string(), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + )), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + }], + }, + })], + }), + ), + ( + "//div[position() > 1]/*[last()]", + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: vec![ + StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "div".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Relational( + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Function( + CoreFunction::Position, + ), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + RelationalOp::Gt, + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::Numeric( + NumericLiteral::Integer(1), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + ), + }], + }, + }), + StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Wildcard, + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Function(CoreFunction::Last), + predicates: PredicateListExpr { predicates: vec![] }, + })], + }), + }], + }, + }), + ], + }), + ), + ]; + + for (input, expected) in cases { + match parse(input) { + Ok(result) => { + assert_eq!(result, expected); + }, + Err(e) => panic!("Failed to parse '{}': {:?}", input, e), + } + } + } +} diff --git a/resources/prefs.json b/resources/prefs.json index ae34fd3a663..274aa238dde 100644 --- a/resources/prefs.json +++ b/resources/prefs.json @@ -54,6 +54,7 @@ "dom.webxr.test": false, "dom.webxr.unsafe-assume-user-intent": false, "dom.worklet.timeout_ms": 10, + "dom.xpath.enabled": false, "gfx.subpixel-text-antialiasing.enabled": true, "gfx.texture-swizzling.enabled": true, "js.asmjs.enabled": true, diff --git a/tests/wpt/include.ini b/tests/wpt/include.ini index 2966fc0ac36..7b10a1e5762 100644 --- a/tests/wpt/include.ini +++ b/tests/wpt/include.ini @@ -131,6 +131,8 @@ skip: true skip: false [domparsing] skip: false +[domxpath] + skip: false [encoding] skip: false [eventsource] diff --git a/tests/wpt/meta/__dir__.ini b/tests/wpt/meta/__dir__.ini index 1061efabecf..bb3e3d73f05 100644 --- a/tests/wpt/meta/__dir__.ini +++ b/tests/wpt/meta/__dir__.ini @@ -1 +1 @@ -prefs: ["dom.imagebitmap.enabled:true", "dom.offscreen_canvas.enabled:true", "dom.shadowdom.enabled:true"] +prefs: ["dom.imagebitmap.enabled:true", "dom.offscreen_canvas.enabled:true", "dom.shadowdom.enabled:true", "dom.xpath.enabled:true"] diff --git a/tests/wpt/meta/dom/idlharness.window.js.ini b/tests/wpt/meta/dom/idlharness.window.js.ini index b07aaa60784..5a904bf2d2b 100644 --- a/tests/wpt/meta/dom/idlharness.window.js.ini +++ b/tests/wpt/meta/dom/idlharness.window.js.ini @@ -1,39 +1,15 @@ [idlharness.window.html?include=Node] - [idl_test setup] - expected: FAIL - [idlharness.window.html?exclude=Node] - [XPathExpression interface: document.createExpression("//*") must inherit property "evaluate(Node, optional unsigned short, optional XPathResult?)" with the proper type] - expected: FAIL - - [XPathResult interface: attribute singleNodeValue] - expected: FAIL - - [XPathResult interface object name] - expected: FAIL - [Element interface: operation replaceWith((Node or DOMString)...)] expected: FAIL - [Document interface: operation createNSResolver(Node)] - expected: FAIL - [Document interface: operation prepend((Node or DOMString)...)] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "snapshotLength" with the proper type] - expected: FAIL - - [XPathEvaluator interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - [AbortSignal must be primary interface of new AbortController().signal] expected: FAIL - [XPathResult interface: constant NUMBER_TYPE on interface object] - expected: FAIL - [Element interface: element must inherit property "assignedSlot" with the proper type] expected: FAIL @@ -46,9 +22,6 @@ [Event interface: attribute composed] expected: FAIL - [XPathResult interface: constant STRING_TYPE on interface object] - expected: FAIL - [Text interface: attribute assignedSlot] expected: FAIL @@ -61,12 +34,6 @@ [AbortSignal interface: existence and properties of interface object] expected: FAIL - [XPathResult interface: constant STRING_TYPE on interface prototype object] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "ORDERED_NODE_ITERATOR_TYPE" with the proper type] - expected: FAIL - [EventTarget interface: calling removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean)) on new AbortController().signal with too few arguments must throw TypeError] expected: FAIL @@ -82,66 +49,27 @@ [Event interface: new CustomEvent("foo") must inherit property "composed" with the proper type] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "UNORDERED_NODE_ITERATOR_TYPE" with the proper type] - expected: FAIL - [EventTarget interface: new AbortController().signal must inherit property "removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))" with the proper type] expected: FAIL - [Document interface: xmlDoc must inherit property "createNSResolver(Node)" with the proper type] - expected: FAIL - - [XPathResult interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [XPathEvaluator interface: operation createExpression(DOMString, optional XPathNSResolver?)] - expected: FAIL - [AbortController interface: operation abort()] expected: FAIL [AbortController interface: existence and properties of interface object] expected: FAIL - [XPathResult interface: constant UNORDERED_NODE_ITERATOR_TYPE on interface prototype object] - expected: FAIL - [Element interface: operation prepend((Node or DOMString)...)] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "ANY_UNORDERED_NODE_TYPE" with the proper type] - expected: FAIL - [AbortController interface: attribute signal] expected: FAIL - [XPathResult interface: constant UNORDERED_NODE_SNAPSHOT_TYPE on interface prototype object] - expected: FAIL - - [XPathResult interface: constant ORDERED_NODE_SNAPSHOT_TYPE on interface object] - expected: FAIL - - [XPathResult interface: constant ANY_TYPE on interface prototype object] - expected: FAIL - [CharacterData interface: operation before((Node or DOMString)...)] expected: FAIL [AbortSignal interface: attribute aborted] expected: FAIL - [XPathResult interface: operation snapshotItem(unsigned long)] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "ANY_TYPE" with the proper type] - expected: FAIL - - [XPathEvaluator interface: operation evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?)] - expected: FAIL - - [XPathResult interface: existence and properties of interface object] - expected: FAIL - [Element interface: operation after((Node or DOMString)...)] expected: FAIL @@ -154,51 +82,18 @@ [Element interface: attribute assignedSlot] expected: FAIL - [XPathEvaluator interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [XPathExpression interface: existence and properties of interface prototype object] - expected: FAIL - - [XPathResult interface: constant ORDERED_NODE_ITERATOR_TYPE on interface object] - expected: FAIL - [CharacterData interface: existence and properties of interface prototype object's @@unscopables property] expected: FAIL [CharacterData interface: operation remove()] expected: FAIL - [XPathEvaluator interface: existence and properties of interface object] - expected: FAIL - - [XPathResult interface: calling snapshotItem(unsigned long) on document.evaluate("//*", document.body) with too few arguments must throw TypeError] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "snapshotItem(unsigned long)" with the proper type] - expected: FAIL - [Element interface: element must inherit property "slot" with the proper type] expected: FAIL - [Document interface: new Document() must inherit property "evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?)" with the proper type] - expected: FAIL - - [Document interface: xmlDoc must inherit property "evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?)" with the proper type] - expected: FAIL - - [XPathExpression interface: operation evaluate(Node, optional unsigned short, optional XPathResult?)] - expected: FAIL - [AbortController interface: new AbortController() must inherit property "signal" with the proper type] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "iterateNext()" with the proper type] - expected: FAIL - - [Document interface: calling createNSResolver(Node) on new Document() with too few arguments must throw TypeError] - expected: FAIL - [ShadowRoot interface: attribute onslotchange] expected: FAIL @@ -208,234 +103,72 @@ [Element interface: attribute slot] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "invalidIteratorState" with the proper type] - expected: FAIL - [DocumentType interface: existence and properties of interface prototype object's @@unscopables property] expected: FAIL - [Document interface: calling evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?) on new Document() with too few arguments must throw TypeError] - expected: FAIL - [AbortController interface object length] expected: FAIL - [XPathExpression interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [XPathResult interface object length] - expected: FAIL - - [Stringification of document.createExpression("//*")] - expected: FAIL - - [XPathResult must be primary interface of document.evaluate("//*", document.body)] - expected: FAIL - [CharacterData interface: operation replaceWith((Node or DOMString)...)] expected: FAIL - [XPathResult interface: existence and properties of interface prototype object] - expected: FAIL - - [XPathResult interface: attribute resultType] - expected: FAIL - - [Document interface: xmlDoc must inherit property "createExpression(DOMString, optional XPathNSResolver?)" with the proper type] - expected: FAIL - - [XPathResult interface: constant FIRST_ORDERED_NODE_TYPE on interface prototype object] - expected: FAIL - [DocumentType interface: operation replaceWith((Node or DOMString)...)] expected: FAIL [EventTarget interface: calling dispatchEvent(Event) on new AbortController().signal with too few arguments must throw TypeError] expected: FAIL - [XPathResult interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - [AbortController interface: existence and properties of interface prototype object] expected: FAIL - [Document interface: new Document() must inherit property "createNSResolver(Node)" with the proper type] - expected: FAIL - - [XPathResult interface: constant ORDERED_NODE_ITERATOR_TYPE on interface prototype object] - expected: FAIL - - [XPathResult interface: attribute stringValue] - expected: FAIL - - [XPathResult interface: attribute booleanValue] - expected: FAIL - - [XPathEvaluator interface: calling createExpression(DOMString, optional XPathNSResolver?) on new XPathEvaluator() with too few arguments must throw TypeError] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "UNORDERED_NODE_SNAPSHOT_TYPE" with the proper type] - expected: FAIL - [DocumentFragment interface: existence and properties of interface prototype object's @@unscopables property] expected: FAIL - [XPathEvaluator interface: operation createNSResolver(Node)] - expected: FAIL - - [Document interface: calling createNSResolver(Node) on xmlDoc with too few arguments must throw TypeError] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "booleanValue" with the proper type] - expected: FAIL - [DocumentType interface: operation before((Node or DOMString)...)] expected: FAIL - [XPathResult interface: constant ORDERED_NODE_SNAPSHOT_TYPE on interface prototype object] - expected: FAIL - [AbortSignal interface: new AbortController().signal must inherit property "aborted" with the proper type] expected: FAIL [EventTarget interface: calling addEventListener(DOMString, EventListener?, optional (AddEventListenerOptions or boolean)) on new AbortController().signal with too few arguments must throw TypeError] expected: FAIL - [Document interface: calling createExpression(DOMString, optional XPathNSResolver?) on xmlDoc with too few arguments must throw TypeError] - expected: FAIL - - [XPathResult interface: attribute snapshotLength] - expected: FAIL - - [XPathEvaluator interface: new XPathEvaluator() must inherit property "createNSResolver(Node)" with the proper type] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "resultType" with the proper type] - expected: FAIL - - [XPathEvaluator interface object length] - expected: FAIL - [AbortSignal interface: existence and properties of interface prototype object's "constructor" property] expected: FAIL - [XPathEvaluator interface: new XPathEvaluator() must inherit property "evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?)" with the proper type] - expected: FAIL - - [XPathEvaluator interface: calling evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?) on new XPathEvaluator() with too few arguments must throw TypeError] - expected: FAIL - [AbortSignal interface: new AbortController().signal must inherit property "onabort" with the proper type] expected: FAIL - [Document interface: calling evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?) on xmlDoc with too few arguments must throw TypeError] - expected: FAIL - - [XPathResult interface: constant UNORDERED_NODE_SNAPSHOT_TYPE on interface object] - expected: FAIL - [AbortController interface: existence and properties of interface prototype object's "constructor" property] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "stringValue" with the proper type] - expected: FAIL - - [XPathResult interface: attribute invalidIteratorState] - expected: FAIL - [AbortSignal interface: attribute onabort] expected: FAIL - [Document interface: operation evaluate(DOMString, Node, optional XPathNSResolver?, optional unsigned short, optional XPathResult?)] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "singleNodeValue" with the proper type] - expected: FAIL - - [Document interface: operation createExpression(DOMString, optional XPathNSResolver?)] - expected: FAIL - [AbortSignal interface object length] expected: FAIL - [XPathEvaluator interface: calling createNSResolver(Node) on new XPathEvaluator() with too few arguments must throw TypeError] - expected: FAIL - [AbortSignal interface: existence and properties of interface prototype object] expected: FAIL - [XPathResult interface: attribute numberValue] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "numberValue" with the proper type] - expected: FAIL - - [XPathResult interface: constant NUMBER_TYPE on interface prototype object] - expected: FAIL - - [XPathExpression interface object name] - expected: FAIL - [Event interface: new Event("foo") must inherit property "composed" with the proper type] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "BOOLEAN_TYPE" with the proper type] - expected: FAIL - - [Document interface: new Document() must inherit property "createExpression(DOMString, optional XPathNSResolver?)" with the proper type] - expected: FAIL - - [XPathResult interface: constant BOOLEAN_TYPE on interface prototype object] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "STRING_TYPE" with the proper type] - expected: FAIL - [Element interface: operation remove()] expected: FAIL [DocumentFragment interface: operation replaceChildren((Node or DOMString)...)] expected: FAIL - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "NUMBER_TYPE" with the proper type] - expected: FAIL - [NodeFilter interface: existence and properties of interface object] expected: FAIL - [XPathEvaluator interface object name] - expected: FAIL - - [XPathExpression interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [Stringification of document.evaluate("//*", document.body)] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "ORDERED_NODE_SNAPSHOT_TYPE" with the proper type] - expected: FAIL - [Stringification of new AbortController().signal] expected: FAIL - [XPathResult interface: constant ANY_UNORDERED_NODE_TYPE on interface prototype object] - expected: FAIL - - [Document interface: calling createExpression(DOMString, optional XPathNSResolver?) on new Document() with too few arguments must throw TypeError] - expected: FAIL - - [XPathEvaluator interface: new XPathEvaluator() must inherit property "createExpression(DOMString, optional XPathNSResolver?)" with the proper type] - expected: FAIL - [CharacterData interface: operation after((Node or DOMString)...)] expected: FAIL - [XPathExpression interface object length] - expected: FAIL - - [Stringification of new XPathEvaluator()] - expected: FAIL - [Element interface: operation replaceChildren((Node or DOMString)...)] expected: FAIL @@ -445,66 +178,30 @@ [Document interface: operation replaceChildren((Node or DOMString)...)] expected: FAIL - [XPathResult interface: constant ANY_UNORDERED_NODE_TYPE on interface object] - expected: FAIL - - [XPathExpression must be primary interface of document.createExpression("//*")] - expected: FAIL - - [XPathResult interface: document.evaluate("//*", document.body) must inherit property "FIRST_ORDERED_NODE_TYPE" with the proper type] - expected: FAIL - [Stringification of new AbortController()] expected: FAIL - [XPathExpression interface: calling evaluate(Node, optional unsigned short, optional XPathResult?) on document.createExpression("//*") with too few arguments must throw TypeError] - expected: FAIL - [Document interface: operation append((Node or DOMString)...)] expected: FAIL - [XPathResult interface: operation iterateNext()] - expected: FAIL - [Event interface: document.createEvent("Event") must inherit property "composed" with the proper type] expected: FAIL - [XPathResult interface: constant UNORDERED_NODE_ITERATOR_TYPE on interface object] - expected: FAIL - [DocumentType interface: operation remove()] expected: FAIL - [XPathResult interface: constant ANY_TYPE on interface object] - expected: FAIL - - [XPathResult interface: constant FIRST_ORDERED_NODE_TYPE on interface object] - expected: FAIL - [DocumentType interface: operation after((Node or DOMString)...)] expected: FAIL - [XPathEvaluator must be primary interface of new XPathEvaluator()] - expected: FAIL - - [XPathExpression interface: existence and properties of interface object] - expected: FAIL - [Element interface: existence and properties of interface prototype object's @@unscopables property] expected: FAIL [DocumentFragment interface: operation append((Node or DOMString)...)] expected: FAIL - [XPathResult interface: constant BOOLEAN_TYPE on interface object] - expected: FAIL - [Element interface: operation append((Node or DOMString)...)] expected: FAIL - [XPathEvaluator interface: existence and properties of interface prototype object] - expected: FAIL - [Element interface: operation before((Node or DOMString)...)] expected: FAIL @@ -649,9 +346,6 @@ [AbortSignal interface: new AbortController().signal must inherit property "throwIfAborted()" with the proper type] expected: FAIL - [idl_test setup] - expected: FAIL - [AbortSignal interface: operation timeout(unsigned long long)] expected: FAIL diff --git a/tests/wpt/meta/domxpath/document.tentative.html.ini b/tests/wpt/meta/domxpath/document.tentative.html.ini new file mode 100644 index 00000000000..32710026d6e --- /dev/null +++ b/tests/wpt/meta/domxpath/document.tentative.html.ini @@ -0,0 +1,3 @@ +[document.tentative.html] + [XPath parent of documentElement] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/fn-lang.html.ini b/tests/wpt/meta/domxpath/fn-lang.html.ini new file mode 100644 index 00000000000..31754463dcc --- /dev/null +++ b/tests/wpt/meta/domxpath/fn-lang.html.ini @@ -0,0 +1,21 @@ +[fn-lang.html] + [lang("en"): <root><match lang="en"></match></root>] + expected: FAIL + + [lang("en"): <root><match lang="EN"></match></root>] + expected: FAIL + + [lang("en"): <root><match lang="en-us"></match></root>] + expected: FAIL + + [lang("en"): <root><unmatch></unmatch></root>] + expected: FAIL + + [lang("ja"): <root lang="ja"><match></match></root>] + expected: FAIL + + [lang("ja"): <root lang="ja-jp"><unmatch lang="ja_JP"></unmatch></root>] + expected: FAIL + + [lang("ko"): <root><unmatch lang="Ko"></unmatch></root>] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/lexical-structure.html.ini b/tests/wpt/meta/domxpath/lexical-structure.html.ini new file mode 100644 index 00000000000..200dac9f2ee --- /dev/null +++ b/tests/wpt/meta/domxpath/lexical-structure.html.ini @@ -0,0 +1,6 @@ +[lexical-structure.html] + [Literal: Only ' and " should be handled as literal quotes.] + expected: FAIL + + [ExprWhitespace: Only #x20 #x9 #xD or #xA must be handled as a whitespace.] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/node-sets.html.ini b/tests/wpt/meta/domxpath/node-sets.html.ini new file mode 100644 index 00000000000..856f22dd655 --- /dev/null +++ b/tests/wpt/meta/domxpath/node-sets.html.ini @@ -0,0 +1,3 @@ +[node-sets.html] + [| operator should evaluate both sides of expressions with the same context node] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/predicates.html.ini b/tests/wpt/meta/domxpath/predicates.html.ini new file mode 100644 index 00000000000..1fb6144e7db --- /dev/null +++ b/tests/wpt/meta/domxpath/predicates.html.ini @@ -0,0 +1,3 @@ +[predicates.html] + [An expression in a predicate should not change the context node] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/resolver-callback-interface-cross-realm.tentative.html.ini b/tests/wpt/meta/domxpath/resolver-callback-interface-cross-realm.tentative.html.ini new file mode 100644 index 00000000000..d129b32d4a2 --- /dev/null +++ b/tests/wpt/meta/domxpath/resolver-callback-interface-cross-realm.tentative.html.ini @@ -0,0 +1,15 @@ +[resolver-callback-interface-cross-realm.tentative.html] + [XPathNSResolver is cross-realm plain object without 'lookupNamespaceURI' property] + expected: FAIL + + [XPathNSResolver is cross-realm plain object with non-callable 'lookupNamespaceURI' property] + expected: FAIL + + [XPathNSResolver is cross-realm non-callable revoked Proxy] + expected: FAIL + + [XPathNSResolver is cross-realm callable revoked Proxy] + expected: FAIL + + [XPathNSResolver is cross-realm plain object with revoked Proxy as 'lookupNamespaceURI' property] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/resolver-callback-interface.html.ini b/tests/wpt/meta/domxpath/resolver-callback-interface.html.ini new file mode 100644 index 00000000000..0410fd68897 --- /dev/null +++ b/tests/wpt/meta/domxpath/resolver-callback-interface.html.ini @@ -0,0 +1,30 @@ +[resolver-callback-interface.html] + [callable resolver] + expected: FAIL + + [callable resolver: result is not cached] + expected: FAIL + + [callable resolver: abrupt completion from Call] + expected: FAIL + + [callable resolver: no 'lookupNamespaceURI' lookups] + expected: FAIL + + [object resolver] + expected: FAIL + + [object resolver: this value and `prefix` argument] + expected: FAIL + + [object resolver: 'lookupNamespaceURI' is not cached] + expected: FAIL + + [object resolver: abrupt completion from Get] + expected: FAIL + + [object resolver: 'lookupNamespaceURI' is thruthy and not callable] + expected: FAIL + + [object resolver: 'lookupNamespaceURI' is falsy and not callable] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/resolver-non-string-result.html.ini b/tests/wpt/meta/domxpath/resolver-non-string-result.html.ini new file mode 100644 index 00000000000..bfbdbe956d2 --- /dev/null +++ b/tests/wpt/meta/domxpath/resolver-non-string-result.html.ini @@ -0,0 +1,18 @@ +[resolver-non-string-result.html] + [undefined] + expected: FAIL + + [null] + expected: FAIL + + [number] + expected: FAIL + + [boolean] + expected: FAIL + + [symbol] + expected: FAIL + + [object coercion (abrupt completion)] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/text-html-attributes.html.ini b/tests/wpt/meta/domxpath/text-html-attributes.html.ini new file mode 100644 index 00000000000..803f418282a --- /dev/null +++ b/tests/wpt/meta/domxpath/text-html-attributes.html.ini @@ -0,0 +1,39 @@ +[text-html-attributes.html] + [Select html element based on attribute] + expected: FAIL + + [Select html element based on attribute mixed case] + expected: FAIL + + [Select both HTML and SVG elements based on attribute] + expected: FAIL + + [Select HTML element with non-ascii attribute 1] + expected: FAIL + + [Select HTML element with non-ascii attribute 2] + expected: FAIL + + [Select HTML element with non-ascii attribute 3] + expected: FAIL + + [Select SVG element based on mixed case attribute] + expected: FAIL + + [Select both HTML and SVG elements based on mixed case attribute] + expected: FAIL + + [Select SVG elements with refX attribute] + expected: FAIL + + [Select SVG element with non-ascii attribute 1] + expected: FAIL + + [Select SVG element with non-ascii attribute 2] + expected: FAIL + + [xmlns attribute] + expected: FAIL + + [svg element with XLink attribute] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/text-html-elements.html.ini b/tests/wpt/meta/domxpath/text-html-elements.html.ini new file mode 100644 index 00000000000..2da633f3c2f --- /dev/null +++ b/tests/wpt/meta/domxpath/text-html-elements.html.ini @@ -0,0 +1,24 @@ +[text-html-elements.html] + [HTML elements namespace prefix] + expected: FAIL + + [HTML elements mixed use of prefix] + expected: FAIL + + [SVG elements namespace prefix] + expected: FAIL + + [HTML elements mixed case] + expected: FAIL + + [SVG elements mixed case selector] + expected: FAIL + + [Non-ascii HTML element] + expected: FAIL + + [Non-ascii HTML element3] + expected: FAIL + + [Throw with invalid prefix] + expected: FAIL diff --git a/tests/wpt/meta/domxpath/xml_xpath_runner.html.ini b/tests/wpt/meta/domxpath/xml_xpath_runner.html.ini new file mode 100644 index 00000000000..e7d698afb6b --- /dev/null +++ b/tests/wpt/meta/domxpath/xml_xpath_runner.html.ini @@ -0,0 +1,3072 @@ +[xml_xpath_runner.html] + [XPath tests] + expected: FAIL + + [XPath tests 1] + expected: FAIL + + [XPath tests 2] + expected: FAIL + + [XPath tests 3] + expected: FAIL + + [XPath tests 4] + expected: FAIL + + [XPath tests 5] + expected: FAIL + + [XPath tests 6] + expected: FAIL + + [XPath tests 7] + expected: FAIL + + [XPath tests 8] + expected: FAIL + + [XPath tests 9] + expected: FAIL + + [XPath tests 10] + expected: FAIL + + [XPath tests 11] + expected: FAIL + + [XPath tests 12] + expected: FAIL + + [XPath tests 13] + expected: FAIL + + [XPath tests 14] + expected: FAIL + + [XPath tests 15] + expected: FAIL + + [XPath tests 16] + expected: FAIL + + [XPath tests 17] + expected: FAIL + + [XPath tests 18] + expected: FAIL + + [XPath tests 19] + expected: FAIL + + [XPath tests 20] + expected: FAIL + + [XPath tests 21] + expected: FAIL + + [XPath tests 22] + expected: FAIL + + [XPath tests 23] + expected: FAIL + + [XPath tests 24] + expected: FAIL + + [XPath tests 25] + expected: FAIL + + [XPath tests 26] + expected: FAIL + + [XPath tests 27] + expected: FAIL + + [XPath tests 28] + expected: FAIL + + [XPath tests 29] + expected: FAIL + + [XPath tests 30] + expected: FAIL + + [XPath tests 31] + expected: FAIL + + [XPath tests 32] + expected: FAIL + + [XPath tests 33] + expected: FAIL + + [XPath tests 34] + expected: FAIL + + [XPath tests 35] + expected: FAIL + + [XPath tests 36] + expected: FAIL + + [XPath tests 37] + expected: FAIL + + [XPath tests 38] + expected: FAIL + + [XPath tests 39] + expected: FAIL + + [XPath tests 40] + expected: FAIL + + [XPath tests 41] + expected: FAIL + + [XPath tests 42] + expected: FAIL + + [XPath tests 43] + expected: FAIL + + [XPath tests 44] + expected: FAIL + + [XPath tests 45] + expected: FAIL + + [XPath tests 46] + expected: FAIL + + [XPath tests 47] + expected: FAIL + + [XPath tests 48] + expected: FAIL + + [XPath tests 49] + expected: FAIL + + [XPath tests 50] + expected: FAIL + + [XPath tests 51] + expected: FAIL + + [XPath tests 52] + expected: FAIL + + [XPath tests 53] + expected: FAIL + + [XPath tests 54] + expected: FAIL + + [XPath tests 55] + expected: FAIL + + [XPath tests 56] + expected: FAIL + + [XPath tests 57] + expected: FAIL + + [XPath tests 58] + expected: FAIL + + [XPath tests 59] + expected: FAIL + + [XPath tests 60] + expected: FAIL + + [XPath tests 61] + expected: FAIL + + [XPath tests 62] + expected: FAIL + + [XPath tests 63] + expected: FAIL + + [XPath tests 64] + expected: FAIL + + [XPath tests 65] + expected: FAIL + + [XPath tests 66] + expected: FAIL + + [XPath tests 67] + expected: FAIL + + [XPath tests 68] + expected: FAIL + + [XPath tests 69] + expected: FAIL + + [XPath tests 70] + expected: FAIL + + [XPath tests 71] + expected: FAIL + + [XPath tests 72] + expected: FAIL + + [XPath tests 73] + expected: FAIL + + [XPath tests 74] + expected: FAIL + + [XPath tests 75] + expected: FAIL + + [XPath tests 76] + expected: FAIL + + [XPath tests 77] + expected: FAIL + + [XPath tests 78] + expected: FAIL + + [XPath tests 79] + expected: FAIL + + [XPath tests 80] + expected: FAIL + + [XPath tests 81] + expected: FAIL + + [XPath tests 82] + expected: FAIL + + [XPath tests 83] + expected: FAIL + + [XPath tests 84] + expected: FAIL + + [XPath tests 85] + expected: FAIL + + [XPath tests 86] + expected: FAIL + + [XPath tests 87] + expected: FAIL + + [XPath tests 88] + expected: FAIL + + [XPath tests 89] + expected: FAIL + + [XPath tests 90] + expected: FAIL + + [XPath tests 91] + expected: FAIL + + [XPath tests 92] + expected: FAIL + + [XPath tests 93] + expected: FAIL + + [XPath tests 94] + expected: FAIL + + [XPath tests 95] + expected: FAIL + + [XPath tests 96] + expected: FAIL + + [XPath tests 97] + expected: FAIL + + [XPath tests 98] + expected: FAIL + + [XPath tests 99] + expected: FAIL + + [XPath tests 100] + expected: FAIL + + [XPath tests 101] + expected: FAIL + + [XPath tests 102] + expected: FAIL + + [XPath tests 103] + expected: FAIL + + [XPath tests 104] + expected: FAIL + + [XPath tests 105] + expected: FAIL + + [XPath tests 106] + expected: FAIL + + [XPath tests 107] + expected: FAIL + + [XPath tests 108] + expected: FAIL + + [XPath tests 109] + expected: FAIL + + [XPath tests 110] + expected: FAIL + + [XPath tests 111] + expected: FAIL + + [XPath tests 112] + expected: FAIL + + [XPath tests 113] + expected: FAIL + + [XPath tests 114] + expected: FAIL + + [XPath tests 115] + expected: FAIL + + [XPath tests 116] + expected: FAIL + + [XPath tests 117] + expected: FAIL + + [XPath tests 118] + expected: FAIL + + [XPath tests 119] + expected: FAIL + + [XPath tests 120] + expected: FAIL + + [XPath tests 121] + expected: FAIL + + [XPath tests 122] + expected: FAIL + + [XPath tests 123] + expected: FAIL + + [XPath tests 124] + expected: FAIL + + [XPath tests 125] + expected: FAIL + + [XPath tests 126] + expected: FAIL + + [XPath tests 127] + expected: FAIL + + [XPath tests 128] + expected: FAIL + + [XPath tests 129] + expected: FAIL + + [XPath tests 130] + expected: FAIL + + [XPath tests 131] + expected: FAIL + + [XPath tests 132] + expected: FAIL + + [XPath tests 133] + expected: FAIL + + [XPath tests 134] + expected: FAIL + + [XPath tests 135] + expected: FAIL + + [XPath tests 136] + expected: FAIL + + [XPath tests 137] + expected: FAIL + + [XPath tests 138] + expected: FAIL + + [XPath tests 139] + expected: FAIL + + [XPath tests 140] + expected: FAIL + + [XPath tests 141] + expected: FAIL + + [XPath tests 142] + expected: FAIL + + [XPath tests 143] + expected: FAIL + + [XPath tests 144] + expected: FAIL + + [XPath tests 145] + expected: FAIL + + [XPath tests 146] + expected: FAIL + + [XPath tests 147] + expected: FAIL + + [XPath tests 148] + expected: FAIL + + [XPath tests 149] + expected: FAIL + + [XPath tests 150] + expected: FAIL + + [XPath tests 151] + expected: FAIL + + [XPath tests 152] + expected: FAIL + + [XPath tests 153] + expected: FAIL + + [XPath tests 154] + expected: FAIL + + [XPath tests 155] + expected: FAIL + + [XPath tests 156] + expected: FAIL + + [XPath tests 157] + expected: FAIL + + [XPath tests 158] + expected: FAIL + + [XPath tests 159] + expected: FAIL + + [XPath tests 160] + expected: FAIL + + [XPath tests 161] + expected: FAIL + + [XPath tests 162] + expected: FAIL + + [XPath tests 163] + expected: FAIL + + [XPath tests 164] + expected: FAIL + + [XPath tests 165] + expected: FAIL + + [XPath tests 166] + expected: FAIL + + [XPath tests 167] + expected: FAIL + + [XPath tests 168] + expected: FAIL + + [XPath tests 169] + expected: FAIL + + [XPath tests 170] + expected: FAIL + + [XPath tests 171] + expected: FAIL + + [XPath tests 172] + expected: FAIL + + [XPath tests 173] + expected: FAIL + + [XPath tests 174] + expected: FAIL + + [XPath tests 175] + expected: FAIL + + [XPath tests 176] + expected: FAIL + + [XPath tests 177] + expected: FAIL + + [XPath tests 178] + expected: FAIL + + [XPath tests 179] + expected: FAIL + + [XPath tests 180] + expected: FAIL + + [XPath tests 181] + expected: FAIL + + [XPath tests 182] + expected: FAIL + + [XPath tests 183] + expected: FAIL + + [XPath tests 184] + expected: FAIL + + [XPath tests 185] + expected: FAIL + + [XPath tests 186] + expected: FAIL + + [XPath tests 187] + expected: FAIL + + [XPath tests 188] + expected: FAIL + + [XPath tests 189] + expected: FAIL + + [XPath tests 190] + expected: FAIL + + [XPath tests 191] + expected: FAIL + + [XPath tests 192] + expected: FAIL + + [XPath tests 193] + expected: FAIL + + [XPath tests 194] + expected: FAIL + + [XPath tests 195] + expected: FAIL + + [XPath tests 196] + expected: FAIL + + [XPath tests 197] + expected: FAIL + + [XPath tests 198] + expected: FAIL + + [XPath tests 199] + expected: FAIL + + [XPath tests 200] + expected: FAIL + + [XPath tests 201] + expected: FAIL + + [XPath tests 202] + expected: FAIL + + [XPath tests 203] + expected: FAIL + + [XPath tests 204] + expected: FAIL + + [XPath tests 205] + expected: FAIL + + [XPath tests 206] + expected: FAIL + + [XPath tests 207] + expected: FAIL + + [XPath tests 208] + expected: FAIL + + [XPath tests 209] + expected: FAIL + + [XPath tests 210] + expected: FAIL + + [XPath tests 211] + expected: FAIL + + [XPath tests 212] + expected: FAIL + + [XPath tests 213] + expected: FAIL + + [XPath tests 214] + expected: FAIL + + [XPath tests 215] + expected: FAIL + + [XPath tests 216] + expected: FAIL + + [XPath tests 217] + expected: FAIL + + [XPath tests 218] + expected: FAIL + + [XPath tests 219] + expected: FAIL + + [XPath tests 220] + expected: FAIL + + [XPath tests 221] + expected: FAIL + + [XPath tests 222] + expected: FAIL + + [XPath tests 223] + expected: FAIL + + [XPath tests 224] + expected: FAIL + + [XPath tests 225] + expected: FAIL + + [XPath tests 226] + expected: FAIL + + [XPath tests 227] + expected: FAIL + + [XPath tests 228] + expected: FAIL + + [XPath tests 229] + expected: FAIL + + [XPath tests 230] + expected: FAIL + + [XPath tests 231] + expected: FAIL + + [XPath tests 232] + expected: FAIL + + [XPath tests 233] + expected: FAIL + + [XPath tests 234] + expected: FAIL + + [XPath tests 235] + expected: FAIL + + [XPath tests 236] + expected: FAIL + + [XPath tests 237] + expected: FAIL + + [XPath tests 238] + expected: FAIL + + [XPath tests 239] + expected: FAIL + + [XPath tests 240] + expected: FAIL + + [XPath tests 241] + expected: FAIL + + [XPath tests 242] + expected: FAIL + + [XPath tests 243] + expected: FAIL + + [XPath tests 244] + expected: FAIL + + [XPath tests 245] + expected: FAIL + + [XPath tests 246] + expected: FAIL + + [XPath tests 247] + expected: FAIL + + [XPath tests 248] + expected: FAIL + + [XPath tests 249] + expected: FAIL + + [XPath tests 250] + expected: FAIL + + [XPath tests 251] + expected: FAIL + + [XPath tests 252] + expected: FAIL + + [XPath tests 253] + expected: FAIL + + [XPath tests 254] + expected: FAIL + + [XPath tests 255] + expected: FAIL + + [XPath tests 256] + expected: FAIL + + [XPath tests 257] + expected: FAIL + + [XPath tests 258] + expected: FAIL + + [XPath tests 259] + expected: FAIL + + [XPath tests 260] + expected: FAIL + + [XPath tests 261] + expected: FAIL + + [XPath tests 262] + expected: FAIL + + [XPath tests 263] + expected: FAIL + + [XPath tests 264] + expected: FAIL + + [XPath tests 265] + expected: FAIL + + [XPath tests 266] + expected: FAIL + + [XPath tests 267] + expected: FAIL + + [XPath tests 268] + expected: FAIL + + [XPath tests 269] + expected: FAIL + + [XPath tests 270] + expected: FAIL + + [XPath tests 271] + expected: FAIL + + [XPath tests 272] + expected: FAIL + + [XPath tests 273] + expected: FAIL + + [XPath tests 274] + expected: FAIL + + [XPath tests 275] + expected: FAIL + + [XPath tests 276] + expected: FAIL + + [XPath tests 277] + expected: FAIL + + [XPath tests 278] + expected: FAIL + + [XPath tests 279] + expected: FAIL + + [XPath tests 280] + expected: FAIL + + [XPath tests 281] + expected: FAIL + + [XPath tests 282] + expected: FAIL + + [XPath tests 283] + expected: FAIL + + [XPath tests 284] + expected: FAIL + + [XPath tests 285] + expected: FAIL + + [XPath tests 286] + expected: FAIL + + [XPath tests 287] + expected: FAIL + + [XPath tests 288] + expected: FAIL + + [XPath tests 289] + expected: FAIL + + [XPath tests 290] + expected: FAIL + + [XPath tests 291] + expected: FAIL + + [XPath tests 292] + expected: FAIL + + [XPath tests 293] + expected: FAIL + + [XPath tests 294] + expected: FAIL + + [XPath tests 295] + expected: FAIL + + [XPath tests 296] + expected: FAIL + + [XPath tests 297] + expected: FAIL + + [XPath tests 298] + expected: FAIL + + [XPath tests 299] + expected: FAIL + + [XPath tests 300] + expected: FAIL + + [XPath tests 301] + expected: FAIL + + [XPath tests 302] + expected: FAIL + + [XPath tests 303] + expected: FAIL + + [XPath tests 304] + expected: FAIL + + [XPath tests 305] + expected: FAIL + + [XPath tests 306] + expected: FAIL + + [XPath tests 307] + expected: FAIL + + [XPath tests 308] + expected: FAIL + + [XPath tests 309] + expected: FAIL + + [XPath tests 310] + expected: FAIL + + [XPath tests 311] + expected: FAIL + + [XPath tests 312] + expected: FAIL + + [XPath tests 313] + expected: FAIL + + [XPath tests 314] + expected: FAIL + + [XPath tests 315] + expected: FAIL + + [XPath tests 316] + expected: FAIL + + [XPath tests 317] + expected: FAIL + + [XPath tests 318] + expected: FAIL + + [XPath tests 319] + expected: FAIL + + [XPath tests 320] + expected: FAIL + + [XPath tests 321] + expected: FAIL + + [XPath tests 322] + expected: FAIL + + [XPath tests 323] + expected: FAIL + + [XPath tests 324] + expected: FAIL + + [XPath tests 325] + expected: FAIL + + [XPath tests 326] + expected: FAIL + + [XPath tests 327] + expected: FAIL + + [XPath tests 328] + expected: FAIL + + [XPath tests 329] + expected: FAIL + + [XPath tests 330] + expected: FAIL + + [XPath tests 331] + expected: FAIL + + [XPath tests 332] + expected: FAIL + + [XPath tests 333] + expected: FAIL + + [XPath tests 334] + expected: FAIL + + [XPath tests 335] + expected: FAIL + + [XPath tests 336] + expected: FAIL + + [XPath tests 337] + expected: FAIL + + [XPath tests 338] + expected: FAIL + + [XPath tests 339] + expected: FAIL + + [XPath tests 340] + expected: FAIL + + [XPath tests 341] + expected: FAIL + + [XPath tests 342] + expected: FAIL + + [XPath tests 343] + expected: FAIL + + [XPath tests 344] + expected: FAIL + + [XPath tests 345] + expected: FAIL + + [XPath tests 346] + expected: FAIL + + [XPath tests 347] + expected: FAIL + + [XPath tests 348] + expected: FAIL + + [XPath tests 349] + expected: FAIL + + [XPath tests 350] + expected: FAIL + + [XPath tests 351] + expected: FAIL + + [XPath tests 352] + expected: FAIL + + [XPath tests 353] + expected: FAIL + + [XPath tests 354] + expected: FAIL + + [XPath tests 355] + expected: FAIL + + [XPath tests 356] + expected: FAIL + + [XPath tests 357] + expected: FAIL + + [XPath tests 358] + expected: FAIL + + [XPath tests 359] + expected: FAIL + + [XPath tests 360] + expected: FAIL + + [XPath tests 361] + expected: FAIL + + [XPath tests 362] + expected: FAIL + + [XPath tests 363] + expected: FAIL + + [XPath tests 364] + expected: FAIL + + [XPath tests 365] + expected: FAIL + + [XPath tests 366] + expected: FAIL + + [XPath tests 367] + expected: FAIL + + [XPath tests 368] + expected: FAIL + + [XPath tests 369] + expected: FAIL + + [XPath tests 370] + expected: FAIL + + [XPath tests 371] + expected: FAIL + + [XPath tests 372] + expected: FAIL + + [XPath tests 373] + expected: FAIL + + [XPath tests 374] + expected: FAIL + + [XPath tests 375] + expected: FAIL + + [XPath tests 376] + expected: FAIL + + [XPath tests 377] + expected: FAIL + + [XPath tests 378] + expected: FAIL + + [XPath tests 379] + expected: FAIL + + [XPath tests 380] + expected: FAIL + + [XPath tests 381] + expected: FAIL + + [XPath tests 382] + expected: FAIL + + [XPath tests 383] + expected: FAIL + + [XPath tests 384] + expected: FAIL + + [XPath tests 385] + expected: FAIL + + [XPath tests 386] + expected: FAIL + + [XPath tests 387] + expected: FAIL + + [XPath tests 388] + expected: FAIL + + [XPath tests 389] + expected: FAIL + + [XPath tests 390] + expected: FAIL + + [XPath tests 391] + expected: FAIL + + [XPath tests 392] + expected: FAIL + + [XPath tests 393] + expected: FAIL + + [XPath tests 394] + expected: FAIL + + [XPath tests 395] + expected: FAIL + + [XPath tests 396] + expected: FAIL + + [XPath tests 397] + expected: FAIL + + [XPath tests 398] + expected: FAIL + + [XPath tests 399] + expected: FAIL + + [XPath tests 400] + expected: FAIL + + [XPath tests 401] + expected: FAIL + + [XPath tests 402] + expected: FAIL + + [XPath tests 403] + expected: FAIL + + [XPath tests 404] + expected: FAIL + + [XPath tests 405] + expected: FAIL + + [XPath tests 406] + expected: FAIL + + [XPath tests 407] + expected: FAIL + + [XPath tests 408] + expected: FAIL + + [XPath tests 409] + expected: FAIL + + [XPath tests 410] + expected: FAIL + + [XPath tests 411] + expected: FAIL + + [XPath tests 412] + expected: FAIL + + [XPath tests 413] + expected: FAIL + + [XPath tests 414] + expected: FAIL + + [XPath tests 415] + expected: FAIL + + [XPath tests 416] + expected: FAIL + + [XPath tests 417] + expected: FAIL + + [XPath tests 418] + expected: FAIL + + [XPath tests 419] + expected: FAIL + + [XPath tests 420] + expected: FAIL + + [XPath tests 421] + expected: FAIL + + [XPath tests 422] + expected: FAIL + + [XPath tests 423] + expected: FAIL + + [XPath tests 424] + expected: FAIL + + [XPath tests 425] + expected: FAIL + + [XPath tests 426] + expected: FAIL + + [XPath tests 427] + expected: FAIL + + [XPath tests 428] + expected: FAIL + + [XPath tests 429] + expected: FAIL + + [XPath tests 430] + expected: FAIL + + [XPath tests 431] + expected: FAIL + + [XPath tests 432] + expected: FAIL + + [XPath tests 433] + expected: FAIL + + [XPath tests 434] + expected: FAIL + + [XPath tests 435] + expected: FAIL + + [XPath tests 436] + expected: FAIL + + [XPath tests 437] + expected: FAIL + + [XPath tests 438] + expected: FAIL + + [XPath tests 439] + expected: FAIL + + [XPath tests 440] + expected: FAIL + + [XPath tests 441] + expected: FAIL + + [XPath tests 442] + expected: FAIL + + [XPath tests 443] + expected: FAIL + + [XPath tests 444] + expected: FAIL + + [XPath tests 445] + expected: FAIL + + [XPath tests 446] + expected: FAIL + + [XPath tests 447] + expected: FAIL + + [XPath tests 448] + expected: FAIL + + [XPath tests 449] + expected: FAIL + + [XPath tests 450] + expected: FAIL + + [XPath tests 451] + expected: FAIL + + [XPath tests 452] + expected: FAIL + + [XPath tests 453] + expected: FAIL + + [XPath tests 454] + expected: FAIL + + [XPath tests 455] + expected: FAIL + + [XPath tests 456] + expected: FAIL + + [XPath tests 457] + expected: FAIL + + [XPath tests 458] + expected: FAIL + + [XPath tests 459] + expected: FAIL + + [XPath tests 460] + expected: FAIL + + [XPath tests 461] + expected: FAIL + + [XPath tests 462] + expected: FAIL + + [XPath tests 463] + expected: FAIL + + [XPath tests 464] + expected: FAIL + + [XPath tests 465] + expected: FAIL + + [XPath tests 466] + expected: FAIL + + [XPath tests 467] + expected: FAIL + + [XPath tests 468] + expected: FAIL + + [XPath tests 469] + expected: FAIL + + [XPath tests 470] + expected: FAIL + + [XPath tests 471] + expected: FAIL + + [XPath tests 472] + expected: FAIL + + [XPath tests 473] + expected: FAIL + + [XPath tests 474] + expected: FAIL + + [XPath tests 475] + expected: FAIL + + [XPath tests 476] + expected: FAIL + + [XPath tests 477] + expected: FAIL + + [XPath tests 478] + expected: FAIL + + [XPath tests 479] + expected: FAIL + + [XPath tests 480] + expected: FAIL + + [XPath tests 481] + expected: FAIL + + [XPath tests 482] + expected: FAIL + + [XPath tests 483] + expected: FAIL + + [XPath tests 484] + expected: FAIL + + [XPath tests 485] + expected: FAIL + + [XPath tests 486] + expected: FAIL + + [XPath tests 487] + expected: FAIL + + [XPath tests 488] + expected: FAIL + + [XPath tests 489] + expected: FAIL + + [XPath tests 490] + expected: FAIL + + [XPath tests 491] + expected: FAIL + + [XPath tests 492] + expected: FAIL + + [XPath tests 493] + expected: FAIL + + [XPath tests 494] + expected: FAIL + + [XPath tests 495] + expected: FAIL + + [XPath tests 496] + expected: FAIL + + [XPath tests 497] + expected: FAIL + + [XPath tests 498] + expected: FAIL + + [XPath tests 499] + expected: FAIL + + [XPath tests 500] + expected: FAIL + + [XPath tests 501] + expected: FAIL + + [XPath tests 502] + expected: FAIL + + [XPath tests 503] + expected: FAIL + + [XPath tests 504] + expected: FAIL + + [XPath tests 505] + expected: FAIL + + [XPath tests 506] + expected: FAIL + + [XPath tests 507] + expected: FAIL + + [XPath tests 508] + expected: FAIL + + [XPath tests 509] + expected: FAIL + + [XPath tests 510] + expected: FAIL + + [XPath tests 511] + expected: FAIL + + [XPath tests 512] + expected: FAIL + + [XPath tests 513] + expected: FAIL + + [XPath tests 514] + expected: FAIL + + [XPath tests 515] + expected: FAIL + + [XPath tests 516] + expected: FAIL + + [XPath tests 517] + expected: FAIL + + [XPath tests 518] + expected: FAIL + + [XPath tests 519] + expected: FAIL + + [XPath tests 520] + expected: FAIL + + [XPath tests 521] + expected: FAIL + + [XPath tests 522] + expected: FAIL + + [XPath tests 523] + expected: FAIL + + [XPath tests 524] + expected: FAIL + + [XPath tests 525] + expected: FAIL + + [XPath tests 526] + expected: FAIL + + [XPath tests 527] + expected: FAIL + + [XPath tests 528] + expected: FAIL + + [XPath tests 529] + expected: FAIL + + [XPath tests 530] + expected: FAIL + + [XPath tests 531] + expected: FAIL + + [XPath tests 532] + expected: FAIL + + [XPath tests 533] + expected: FAIL + + [XPath tests 534] + expected: FAIL + + [XPath tests 535] + expected: FAIL + + [XPath tests 536] + expected: FAIL + + [XPath tests 537] + expected: FAIL + + [XPath tests 538] + expected: FAIL + + [XPath tests 539] + expected: FAIL + + [XPath tests 540] + expected: FAIL + + [XPath tests 541] + expected: FAIL + + [XPath tests 542] + expected: FAIL + + [XPath tests 543] + expected: FAIL + + [XPath tests 544] + expected: FAIL + + [XPath tests 545] + expected: FAIL + + [XPath tests 546] + expected: FAIL + + [XPath tests 547] + expected: FAIL + + [XPath tests 548] + expected: FAIL + + [XPath tests 549] + expected: FAIL + + [XPath tests 550] + expected: FAIL + + [XPath tests 551] + expected: FAIL + + [XPath tests 552] + expected: FAIL + + [XPath tests 553] + expected: FAIL + + [XPath tests 554] + expected: FAIL + + [XPath tests 555] + expected: FAIL + + [XPath tests 556] + expected: FAIL + + [XPath tests 557] + expected: FAIL + + [XPath tests 558] + expected: FAIL + + [XPath tests 559] + expected: FAIL + + [XPath tests 560] + expected: FAIL + + [XPath tests 561] + expected: FAIL + + [XPath tests 562] + expected: FAIL + + [XPath tests 563] + expected: FAIL + + [XPath tests 564] + expected: FAIL + + [XPath tests 565] + expected: FAIL + + [XPath tests 566] + expected: FAIL + + [XPath tests 567] + expected: FAIL + + [XPath tests 568] + expected: FAIL + + [XPath tests 569] + expected: FAIL + + [XPath tests 570] + expected: FAIL + + [XPath tests 571] + expected: FAIL + + [XPath tests 572] + expected: FAIL + + [XPath tests 573] + expected: FAIL + + [XPath tests 574] + expected: FAIL + + [XPath tests 575] + expected: FAIL + + [XPath tests 576] + expected: FAIL + + [XPath tests 577] + expected: FAIL + + [XPath tests 578] + expected: FAIL + + [XPath tests 579] + expected: FAIL + + [XPath tests 580] + expected: FAIL + + [XPath tests 581] + expected: FAIL + + [XPath tests 582] + expected: FAIL + + [XPath tests 583] + expected: FAIL + + [XPath tests 584] + expected: FAIL + + [XPath tests 585] + expected: FAIL + + [XPath tests 586] + expected: FAIL + + [XPath tests 587] + expected: FAIL + + [XPath tests 588] + expected: FAIL + + [XPath tests 589] + expected: FAIL + + [XPath tests 590] + expected: FAIL + + [XPath tests 591] + expected: FAIL + + [XPath tests 592] + expected: FAIL + + [XPath tests 593] + expected: FAIL + + [XPath tests 594] + expected: FAIL + + [XPath tests 595] + expected: FAIL + + [XPath tests 596] + expected: FAIL + + [XPath tests 597] + expected: FAIL + + [XPath tests 598] + expected: FAIL + + [XPath tests 599] + expected: FAIL + + [XPath tests 600] + expected: FAIL + + [XPath tests 601] + expected: FAIL + + [XPath tests 602] + expected: FAIL + + [XPath tests 603] + expected: FAIL + + [XPath tests 604] + expected: FAIL + + [XPath tests 605] + expected: FAIL + + [XPath tests 606] + expected: FAIL + + [XPath tests 607] + expected: FAIL + + [XPath tests 608] + expected: FAIL + + [XPath tests 609] + expected: FAIL + + [XPath tests 610] + expected: FAIL + + [XPath tests 611] + expected: FAIL + + [XPath tests 612] + expected: FAIL + + [XPath tests 613] + expected: FAIL + + [XPath tests 614] + expected: FAIL + + [XPath tests 615] + expected: FAIL + + [XPath tests 616] + expected: FAIL + + [XPath tests 617] + expected: FAIL + + [XPath tests 618] + expected: FAIL + + [XPath tests 619] + expected: FAIL + + [XPath tests 620] + expected: FAIL + + [XPath tests 621] + expected: FAIL + + [XPath tests 622] + expected: FAIL + + [XPath tests 623] + expected: FAIL + + [XPath tests 624] + expected: FAIL + + [XPath tests 625] + expected: FAIL + + [XPath tests 626] + expected: FAIL + + [XPath tests 627] + expected: FAIL + + [XPath tests 628] + expected: FAIL + + [XPath tests 629] + expected: FAIL + + [XPath tests 630] + expected: FAIL + + [XPath tests 631] + expected: FAIL + + [XPath tests 632] + expected: FAIL + + [XPath tests 633] + expected: FAIL + + [XPath tests 634] + expected: FAIL + + [XPath tests 635] + expected: FAIL + + [XPath tests 636] + expected: FAIL + + [XPath tests 637] + expected: FAIL + + [XPath tests 638] + expected: FAIL + + [XPath tests 639] + expected: FAIL + + [XPath tests 640] + expected: FAIL + + [XPath tests 641] + expected: FAIL + + [XPath tests 642] + expected: FAIL + + [XPath tests 643] + expected: FAIL + + [XPath tests 644] + expected: FAIL + + [XPath tests 645] + expected: FAIL + + [XPath tests 646] + expected: FAIL + + [XPath tests 647] + expected: FAIL + + [XPath tests 648] + expected: FAIL + + [XPath tests 649] + expected: FAIL + + [XPath tests 650] + expected: FAIL + + [XPath tests 651] + expected: FAIL + + [XPath tests 652] + expected: FAIL + + [XPath tests 653] + expected: FAIL + + [XPath tests 654] + expected: FAIL + + [XPath tests 655] + expected: FAIL + + [XPath tests 656] + expected: FAIL + + [XPath tests 657] + expected: FAIL + + [XPath tests 658] + expected: FAIL + + [XPath tests 659] + expected: FAIL + + [XPath tests 660] + expected: FAIL + + [XPath tests 661] + expected: FAIL + + [XPath tests 662] + expected: FAIL + + [XPath tests 663] + expected: FAIL + + [XPath tests 664] + expected: FAIL + + [XPath tests 665] + expected: FAIL + + [XPath tests 666] + expected: FAIL + + [XPath tests 667] + expected: FAIL + + [XPath tests 668] + expected: FAIL + + [XPath tests 669] + expected: FAIL + + [XPath tests 670] + expected: FAIL + + [XPath tests 671] + expected: FAIL + + [XPath tests 672] + expected: FAIL + + [XPath tests 673] + expected: FAIL + + [XPath tests 674] + expected: FAIL + + [XPath tests 675] + expected: FAIL + + [XPath tests 676] + expected: FAIL + + [XPath tests 677] + expected: FAIL + + [XPath tests 678] + expected: FAIL + + [XPath tests 679] + expected: FAIL + + [XPath tests 680] + expected: FAIL + + [XPath tests 681] + expected: FAIL + + [XPath tests 682] + expected: FAIL + + [XPath tests 683] + expected: FAIL + + [XPath tests 684] + expected: FAIL + + [XPath tests 685] + expected: FAIL + + [XPath tests 686] + expected: FAIL + + [XPath tests 687] + expected: FAIL + + [XPath tests 688] + expected: FAIL + + [XPath tests 689] + expected: FAIL + + [XPath tests 690] + expected: FAIL + + [XPath tests 691] + expected: FAIL + + [XPath tests 692] + expected: FAIL + + [XPath tests 693] + expected: FAIL + + [XPath tests 694] + expected: FAIL + + [XPath tests 695] + expected: FAIL + + [XPath tests 696] + expected: FAIL + + [XPath tests 697] + expected: FAIL + + [XPath tests 698] + expected: FAIL + + [XPath tests 699] + expected: FAIL + + [XPath tests 700] + expected: FAIL + + [XPath tests 701] + expected: FAIL + + [XPath tests 702] + expected: FAIL + + [XPath tests 703] + expected: FAIL + + [XPath tests 704] + expected: FAIL + + [XPath tests 705] + expected: FAIL + + [XPath tests 706] + expected: FAIL + + [XPath tests 707] + expected: FAIL + + [XPath tests 708] + expected: FAIL + + [XPath tests 709] + expected: FAIL + + [XPath tests 710] + expected: FAIL + + [XPath tests 711] + expected: FAIL + + [XPath tests 712] + expected: FAIL + + [XPath tests 713] + expected: FAIL + + [XPath tests 714] + expected: FAIL + + [XPath tests 715] + expected: FAIL + + [XPath tests 716] + expected: FAIL + + [XPath tests 717] + expected: FAIL + + [XPath tests 718] + expected: FAIL + + [XPath tests 719] + expected: FAIL + + [XPath tests 720] + expected: FAIL + + [XPath tests 721] + expected: FAIL + + [XPath tests 722] + expected: FAIL + + [XPath tests 723] + expected: FAIL + + [XPath tests 724] + expected: FAIL + + [XPath tests 725] + expected: FAIL + + [XPath tests 726] + expected: FAIL + + [XPath tests 727] + expected: FAIL + + [XPath tests 728] + expected: FAIL + + [XPath tests 729] + expected: FAIL + + [XPath tests 730] + expected: FAIL + + [XPath tests 731] + expected: FAIL + + [XPath tests 732] + expected: FAIL + + [XPath tests 733] + expected: FAIL + + [XPath tests 734] + expected: FAIL + + [XPath tests 735] + expected: FAIL + + [XPath tests 736] + expected: FAIL + + [XPath tests 737] + expected: FAIL + + [XPath tests 738] + expected: FAIL + + [XPath tests 739] + expected: FAIL + + [XPath tests 740] + expected: FAIL + + [XPath tests 741] + expected: FAIL + + [XPath tests 742] + expected: FAIL + + [XPath tests 743] + expected: FAIL + + [XPath tests 744] + expected: FAIL + + [XPath tests 745] + expected: FAIL + + [XPath tests 746] + expected: FAIL + + [XPath tests 747] + expected: FAIL + + [XPath tests 748] + expected: FAIL + + [XPath tests 749] + expected: FAIL + + [XPath tests 750] + expected: FAIL + + [XPath tests 751] + expected: FAIL + + [XPath tests 752] + expected: FAIL + + [XPath tests 753] + expected: FAIL + + [XPath tests 754] + expected: FAIL + + [XPath tests 755] + expected: FAIL + + [XPath tests 756] + expected: FAIL + + [XPath tests 757] + expected: FAIL + + [XPath tests 758] + expected: FAIL + + [XPath tests 759] + expected: FAIL + + [XPath tests 760] + expected: FAIL + + [XPath tests 761] + expected: FAIL + + [XPath tests 762] + expected: FAIL + + [XPath tests 763] + expected: FAIL + + [XPath tests 764] + expected: FAIL + + [XPath tests 765] + expected: FAIL + + [XPath tests 766] + expected: FAIL + + [XPath tests 767] + expected: FAIL + + [XPath tests 768] + expected: FAIL + + [XPath tests 769] + expected: FAIL + + [XPath tests 770] + expected: FAIL + + [XPath tests 771] + expected: FAIL + + [XPath tests 772] + expected: FAIL + + [XPath tests 773] + expected: FAIL + + [XPath tests 774] + expected: FAIL + + [XPath tests 775] + expected: FAIL + + [XPath tests 776] + expected: FAIL + + [XPath tests 777] + expected: FAIL + + [XPath tests 778] + expected: FAIL + + [XPath tests 779] + expected: FAIL + + [XPath tests 780] + expected: FAIL + + [XPath tests 781] + expected: FAIL + + [XPath tests 782] + expected: FAIL + + [XPath tests 783] + expected: FAIL + + [XPath tests 784] + expected: FAIL + + [XPath tests 785] + expected: FAIL + + [XPath tests 786] + expected: FAIL + + [XPath tests 787] + expected: FAIL + + [XPath tests 788] + expected: FAIL + + [XPath tests 789] + expected: FAIL + + [XPath tests 790] + expected: FAIL + + [XPath tests 791] + expected: FAIL + + [XPath tests 792] + expected: FAIL + + [XPath tests 793] + expected: FAIL + + [XPath tests 794] + expected: FAIL + + [XPath tests 795] + expected: FAIL + + [XPath tests 796] + expected: FAIL + + [XPath tests 797] + expected: FAIL + + [XPath tests 798] + expected: FAIL + + [XPath tests 799] + expected: FAIL + + [XPath tests 800] + expected: FAIL + + [XPath tests 801] + expected: FAIL + + [XPath tests 802] + expected: FAIL + + [XPath tests 803] + expected: FAIL + + [XPath tests 804] + expected: FAIL + + [XPath tests 805] + expected: FAIL + + [XPath tests 806] + expected: FAIL + + [XPath tests 807] + expected: FAIL + + [XPath tests 808] + expected: FAIL + + [XPath tests 809] + expected: FAIL + + [XPath tests 810] + expected: FAIL + + [XPath tests 811] + expected: FAIL + + [XPath tests 812] + expected: FAIL + + [XPath tests 813] + expected: FAIL + + [XPath tests 814] + expected: FAIL + + [XPath tests 815] + expected: FAIL + + [XPath tests 816] + expected: FAIL + + [XPath tests 817] + expected: FAIL + + [XPath tests 818] + expected: FAIL + + [XPath tests 819] + expected: FAIL + + [XPath tests 820] + expected: FAIL + + [XPath tests 821] + expected: FAIL + + [XPath tests 822] + expected: FAIL + + [XPath tests 823] + expected: FAIL + + [XPath tests 824] + expected: FAIL + + [XPath tests 825] + expected: FAIL + + [XPath tests 826] + expected: FAIL + + [XPath tests 827] + expected: FAIL + + [XPath tests 828] + expected: FAIL + + [XPath tests 829] + expected: FAIL + + [XPath tests 830] + expected: FAIL + + [XPath tests 831] + expected: FAIL + + [XPath tests 832] + expected: FAIL + + [XPath tests 833] + expected: FAIL + + [XPath tests 834] + expected: FAIL + + [XPath tests 835] + expected: FAIL + + [XPath tests 836] + expected: FAIL + + [XPath tests 837] + expected: FAIL + + [XPath tests 838] + expected: FAIL + + [XPath tests 839] + expected: FAIL + + [XPath tests 840] + expected: FAIL + + [XPath tests 841] + expected: FAIL + + [XPath tests 842] + expected: FAIL + + [XPath tests 843] + expected: FAIL + + [XPath tests 844] + expected: FAIL + + [XPath tests 845] + expected: FAIL + + [XPath tests 846] + expected: FAIL + + [XPath tests 847] + expected: FAIL + + [XPath tests 848] + expected: FAIL + + [XPath tests 849] + expected: FAIL + + [XPath tests 850] + expected: FAIL + + [XPath tests 851] + expected: FAIL + + [XPath tests 852] + expected: FAIL + + [XPath tests 853] + expected: FAIL + + [XPath tests 854] + expected: FAIL + + [XPath tests 855] + expected: FAIL + + [XPath tests 856] + expected: FAIL + + [XPath tests 857] + expected: FAIL + + [XPath tests 858] + expected: FAIL + + [XPath tests 859] + expected: FAIL + + [XPath tests 860] + expected: FAIL + + [XPath tests 861] + expected: FAIL + + [XPath tests 862] + expected: FAIL + + [XPath tests 863] + expected: FAIL + + [XPath tests 864] + expected: FAIL + + [XPath tests 865] + expected: FAIL + + [XPath tests 866] + expected: FAIL + + [XPath tests 867] + expected: FAIL + + [XPath tests 868] + expected: FAIL + + [XPath tests 869] + expected: FAIL + + [XPath tests 870] + expected: FAIL + + [XPath tests 871] + expected: FAIL + + [XPath tests 872] + expected: FAIL + + [XPath tests 873] + expected: FAIL + + [XPath tests 874] + expected: FAIL + + [XPath tests 875] + expected: FAIL + + [XPath tests 876] + expected: FAIL + + [XPath tests 877] + expected: FAIL + + [XPath tests 878] + expected: FAIL + + [XPath tests 879] + expected: FAIL + + [XPath tests 880] + expected: FAIL + + [XPath tests 881] + expected: FAIL + + [XPath tests 882] + expected: FAIL + + [XPath tests 883] + expected: FAIL + + [XPath tests 884] + expected: FAIL + + [XPath tests 885] + expected: FAIL + + [XPath tests 886] + expected: FAIL + + [XPath tests 887] + expected: FAIL + + [XPath tests 888] + expected: FAIL + + [XPath tests 889] + expected: FAIL + + [XPath tests 890] + expected: FAIL + + [XPath tests 891] + expected: FAIL + + [XPath tests 892] + expected: FAIL + + [XPath tests 893] + expected: FAIL + + [XPath tests 894] + expected: FAIL + + [XPath tests 895] + expected: FAIL + + [XPath tests 896] + expected: FAIL + + [XPath tests 897] + expected: FAIL + + [XPath tests 898] + expected: FAIL + + [XPath tests 899] + expected: FAIL + + [XPath tests 900] + expected: FAIL + + [XPath tests 901] + expected: FAIL + + [XPath tests 902] + expected: FAIL + + [XPath tests 903] + expected: FAIL + + [XPath tests 904] + expected: FAIL + + [XPath tests 905] + expected: FAIL + + [XPath tests 906] + expected: FAIL + + [XPath tests 907] + expected: FAIL + + [XPath tests 908] + expected: FAIL + + [XPath tests 909] + expected: FAIL + + [XPath tests 910] + expected: FAIL + + [XPath tests 911] + expected: FAIL + + [XPath tests 912] + expected: FAIL + + [XPath tests 913] + expected: FAIL + + [XPath tests 914] + expected: FAIL + + [XPath tests 915] + expected: FAIL + + [XPath tests 916] + expected: FAIL + + [XPath tests 917] + expected: FAIL + + [XPath tests 918] + expected: FAIL + + [XPath tests 919] + expected: FAIL + + [XPath tests 920] + expected: FAIL + + [XPath tests 921] + expected: FAIL + + [XPath tests 922] + expected: FAIL + + [XPath tests 923] + expected: FAIL + + [XPath tests 924] + expected: FAIL + + [XPath tests 925] + expected: FAIL + + [XPath tests 926] + expected: FAIL + + [XPath tests 927] + expected: FAIL + + [XPath tests 928] + expected: FAIL + + [XPath tests 929] + expected: FAIL + + [XPath tests 930] + expected: FAIL + + [XPath tests 931] + expected: FAIL + + [XPath tests 932] + expected: FAIL + + [XPath tests 933] + expected: FAIL + + [XPath tests 934] + expected: FAIL + + [XPath tests 935] + expected: FAIL + + [XPath tests 936] + expected: FAIL + + [XPath tests 937] + expected: FAIL + + [XPath tests 938] + expected: FAIL + + [XPath tests 939] + expected: FAIL + + [XPath tests 940] + expected: FAIL + + [XPath tests 941] + expected: FAIL + + [XPath tests 942] + expected: FAIL + + [XPath tests 943] + expected: FAIL + + [XPath tests 944] + expected: FAIL + + [XPath tests 945] + expected: FAIL + + [XPath tests 946] + expected: FAIL + + [XPath tests 947] + expected: FAIL + + [XPath tests 948] + expected: FAIL + + [XPath tests 949] + expected: FAIL + + [XPath tests 950] + expected: FAIL + + [XPath tests 951] + expected: FAIL + + [XPath tests 952] + expected: FAIL + + [XPath tests 953] + expected: FAIL + + [XPath tests 954] + expected: FAIL + + [XPath tests 955] + expected: FAIL + + [XPath tests 956] + expected: FAIL + + [XPath tests 957] + expected: FAIL + + [XPath tests 958] + expected: FAIL + + [XPath tests 959] + expected: FAIL + + [XPath tests 960] + expected: FAIL + + [XPath tests 961] + expected: FAIL + + [XPath tests 962] + expected: FAIL + + [XPath tests 963] + expected: FAIL + + [XPath tests 964] + expected: FAIL + + [XPath tests 965] + expected: FAIL + + [XPath tests 966] + expected: FAIL + + [XPath tests 967] + expected: FAIL + + [XPath tests 968] + expected: FAIL + + [XPath tests 969] + expected: FAIL + + [XPath tests 970] + expected: FAIL + + [XPath tests 971] + expected: FAIL + + [XPath tests 972] + expected: FAIL + + [XPath tests 973] + expected: FAIL + + [XPath tests 974] + expected: FAIL + + [XPath tests 975] + expected: FAIL + + [XPath tests 976] + expected: FAIL + + [XPath tests 977] + expected: FAIL + + [XPath tests 978] + expected: FAIL + + [XPath tests 979] + expected: FAIL + + [XPath tests 980] + expected: FAIL + + [XPath tests 981] + expected: FAIL + + [XPath tests 982] + expected: FAIL + + [XPath tests 983] + expected: FAIL + + [XPath tests 984] + expected: FAIL + + [XPath tests 985] + expected: FAIL + + [XPath tests 986] + expected: FAIL + + [XPath tests 987] + expected: FAIL + + [XPath tests 988] + expected: FAIL + + [XPath tests 989] + expected: FAIL + + [XPath tests 990] + expected: FAIL + + [XPath tests 991] + expected: FAIL + + [XPath tests 992] + expected: FAIL + + [XPath tests 993] + expected: FAIL + + [XPath tests 994] + expected: FAIL + + [XPath tests 995] + expected: FAIL + + [XPath tests 996] + expected: FAIL + + [XPath tests 997] + expected: FAIL + + [XPath tests 998] + expected: FAIL + + [XPath tests 999] + expected: FAIL + + [XPath tests 1000] + expected: FAIL + + [XPath tests 1001] + expected: FAIL + + [XPath tests 1002] + expected: FAIL + + [XPath tests 1003] + expected: FAIL + + [XPath tests 1004] + expected: FAIL + + [XPath tests 1005] + expected: FAIL + + [XPath tests 1006] + expected: FAIL + + [XPath tests 1007] + expected: FAIL + + [XPath tests 1008] + expected: FAIL + + [XPath tests 1009] + expected: FAIL + + [XPath tests 1010] + expected: FAIL + + [XPath tests 1011] + expected: FAIL + + [XPath tests 1012] + expected: FAIL + + [XPath tests 1013] + expected: FAIL + + [XPath tests 1014] + expected: FAIL + + [XPath tests 1015] + expected: FAIL + + [XPath tests 1016] + expected: FAIL + + [XPath tests 1017] + expected: FAIL + + [XPath tests 1018] + expected: FAIL + + [XPath tests 1019] + expected: FAIL + + [XPath tests 1020] + expected: FAIL + + [XPath tests 1021] + expected: FAIL + + [XPath tests 1022] + expected: FAIL + + [XPath tests 1023] + expected: FAIL |