mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
* 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>
357 lines
15 KiB
Rust
357 lines
15 KiB
Rust
/* 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)), "");
|
|
}
|
|
}
|