/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/xpath/src/functions.rs
413 строк
16 KB
Martin Robinson
script: Add `TreeOrderedIndexMap` to manage `id` and `name` mappings (#45432)
08 июн 2026, 09:02
Не верифицирован
08 июн 2026, 09:02
1c43335
Код
Авторство
О чём код?
/* 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 crate::ast::CoreFunction; use crate::context::EvaluationCtx; use crate::eval::try_extract_nodeset; use crate::value::{NodeSet, parse_number_from_string}; use crate::{Document, Dom, Element, Error, Node, Value}; /// Returns e.g. "rect" for `<svg:rect>` fn local_name<N: Node>(node: &N) -> Option<String> { node.as_element() .map(|element| element.local_name().to_string()) } /// Returns e.g. "svg:rect" for `<svg:rect>` fn name<N: Node>(node: &N) -> Option<String> { node.as_element().map(|element| { if let Some(prefix) = element.prefix().as_ref() { format!("{}:{}", prefix, element.local_name()) } else { element.local_name().to_string() } }) } /// Returns e.g. the SVG namespace URI for `<svg:rect>` fn namespace_uri<N: Node>(node: &N) -> Option<String> { node.as_element() .map(|element| element.namespace().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(), } } /// <https://www.w3.org/TR/xpath-10/#function-substring> fn substring(source: &str, start: isize, length: Option<isize>) -> String { let start_index = start.max(0) as usize; let length = length .map(|length| length.max(0) as usize) .unwrap_or(usize::MAX); // The specification doesn't tell us whether the term "length" refers // to bytes, codepoints, graphemes etc. We choose code points. // Firefox uses bytes and allows slicing at indices that are not char boundaries... Let's not do that. source.chars().skip(start_index).take(length).collect() } /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-normalize-space> pub(crate) fn normalize_space(input: &str) -> String { // Trim leading and trailing whitespace let input = input.trim_ascii(); let mut result = String::with_capacity(input.len()); input .split([' ', '\x09', '\x0D', '\x0A']) .filter(|segment| !segment.is_empty()) .for_each(|segment| { if !result.is_empty() { result.push(' '); } result.push_str(segment); }); 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 } /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-translate> fn translate(input: &str, from: &str, to: &str) -> String { let mut result = String::with_capacity(input.len()); for character in input.chars() { let Some(replacement_index) = from.chars().position(|to_replace| to_replace == character) else { result.push(character); continue; }; if let Some(replace_with) = to.chars().nth(replacement_index) { result.push(replace_with); } } result } impl CoreFunction { pub(crate) fn evaluate<D: Dom>( &self, cx: &mut D::Context, context: &EvaluationCtx<D>, ) -> Result<Value<D::Node>, 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(cx, 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(cx, context)?.convert_to_string(), )), None => Ok(Value::String(context.context_node.text_content())), }, CoreFunction::Concat(exprs) => { let strings: Result<Vec<_>, _> = exprs .iter() .map(|e| Ok(e.evaluate(cx, context)?.convert_to_string())) .collect(); Ok(Value::String(strings?.join(""))) }, CoreFunction::Id(expr) => { let argument = expr.evaluate(cx, context)?; let document = context.context_node.owner_document(); let mut result = NodeSet::default(); // https://www.w3.org/TR/1999/REC-xpath-19991116/#function-id // > When the argument to id is of type node-set, then the result is the union of the result // > of applying id to the string-value of each of the nodes in the argument node-set. let mut extend_result_with_matching_nodes = |cx: &mut D::Context, input: &str| { for id in normalize_space(input).split(' ') { result.extend( document .get_elements_with_id(cx, id) .map(|element| element.as_node()), ); } }; if let Value::NodeSet(node_set) = argument { for node in node_set.iter() { extend_result_with_matching_nodes(cx, &node.text_content()) } } else { extend_result_with_matching_nodes(cx, &argument.convert_to_string()) } result.sort(); Ok(Value::NodeSet(result)) }, CoreFunction::LocalName(expr_opt) => { let node = match expr_opt { Some(expr) => expr .evaluate(cx, context) .and_then(try_extract_nodeset)? .first(), None => Some(context.context_node.clone()), }; let name = node.and_then(|n| local_name(&n)).unwrap_or_default(); Ok(Value::String(name)) }, CoreFunction::NamespaceUri(expr_opt) => { let node = match expr_opt { Some(expr) => expr .evaluate(cx, context) .and_then(try_extract_nodeset)? .first(), None => Some(context.context_node.clone()), }; let ns = node.and_then(|n| namespace_uri(&n)).unwrap_or_default(); Ok(Value::String(ns)) }, CoreFunction::Name(expr_opt) => { let node = match expr_opt { Some(expr) => expr .evaluate(cx, context) .and_then(try_extract_nodeset)? .first(), 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(cx, context)?.convert_to_string(); let s2 = str2.evaluate(cx, context)?.convert_to_string(); Ok(Value::Boolean(s1.starts_with(&s2))) }, CoreFunction::Contains(str1, str2) => { let s1 = str1.evaluate(cx, context)?.convert_to_string(); let s2 = str2.evaluate(cx, context)?.convert_to_string(); Ok(Value::Boolean(s1.contains(&s2))) }, CoreFunction::SubstringBefore(str1, str2) => { let s1 = str1.evaluate(cx, context)?.convert_to_string(); let s2 = str2.evaluate(cx, context)?.convert_to_string(); Ok(Value::String(substring_before(&s1, &s2))) }, CoreFunction::SubstringAfter(str1, str2) => { let s1 = str1.evaluate(cx, context)?.convert_to_string(); let s2 = str2.evaluate(cx, context)?.convert_to_string(); Ok(Value::String(substring_after(&s1, &s2))) }, CoreFunction::Substring(source_expression, start, length) => { let source = source_expression.evaluate(cx, context)?.convert_to_string(); let start_idx = start.evaluate(cx, context)?.convert_to_number().round() as isize - 1; let result = if let Some(length_expression) = length { let length = length_expression .evaluate(cx, context)? .convert_to_number() .round() as isize; substring(&source, start_idx, Some(length)) } else { substring(&source, start_idx, None) }; Ok(Value::String(result)) }, CoreFunction::StringLength(expr_opt) => { let string = match expr_opt { Some(expr) => expr.evaluate(cx, context)?.convert_to_string(), None => context.context_node.text_content(), }; Ok(Value::Number(string.chars().count() as f64)) }, CoreFunction::NormalizeSpace(expr_opt) => { let string = match expr_opt { Some(expr) => expr.evaluate(cx, context)?.convert_to_string(), None => context.context_node.text_content(), }; Ok(Value::String(normalize_space(&string))) }, CoreFunction::Translate(str1, str2, str3) => { let string = str1.evaluate(cx, context)?.convert_to_string(); let from = str2.evaluate(cx, context)?.convert_to_string(); let to = str3.evaluate(cx, context)?.convert_to_string(); Ok(Value::String(translate(&string, &from, &to))) }, CoreFunction::Number(expr_opt) => { let val = match expr_opt { Some(expr) => expr.evaluate(cx, context)?, None => Value::String(context.context_node.text_content()), }; Ok(Value::Number(val.convert_to_number())) }, CoreFunction::Sum(expr) => { let nodes = expr.evaluate(cx, context).and_then(try_extract_nodeset)?; let sum = nodes .iter() .map(|node| parse_number_from_string(&node.text_content())) .sum(); Ok(Value::Number(sum)) }, CoreFunction::Floor(expr) => { let num = expr.evaluate(cx, context)?.convert_to_number(); Ok(Value::Number(num.floor())) }, CoreFunction::Ceiling(expr) => { let num = expr.evaluate(cx, context)?.convert_to_number(); Ok(Value::Number(num.ceil())) }, CoreFunction::Round(expr) => { let num = expr.evaluate(cx, context)?.convert_to_number(); Ok(Value::Number(num.round())) }, CoreFunction::Boolean(expr) => Ok(Value::Boolean( expr.evaluate(cx, context)?.convert_to_boolean(), )), CoreFunction::Not(expr) => Ok(Value::Boolean( !expr.evaluate(cx, context)?.convert_to_boolean(), )), CoreFunction::True => Ok(Value::Boolean(true)), CoreFunction::False => Ok(Value::Boolean(false)), CoreFunction::Lang(expr) => { let context_lang = context.context_node.language(); let lang = expr.evaluate(cx, context)?.convert_to_string(); Ok(Value::Boolean(lang_matches(context_lang.as_deref(), &lang))) }, } } } #[cfg(test)] mod tests { use super::{lang_matches, substring, substring_after, substring_before}; use crate::functions::{normalize_space, translate}; #[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)), ""); } #[test] fn test_substring_with_out_of_bounds_index() { assert_eq!(substring("Servo", 42, None), ""); } #[test] fn test_substring_with_multi_byte_characters() { assert_eq!(substring("🦞🦞🦞", 1, None), "🦞🦞"); } #[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")); } #[test] fn test_normalize_space() { assert_eq!(normalize_space(" "), ""); assert_eq!(normalize_space("\n\t\r "), ""); assert_eq!(normalize_space("no-space"), "no-space"); assert_eq!(normalize_space("one space"), "one space"); assert_eq!(normalize_space("more whitespace"), "more whitespace"); assert_eq!( normalize_space(" \t leading and trailing\n"), "leading and trailing" ); } #[test] fn test_translate() { assert_eq!(translate("", "", ""), ""); assert_eq!(translate("", "abc", ""), ""); assert_eq!(translate("abcd", "abc", ""), "d"); assert_eq!(translate("abcd", "abc", "cba"), "cbad"); assert_eq!(translate("abc", "", "abc"), "abc"); } #[test] fn test_translate_with_multi_byte_characters() { assert_eq!(translate("a🦞b😐c🦞d", "😐c", "🤨🤖"), "a🦞b🤨🤖🦞d"); } }