diff options
Diffstat (limited to 'components/script/xpath')
-rw-r--r-- | components/script/xpath/context.rs | 12 | ||||
-rw-r--r-- | components/script/xpath/eval.rs | 88 | ||||
-rw-r--r-- | components/script/xpath/eval_function.rs | 65 | ||||
-rw-r--r-- | components/script/xpath/eval_value.rs | 14 | ||||
-rw-r--r-- | components/script/xpath/parser.rs | 199 |
5 files changed, 304 insertions, 74 deletions
diff --git a/components/script/xpath/context.rs b/components/script/xpath/context.rs index df78a9a7bec..ff51c92521d 100644 --- a/components/script/xpath/context.rs +++ b/components/script/xpath/context.rs @@ -5,10 +5,14 @@ use std::iter::Enumerate; use std::vec::IntoIter; +use script_bindings::str::DOMString; + use super::Node; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::root::DomRoot; /// The context during evaluation of an XPath expression. +#[derive(Debug)] pub(crate) struct EvaluationCtx { /// Where we started at pub(crate) starting_node: DomRoot<Node>, @@ -20,7 +24,7 @@ pub(crate) struct EvaluationCtx { pub(crate) predicate_nodes: Option<Vec<DomRoot<Node>>>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub(crate) struct PredicateCtx { pub(crate) index: usize, pub(crate) size: usize, @@ -68,6 +72,12 @@ impl EvaluationCtx { size, } } + + /// Resolve a namespace prefix using the context node's document + pub(crate) fn resolve_namespace(&self, prefix: Option<&str>) -> Option<DOMString> { + self.context_node + .LookupNamespaceURI(prefix.map(DOMString::from)) + } } /// When evaluating predicates, we need to keep track of the current node being evaluated and diff --git a/components/script/xpath/eval.rs b/components/script/xpath/eval.rs index 75d7ac5849c..38d8c2fee90 100644 --- a/components/script/xpath/eval.rs +++ b/components/script/xpath/eval.rs @@ -12,6 +12,7 @@ use super::parser::{ QName as ParserQualName, RelationalOp, StepExpr, UnaryOp, }; use super::{EvaluationCtx, Value}; +use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::inheritance::{Castable, CharacterDataTypeId, NodeTypeId}; use crate::dom::bindings::root::DomRoot; @@ -83,6 +84,22 @@ where } } +impl<T> Evaluatable for Option<T> +where + T: Evaluatable, +{ + fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { + match self { + Some(expr) => expr.evaluate(context), + None => Ok(Value::Nodeset(vec![])), + } + } + + fn is_primitive(&self) -> bool { + self.as_ref().is_some_and(|t| T::is_primitive(t)) + } +} + impl Evaluatable for Expr { fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { match self { @@ -230,21 +247,31 @@ impl Evaluatable for PathExpr { } } -impl TryFrom<&ParserQualName> for QualName { +pub(crate) struct QualNameConverter<'a> { + qname: &'a ParserQualName, + context: &'a EvaluationCtx, +} + +impl<'a> TryFrom<QualNameConverter<'a>> 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) { + fn try_from(converter: QualNameConverter<'a>) -> Result<Self, Self::Error> { + 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: qname.clone(), + qname: converter.qname.clone(), }) } } } +#[derive(Debug)] pub(crate) enum NameTestComparisonMode { /// Namespaces must match exactly XHtml, @@ -294,29 +321,41 @@ pub(crate) fn element_name_test( } } -fn apply_node_test(test: &NodeTest, node: &Node) -> Result<bool, Error> { +fn apply_node_test(context: &EvaluationCtx, 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 + let wanted_name: QualName = QualNameConverter { qname, context }.try_into()?; + match node.type_id() { + NodeTypeId::Element(_) => { + let element = node.downcast::<Element>().unwrap(); + let comparison_mode = if node.owner_doc().is_html_document() { + NameTestComparisonMode::Html + } else { + NameTestComparisonMode::XHtml + }; + 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) + }, + NodeTypeId::Attr => { + let attr = node.downcast::<Attr>().unwrap(); + let attr_qualname = QualName::new( + attr.prefix().cloned(), + attr.namespace().clone(), + attr.local_name().clone(), + ); + // attributes are always compared with strict namespace matching + let comparison_mode = NameTestComparisonMode::XHtml; + element_name_test(wanted_name, attr_qualname, comparison_mode) + }, + _ => false, } }, - NodeTest::Wildcard => true, + NodeTest::Wildcard => matches!(node.type_id(), NodeTypeId::Element(_)), NodeTest::Kind(kind) => match kind { KindTest::PI(target) => { if NodeTypeId::CharacterData(CharacterDataTypeId::ProcessingInstruction) == @@ -411,7 +450,7 @@ impl Evaluatable for StepExpr { let filtered_nodes: Vec<DomRoot<Node>> = nodes .into_iter() .map(|node| { - apply_node_test(&axis_step.node_test, &node) + apply_node_test(context, &axis_step.node_test, &node) .map(|matches| matches.then_some(node)) }) .collect::<Result<Vec<_>, _>>()? @@ -489,6 +528,7 @@ impl Evaluatable for PredicateExpr { let v = match eval_result { Ok(Value::Number(v)) => Ok(predicate_ctx.index == v as usize), + Ok(Value::Boolean(v)) => Ok(v), Ok(v) => Ok(v.boolean()), Err(e) => Err(e), }; diff --git a/components/script/xpath/eval_function.rs b/components/script/xpath/eval_function.rs index caf0782c07b..53c14944474 100644 --- a/components/script/xpath/eval_function.rs +++ b/components/script/xpath/eval_function.rs @@ -2,12 +2,15 @@ * 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 style::Atom; + use super::Value; use super::context::EvaluationCtx; use super::eval::{Error, Evaluatable, try_extract_nodeset}; use super::parser::CoreFunction; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::inheritance::{Castable, NodeTypeId}; +use crate::dom::bindings::root::DomRoot; use crate::dom::element::Element; use crate::dom::node::Node; @@ -101,6 +104,31 @@ pub(crate) fn normalize_space(s: &str) -> String { result } +/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-lang> +fn lang_matches(context_lang: Option<&str>, target_lang: &str) -> bool { + let Some(context_lang) = context_lang else { + return false; + }; + + let context_lower = context_lang.to_ascii_lowercase(); + let target_lower = target_lang.to_ascii_lowercase(); + + if context_lower == target_lower { + return true; + } + + // Check if context is target with additional suffix + if context_lower.starts_with(&target_lower) { + // Make sure the next character is a hyphen to avoid matching + // e.g. "england" when target is "en" + if let Some(next_char) = context_lower.chars().nth(target_lower.len()) { + return next_char == '-'; + } + } + + false +} + impl Evaluatable for CoreFunction { fn evaluate(&self, context: &EvaluationCtx) -> Result<Value, Error> { match self { @@ -131,7 +159,20 @@ impl Evaluatable for CoreFunction { .collect(); Ok(Value::String(strings?.join(""))) }, - CoreFunction::Id(_expr) => todo!(), + CoreFunction::Id(expr) => { + let args_str = expr.evaluate(context)?.string(); + let args_normalized = normalize_space(&args_str); + let args = args_normalized.split(' '); + + let document = context.context_node.owner_doc(); + let mut result = Vec::new(); + for arg in args { + for element in document.get_elements_with_id(&Atom::from(arg)).iter() { + result.push(DomRoot::from_ref(element.upcast::<Node>())); + } + } + Ok(Value::Nodeset(result)) + }, CoreFunction::LocalName(expr_opt) => { let node = match expr_opt { Some(expr) => expr @@ -256,7 +297,11 @@ impl Evaluatable for CoreFunction { 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 + CoreFunction::Lang(expr) => { + let context_lang = context.context_node.get_lang(); + let lang = expr.evaluate(context)?.string(); + Ok(Value::Boolean(lang_matches(context_lang.as_deref(), &lang))) + }, } } @@ -319,7 +364,7 @@ impl Evaluatable for CoreFunction { } #[cfg(test)] mod tests { - use super::{substring, substring_after, substring_before}; + use super::{lang_matches, substring, substring_after, substring_before}; #[test] fn test_substring_before() { @@ -354,4 +399,18 @@ mod tests { assert_eq!(substring("hello", 0, Some(0)), ""); assert_eq!(substring("hello", 0, Some(-5)), ""); } + + #[test] + fn test_lang_matches() { + assert!(lang_matches(Some("en"), "en")); + assert!(lang_matches(Some("EN"), "en")); + assert!(lang_matches(Some("en"), "EN")); + assert!(lang_matches(Some("en-US"), "en")); + assert!(lang_matches(Some("en-GB"), "en")); + + assert!(!lang_matches(Some("eng"), "en")); + assert!(!lang_matches(Some("fr"), "en")); + assert!(!lang_matches(Some("fr-en"), "en")); + assert!(!lang_matches(None, "en")); + } } diff --git a/components/script/xpath/eval_value.rs b/components/script/xpath/eval_value.rs index de6c13e3454..66f1b92c6d4 100644 --- a/components/script/xpath/eval_value.rs +++ b/components/script/xpath/eval_value.rs @@ -8,7 +8,6 @@ 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. @@ -216,7 +215,7 @@ impl NodesetHelpers for Vec<DomRoot<Node>> { } fn document_order(&self) -> Vec<DomRoot<Node>> { let mut nodes: Vec<DomRoot<Node>> = self.clone(); - if nodes.len() == 1 { + if nodes.len() <= 1 { return nodes; } @@ -233,10 +232,13 @@ impl NodesetHelpers for Vec<DomRoot<Node>> { 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()); + let mut seen = HashSet::new(); + let unique_nodes: Vec<DomRoot<Node>> = self + .iter() + .filter(|node| seen.insert(node.to_opaque())) + .cloned() + .collect(); - nodes + unique_nodes.document_order() } } diff --git a/components/script/xpath/parser.rs b/components/script/xpath/parser.rs index b1a4bfcc42d..272fa41dcdf 100644 --- a/components/script/xpath/parser.rs +++ b/components/script/xpath/parser.rs @@ -510,40 +510,45 @@ fn union_expr(input: &str) -> IResult<&str, 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 { + map( + pair(tag("//"), move |i| relative_path_expr(true, i)), + |(_, rel_path)| { + Expr::Path(PathExpr { + is_absolute: true, + is_descendant: true, + steps: match rel_path { Expr::Path(p) => p.steps, _ => unreachable!(), - }) - .unwrap_or_default(), - }) - }), + }, + }) + }, + ), + // "/" RelativePathExpr? + map( + pair(char('/'), opt(move |i| relative_path_expr(false, i))), + |(_, 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, + move |i| relative_path_expr(false, i), ))(input) } -fn relative_path_expr(input: &str) -> IResult<&str, Expr> { - let (input, first) = step_expr(input)?; +fn relative_path_expr(is_descendant: bool, input: &str) -> IResult<&str, Expr> { + let (input, first) = step_expr(is_descendant, input)?; let (input, steps) = many0(pair( - // ("/" | "//") - ws(alt((value(false, char('/')), value(true, tag("//"))))), - step_expr, + ws(alt((value(true, tag("//")), value(false, char('/'))))), + move |i| step_expr(is_descendant, i), ))(input)?; let mut all_steps = vec![first]; @@ -569,16 +574,18 @@ fn relative_path_expr(input: &str) -> IResult<&str, Expr> { )) } -fn step_expr(input: &str) -> IResult<&str, StepExpr> { +fn step_expr(is_descendant: bool, input: &str) -> IResult<&str, StepExpr> { alt(( map(filter_expr, StepExpr::Filter), - map(axis_step, StepExpr::Axis), + map(|i| axis_step(is_descendant, i), StepExpr::Axis), ))(input) } -fn axis_step(input: &str) -> IResult<&str, AxisStep> { - let (input, (step, predicates)) = - pair(alt((forward_step, reverse_step)), predicate_list)(input)?; +fn axis_step(is_descendant: bool, input: &str) -> IResult<&str, AxisStep> { + let (input, (step, predicates)) = pair( + alt((move |i| forward_step(is_descendant, i), reverse_step)), + predicate_list, + )(input)?; let (axis, node_test) = step; Ok(( @@ -591,13 +598,10 @@ fn axis_step(input: &str) -> IResult<&str, AxisStep> { )) } -fn forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { - alt(( - // ForwardAxis NodeTest - pair(forward_axis, node_test), - // AbbrevForwardStep - abbrev_forward_step, - ))(input) +fn forward_step(is_descendant: bool, input: &str) -> IResult<&str, (Axis, NodeTest)> { + alt((pair(forward_axis, node_test), move |i| { + abbrev_forward_step(is_descendant, i) + }))(input) } fn forward_axis(input: &str) -> IResult<&str, Axis> { @@ -615,7 +619,7 @@ fn forward_axis(input: &str) -> IResult<&str, Axis> { Ok((input, axis)) } -fn abbrev_forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { +fn abbrev_forward_step(is_descendant: bool, input: &str) -> IResult<&str, (Axis, NodeTest)> { let (input, attr) = opt(char('@'))(input)?; let (input, test) = node_test(input)?; @@ -624,6 +628,8 @@ fn abbrev_forward_step(input: &str) -> IResult<&str, (Axis, NodeTest)> { ( if attr.is_some() { Axis::Attribute + } else if is_descendant { + Axis::DescendantOrSelf } else { Axis::Child }, @@ -704,6 +710,7 @@ fn filter_expr(input: &str) -> IResult<&str, FilterExpr> { fn predicate_list(input: &str) -> IResult<&str, PredicateListExpr> { let (input, predicates) = many0(predicate)(input)?; + Ok((input, PredicateListExpr { predicates })) } @@ -1195,6 +1202,118 @@ mod tests { ], }), ), + ( + "//mu[@xml:id=\"id1\"]//rho[@title][@xml:lang=\"en-GB\"]", + 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: "mu".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![PredicateExpr { + expr: Expr::Equality( + 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: Some("xml".to_string()), + local_part: "id".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + EqualityOp::Eq, + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "id1".to_string(), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + ), + }], + }, + }), + StepExpr::Axis(AxisStep { + axis: Axis::DescendantOrSelf, // Represents the second '//' + node_test: NodeTest::Kind(KindTest::Node), + predicates: PredicateListExpr { predicates: vec![] }, + }), + StepExpr::Axis(AxisStep { + axis: Axis::Child, + node_test: NodeTest::Name(QName { + prefix: None, + local_part: "rho".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![ + PredicateExpr { + expr: 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: "title".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + }), + }, + PredicateExpr { + expr: Expr::Equality( + 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: Some("xml".to_string()), + local_part: "lang".to_string(), + }), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + EqualityOp::Eq, + Box::new(Expr::Path(PathExpr { + is_absolute: false, + is_descendant: false, + steps: vec![StepExpr::Filter(FilterExpr { + primary: PrimaryExpr::Literal(Literal::String( + "en-GB".to_string(), + )), + predicates: PredicateListExpr { + predicates: vec![], + }, + })], + })), + ), + }, + ], + }, + }), + ], + }), + ), ]; for (input, expected) in cases { |