diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index e2e927318c4..6fc69f13400 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -1812,16 +1812,16 @@ impl Element { Ref::map(self.attrs.borrow(), |attrs| &**attrs) } - // Element branch of https://dom.spec.whatwg.org/#locate-a-namespace + /// Element branch of pub(crate) fn locate_namespace(&self, prefix: Option) -> Namespace { let namespace_prefix = prefix.clone().map(|s| Prefix::from(&*s)); - // "1. If prefix is "xml", then return the XML namespace." + // Step 1. If prefix is "xml", then return the XML namespace. if namespace_prefix == Some(namespace_prefix!("xml")) { return ns!(xml); } - // "2. If prefix is "xmlns", then return the XMLNS namespace." + // Step 2. If prefix is "xmlns", then return the XMLNS namespace. if namespace_prefix == Some(namespace_prefix!("xmlns")) { return ns!(xmlns); } @@ -1833,21 +1833,20 @@ impl Element { .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::); - // "5. If its parent element is null, then return null." - // "6. Return the result of running locate a namespace on its parent element using prefix." + // Step 5. If its parent element is null, then return null. + // Step 6. Return the result of running locate a namespace on its parent element using prefix. for element in inclusive_ancestor_elements { - // "3. If its namespace is non-null and its namespace prefix is prefix, then return - // namespace." + // Step 3. If its namespace is non-null and its namespace prefix is prefix, then return namespace. if element.namespace() != &ns!() && element.prefix().as_ref().map(|p| &**p) == prefix.as_deref() { return element.namespace().clone(); } - // "4. If it has an attribute whose namespace is the XMLNS namespace, namespace prefix + // Step 4. If it has an attribute whose namespace is the XMLNS namespace, namespace prefix // is "xmlns", and local name is prefix, or if prefix is null and it has an attribute // whose namespace is the XMLNS namespace, namespace prefix is null, and local name is - // "xmlns", then return its value if it is not the empty string, and null otherwise." + // "xmlns", then return its value if it is not the empty string, and null otherwise. let attr = Ref::filter_map(self.attrs(), |attrs| { attrs.iter().find(|attr| { if attr.namespace() != &ns!(xmlns) { @@ -3090,18 +3089,18 @@ impl Element { #[allow(non_snake_case)] impl ElementMethods for Element { - // https://dom.spec.whatwg.org/#dom-element-namespaceuri + /// fn GetNamespaceURI(&self) -> Option { Node::namespace_to_string(self.namespace.clone()) } - // https://dom.spec.whatwg.org/#dom-element-localname + /// fn LocalName(&self) -> DOMString { // FIXME(ajeffrey): Convert directly from LocalName to DOMString DOMString::from(&*self.local_name) } - // https://dom.spec.whatwg.org/#dom-element-prefix + /// fn GetPrefix(&self) -> Option { self.prefix.borrow().as_ref().map(|p| DOMString::from(&**p)) } diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 8a168217169..85fc7a8ec19 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -3988,13 +3988,10 @@ impl NodeMethods for Node { /// fn LookupNamespaceURI(&self, prefix: Option) -> Option { - // Step 1. - let prefix = match prefix { - Some(ref p) if p.is_empty() => None, - pre => pre, - }; + // Step 1. If prefix is the empty string, then set it to null. + let prefix = prefix.filter(|prefix| !prefix.is_empty()); - // Step 2. + // Step 2. Return the result of running locate a namespace for this using prefix. Node::namespace_to_string(Node::locate_namespace(self, prefix)) } diff --git a/components/script/dom/xpathevaluator.rs b/components/script/dom/xpathevaluator.rs index 53f02ad5b6e..259c447d259 100644 --- a/components/script/dom/xpathevaluator.rs +++ b/components/script/dom/xpathevaluator.rs @@ -9,7 +9,6 @@ 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::{DomGlobal, Reflector, reflect_dom_object_with_proto}; @@ -91,7 +90,7 @@ impl XPathEvaluatorMethods for XPathEvaluator { &self, expression_str: DOMString, context_node: &Node, - _resolver: Option>, + resolver: Option>, result_type: u16, result: Option<&XPathResult>, can_gc: CanGc, @@ -101,12 +100,6 @@ impl XPathEvaluatorMethods for XPathEvaluator { let parsed_expression = crate::xpath::parse(&expression_str).map_err(|_| Error::Syntax(None))?; let expression = XPathExpression::new(window, None, can_gc, parsed_expression); - XPathExpressionMethods::::Evaluate( - &*expression, - context_node, - result_type, - result, - can_gc, - ) + expression.evaluate_internal(context_node, result_type, result, resolver, can_gc) } } diff --git a/components/script/dom/xpathexpression.rs b/components/script/dom/xpathexpression.rs index 1cacb179d4f..f54dd0eafdb 100644 --- a/components/script/dom/xpathexpression.rs +++ b/components/script/dom/xpathexpression.rs @@ -2,10 +2,13 @@ * 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 crate::dom::bindings::codegen::Bindings::XPathExpressionBinding::XPathExpressionMethods; +use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot}; @@ -45,15 +48,13 @@ impl XPathExpression { can_gc, ) } -} -impl XPathExpressionMethods for XPathExpression { - /// - fn Evaluate( + pub(crate) fn evaluate_internal( &self, context_node: &Node, result_type_num: u16, _result: Option<&XPathResult>, + resolver: Option>, can_gc: CanGc, ) -> Fallible> { let result_type = XPathResultType::try_from(result_type_num) @@ -62,8 +63,7 @@ impl XPathExpressionMethods for XPathExpression { 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)?; + let result_value = evaluate_parsed_xpath(&self.parsed_expression, context_node, resolver)?; // TODO(vlindhol): support putting results into mutable `_result` as per the spec Ok(XPathResult::new( @@ -75,3 +75,16 @@ impl XPathExpressionMethods for XPathExpression { )) } } + +impl XPathExpressionMethods for XPathExpression { + /// + fn Evaluate( + &self, + context_node: &Node, + result_type_num: u16, + result: Option<&XPathResult>, + can_gc: CanGc, + ) -> Fallible> { + self.evaluate_internal(context_node, result_type_num, result, None, can_gc) + } +} diff --git a/components/script/xpath/context.rs b/components/script/xpath/context.rs index 8b73fb8058e..05401db4a66 100644 --- a/components/script/xpath/context.rs +++ b/components/script/xpath/context.rs @@ -2,26 +2,33 @@ * 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 std::iter::Enumerate; +use std::rc::Rc; use std::vec::IntoIter; +use script_bindings::error::Fallible; +use script_bindings::script_runtime::CanGc; use script_bindings::str::DOMString; use super::Node; +use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; +use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::root::DomRoot; /// The context during evaluation of an XPath expression. -#[derive(Debug)] pub(crate) struct EvaluationCtx { - /// Where we started at + /// Where we started at. pub(crate) starting_node: DomRoot, - /// The "current" node in the evaluation + /// The "current" node in the evaluation. pub(crate) context_node: DomRoot, - /// Details needed for evaluating a predicate list + /// Details needed for evaluating a predicate list. pub(crate) predicate_ctx: Option, - /// The nodes we're currently matching against + /// The nodes we're currently matching against. pub(crate) predicate_nodes: Option>>, + /// A list of known namespace prefixes. + pub(crate) resolver: Option>, } #[derive(Clone, Copy, Debug)] @@ -32,12 +39,13 @@ pub(crate) struct PredicateCtx { impl EvaluationCtx { /// Prepares the context used while evaluating the XPath expression - pub(crate) fn new(context_node: &Node) -> EvaluationCtx { + pub(crate) fn new(context_node: &Node, resolver: Option>) -> EvaluationCtx { EvaluationCtx { starting_node: DomRoot::from_ref(context_node), context_node: DomRoot::from_ref(context_node), predicate_ctx: None, predicate_nodes: None, + resolver, } } @@ -48,6 +56,7 @@ impl EvaluationCtx { context_node: DomRoot::from_ref(node), predicate_ctx: self.predicate_ctx, predicate_nodes: self.predicate_nodes.clone(), + resolver: self.resolver.clone(), } } @@ -57,6 +66,7 @@ impl EvaluationCtx { context_node: self.context_node.clone(), predicate_ctx: None, predicate_nodes: Some(nodes.into_iter().map(DomRoot::from_ref).collect()), + resolver: self.resolver.clone(), } } @@ -74,9 +84,26 @@ impl EvaluationCtx { } /// Resolve a namespace prefix using the context node's document - pub(crate) fn resolve_namespace(&self, prefix: Option<&str>) -> Option { - self.context_node - .LookupNamespaceURI(prefix.map(DOMString::from)) + pub(crate) fn resolve_namespace( + &self, + prefix: Option<&str>, + can_gc: CanGc, + ) -> Fallible> { + // First check if the prefix is known by our resolver function + if let Some(resolver) = self.resolver.as_ref() { + if let Some(namespace_uri) = resolver.LookupNamespaceURI__( + prefix.map(DOMString::from), + ExceptionHandling::Rethrow, + can_gc, + )? { + return Ok(Some(namespace_uri)); + } + } + + // Then, see if it's defined on the context node + Ok(self + .context_node + .LookupNamespaceURI(prefix.map(DOMString::from))) } } @@ -100,6 +127,19 @@ impl Iterator for EvalNodesetIter<'_> { index: idx + 1, size: self.size, }), + resolver: self.ctx.resolver.clone(), }) } } + +impl fmt::Debug for EvaluationCtx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EvaluationCtx") + .field("starting_node", &self.starting_node) + .field("context_node", &self.context_node) + .field("predicate_ctx", &self.predicate_ctx) + .field("predicate_nodes", &self.predicate_nodes) + .field("resolver", &"") + .finish() + } +} diff --git a/components/script/xpath/eval.rs b/components/script/xpath/eval.rs index 5d4407ab019..9291602b70d 100644 --- a/components/script/xpath/eval.rs +++ b/components/script/xpath/eval.rs @@ -5,6 +5,7 @@ use std::fmt; use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_prefix, ns}; +use script_bindings::script_runtime::CanGc; use super::parser::{ AdditiveOp, Axis, EqualityOp, Expr, FilterExpr, KindTest, Literal, MultiplicativeOp, NodeTest, @@ -15,6 +16,7 @@ use super::{EvaluationCtx, Value}; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::domname::namespace_from_domstring; +use crate::dom::bindings::error::Error as JsError; use crate::dom::bindings::inheritance::{Castable, CharacterDataTypeId, NodeTypeId}; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::str::DOMString; @@ -24,16 +26,30 @@ use crate::dom::node::{Node, ShadowIncluding}; use crate::dom::processinginstruction::ProcessingInstruction; use crate::xpath::context::PredicateCtx; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum Error { NotANodeset, InvalidPath, - UnknownFunction { name: QualName }, - UnknownVariable { name: QualName }, - UnknownNamespace { prefix: String }, - InvalidQName { qname: ParserQualName }, - FunctionEvaluation { fname: String }, - Internal { msg: String }, + UnknownFunction { + name: QualName, + }, + UnknownVariable { + name: QualName, + }, + UnknownNamespace { + prefix: String, + }, + InvalidQName { + qname: ParserQualName, + }, + FunctionEvaluation { + fname: String, + }, + Internal { + msg: String, + }, + /// A JS exception that needs to be propagated to the caller. + JsException(JsError), } impl std::fmt::Display for Error { @@ -55,6 +71,9 @@ impl std::fmt::Display for Error { Error::Internal { msg } => { write!(f, "internal error: {}", msg) }, + Error::JsException(exception) => { + write!(f, "JS exception: {:?}", exception) + }, } } } @@ -360,27 +379,22 @@ fn validate_and_extract( } } -pub(crate) struct QualNameConverter<'a> { - qname: &'a ParserQualName, - context: &'a EvaluationCtx, -} +pub(crate) fn convert_parsed_qname_to_qualified_name( + qname: &ParserQualName, + context: &EvaluationCtx, + can_gc: CanGc, +) -> Result { + let qname_as_str = qname.to_string(); + let namespace = context + .resolve_namespace(qname.prefix.as_deref(), can_gc) + .map_err(Error::JsException)?; -impl<'a> TryFrom> for QualName { - type Error = Error; - - fn try_from(converter: QualNameConverter<'a>) -> Result { - let qname_as_str = converter.qname.to_string(); - let namespace = converter - .context - .resolve_namespace(converter.qname.prefix.as_deref()); - - if let Ok((ns, prefix, local)) = validate_and_extract(namespace, &qname_as_str) { - Ok(QualName { prefix, ns, local }) - } else { - Err(Error::InvalidQName { - qname: converter.qname.clone(), - }) - } + if let Ok((ns, prefix, local)) = validate_and_extract(namespace, &qname_as_str) { + Ok(QualName { prefix, ns, local }) + } else { + Err(Error::InvalidQName { + qname: qname.clone(), + }) } } @@ -434,11 +448,16 @@ pub(crate) fn element_name_test( } } -fn apply_node_test(context: &EvaluationCtx, test: &NodeTest, node: &Node) -> Result { +fn apply_node_test( + context: &EvaluationCtx, + test: &NodeTest, + node: &Node, + can_gc: CanGc, +) -> Result { let result = match test { NodeTest::Name(qname) => { // Convert the unvalidated "parser QualName" into the proper QualName structure - let wanted_name: QualName = QualNameConverter { qname, context }.try_into()?; + let wanted_name = convert_parsed_qname_to_qualified_name(qname, context, can_gc)?; match node.type_id() { NodeTypeId::Element(_) => { let element = node.downcast::().unwrap(); @@ -563,7 +582,9 @@ impl Evaluatable for StepExpr { let filtered_nodes: Vec> = nodes .into_iter() .map(|node| { - apply_node_test(context, &axis_step.node_test, &node) + // FIXME: propagate this can_gc up further. This likely requires removing the "Evaluate" + // trait or changing the signature of "evaluate". The trait is not really necessary anyways. + apply_node_test(context, &axis_step.node_test, &node, CanGc::note()) .map(|matches| matches.then_some(node)) }) .collect::, _>>()? @@ -613,6 +634,7 @@ impl Evaluatable for PredicateListExpr { context_node: node.clone(), predicate_nodes: context.predicate_nodes.clone(), predicate_ctx: Some(PredicateCtx { index: i + 1, size }), + resolver: context.resolver.clone(), }; let eval_result = predicate_expr.expr.evaluate(&predicate_ctx); diff --git a/components/script/xpath/mod.rs b/components/script/xpath/mod.rs index a48cb3fd27b..15424019253 100644 --- a/components/script/xpath/mod.rs +++ b/components/script/xpath/mod.rs @@ -2,13 +2,16 @@ * 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 context::EvaluationCtx; -use eval::Evaluatable; +use eval::{Error as EvaluationError, Evaluatable}; pub(crate) use eval_value::{NodesetHelpers, Value}; -use parser::OwnedParserError; pub(crate) use parser::{Expr, parse as parse_impl}; use super::dom::node::Node; +use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; +use crate::dom::bindings::error::{Error as JsError, Error, Fallible}; mod context; #[allow(dead_code)] @@ -19,58 +22,41 @@ mod eval_value; #[allow(dead_code)] mod parser; -/// The failure modes of executing an XPath. -#[derive(Debug, PartialEq)] -pub(crate) 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(crate) fn parse(xpath: &str) -> Result { +pub(crate) fn parse(xpath: &str) -> Fallible { match parse_impl(xpath) { Ok(expr) => { - debug!("Parsed XPath: {:?}", expr); + debug!("Parsed XPath: {expr:?}"); Ok(expr) }, - Err(e) => { - debug!("Unable to parse XPath: {}", e); - Err(Error::Parsing { source: e }) + Err(error) => { + debug!("Unable to parse XPath: {error}"); + Err(Error::Operation) }, } } /// Evaluate an already-parsed XPath expression -pub(crate) fn evaluate_parsed_xpath(expr: &Expr, context_node: &Node) -> Result { - let context = EvaluationCtx::new(context_node); +pub(crate) fn evaluate_parsed_xpath( + expr: &Expr, + context_node: &Node, + resolver: Option>, +) -> Fallible { + let context = EvaluationCtx::new(context_node, resolver); match expr.evaluate(&context) { - Ok(v) => { - debug!("Evaluated XPath: {:?}", v); - Ok(v) + Ok(value) => { + debug!("Evaluated XPath: {value:?}"); + Ok(value) }, - Err(e) => { - debug!("Unable to evaluate XPath: {}", e); - Err(Error::Evaluating { source: e }) + Err(error) => { + debug!("Unable to evaluate XPath: {error}"); + + let error = match error { + EvaluationError::JsException(exception) => exception, + _ => JsError::Operation, + }; + + Err(error) }, } } diff --git a/tests/wpt/meta/domxpath/text-html-attributes.html.ini b/tests/wpt/meta/domxpath/text-html-attributes.html.ini index 3f9df75b097..22dc2e57ac1 100644 --- a/tests/wpt/meta/domxpath/text-html-attributes.html.ini +++ b/tests/wpt/meta/domxpath/text-html-attributes.html.ini @@ -11,9 +11,6 @@ [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 @@ -22,6 +19,3 @@ [Select SVG element with non-ascii attribute 2] 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 index 2da633f3c2f..5fdd314b4c7 100644 --- a/tests/wpt/meta/domxpath/text-html-elements.html.ini +++ b/tests/wpt/meta/domxpath/text-html-elements.html.ini @@ -1,19 +1,7 @@ [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