aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/xpath
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/xpath')
-rw-r--r--components/script/xpath/context.rs12
-rw-r--r--components/script/xpath/eval.rs88
-rw-r--r--components/script/xpath/eval_function.rs65
-rw-r--r--components/script/xpath/eval_value.rs14
-rw-r--r--components/script/xpath/parser.rs199
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 {