Skip to main content

servo_xpath/
functions.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use markup5ever::{LocalName, Prefix};
6
7use crate::ast::CoreFunction;
8use crate::context::EvaluationCtx;
9use crate::eval::try_extract_nodeset;
10use crate::value::{NodeSet, parse_number_from_string};
11use crate::{Attribute, Document, Dom, Element, Error, Node, Value};
12
13struct NodeNameParts {
14    prefix: Option<Prefix>,
15    local_name: LocalName,
16}
17
18fn name_parts<N: Node>(node: &N) -> Option<NodeNameParts> {
19    match (node.as_element(), node.as_attribute()) {
20        (Some(element), _) => Some(NodeNameParts {
21            prefix: element.prefix(),
22            local_name: element.local_name(),
23        }),
24        (_, Some(attribute)) => Some(NodeNameParts {
25            prefix: attribute.prefix(),
26            local_name: attribute.local_name(),
27        }),
28        _ => None,
29    }
30}
31
32/// Returns e.g. "rect" for `<svg:rect>`
33fn local_name<N: Node>(node: &N) -> Option<String> {
34    name_parts(node).map(|node_name_parts| node_name_parts.local_name.to_string())
35}
36
37/// Returns e.g. "svg:rect" for `<svg:rect>`
38fn name<N: Node>(node: &N) -> Option<String> {
39    let NodeNameParts { prefix, local_name } = name_parts(node)?;
40
41    if let Some(prefix) = prefix {
42        Some(format!("{}:{}", prefix, local_name))
43    } else {
44        Some(local_name.to_string())
45    }
46}
47
48/// Returns e.g. the SVG namespace URI for `<svg:rect>`
49fn namespace_uri<N: Node>(node: &N) -> Option<String> {
50    match (node.as_element(), node.as_attribute()) {
51        (Some(element), _) => Some(element.namespace().to_string()),
52        (_, Some(attribute)) => Some(attribute.namespace().to_string()),
53        _ => None,
54    }
55}
56
57/// If s2 is found inside s1, return everything *before* s2. Return all of s1 otherwise.
58fn substring_before(s1: &str, s2: &str) -> String {
59    match s1.find(s2) {
60        Some(pos) => s1[..pos].to_string(),
61        None => String::new(),
62    }
63}
64
65/// If s2 is found inside s1, return everything *after* s2. Return all of s1 otherwise.
66fn substring_after(s1: &str, s2: &str) -> String {
67    match s1.find(s2) {
68        Some(pos) => s1[pos + s2.len()..].to_string(),
69        None => String::new(),
70    }
71}
72
73/// <https://www.w3.org/TR/xpath-10/#function-substring>
74fn substring(source: &str, start: isize, length: Option<isize>) -> String {
75    let start_index = start.max(0) as usize;
76    let length = length
77        .map(|length| length.max(0) as usize)
78        .unwrap_or(usize::MAX);
79
80    // The specification doesn't tell us whether the term "length" refers
81    // to bytes, codepoints, graphemes etc. We choose code points.
82    // Firefox uses bytes and allows slicing at indices that are not char boundaries... Let's not do that.
83    source.chars().skip(start_index).take(length).collect()
84}
85
86/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-normalize-space>
87pub(crate) fn normalize_space(input: &str) -> String {
88    // Trim leading and trailing whitespace
89    let input = input.trim_ascii();
90
91    let mut result = String::with_capacity(input.len());
92    input
93        .split([' ', '\x09', '\x0D', '\x0A'])
94        .filter(|segment| !segment.is_empty())
95        .for_each(|segment| {
96            if !result.is_empty() {
97                result.push(' ');
98            }
99
100            result.push_str(segment);
101        });
102
103    result
104}
105
106/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-lang>
107fn lang_matches(context_lang: Option<&str>, target_lang: &str) -> bool {
108    let Some(context_lang) = context_lang else {
109        return false;
110    };
111
112    let context_lower = context_lang.to_ascii_lowercase();
113    let target_lower = target_lang.to_ascii_lowercase();
114
115    if context_lower == target_lower {
116        return true;
117    }
118
119    // Check if context is target with additional suffix
120    if context_lower.starts_with(&target_lower) {
121        // Make sure the next character is a hyphen to avoid matching
122        // e.g. "england" when target is "en"
123        if let Some(next_char) = context_lower.chars().nth(target_lower.len()) {
124            return next_char == '-';
125        }
126    }
127
128    false
129}
130
131/// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-translate>
132fn translate(input: &str, from: &str, to: &str) -> String {
133    let mut result = String::with_capacity(input.len());
134
135    for character in input.chars() {
136        let Some(replacement_index) = from.chars().position(|to_replace| to_replace == character)
137        else {
138            result.push(character);
139            continue;
140        };
141
142        if let Some(replace_with) = to.chars().nth(replacement_index) {
143            result.push(replace_with);
144        }
145    }
146
147    result
148}
149
150impl CoreFunction {
151    pub(crate) fn evaluate<D: Dom>(
152        &self,
153        cx: &mut D::Context,
154        context: &EvaluationCtx<D>,
155    ) -> Result<Value<D::Node>, Error> {
156        match self {
157            CoreFunction::Last => {
158                let predicate_ctx = context.predicate_ctx.ok_or_else(|| Error::Internal {
159                    msg: "[CoreFunction] last() is only usable as a predicate".to_string(),
160                })?;
161                Ok(Value::Number(predicate_ctx.size as f64))
162            },
163            CoreFunction::Position => {
164                let predicate_ctx = context.predicate_ctx.ok_or_else(|| Error::Internal {
165                    msg: "[CoreFunction] position() is only usable as a predicate".to_string(),
166                })?;
167                Ok(Value::Number(predicate_ctx.index as f64))
168            },
169            CoreFunction::Count(expr) => {
170                let nodes = expr.evaluate(cx, context).and_then(try_extract_nodeset)?;
171                Ok(Value::Number(nodes.len() as f64))
172            },
173            CoreFunction::String(expr_opt) => match expr_opt {
174                Some(expr) => Ok(Value::String(
175                    expr.evaluate(cx, context)?.convert_to_string(cx),
176                )),
177                None => Ok(Value::String(context.context_node.text_content())),
178            },
179            CoreFunction::Concat(exprs) => {
180                let strings: Result<Vec<_>, _> = exprs
181                    .iter()
182                    .map(|e| Ok(e.evaluate(cx, context)?.convert_to_string(cx)))
183                    .collect();
184                Ok(Value::String(strings?.join("")))
185            },
186            CoreFunction::Id(expr) => {
187                let argument = expr.evaluate(cx, context)?;
188                let document = context.context_node.owner_document();
189                let mut result = NodeSet::default();
190
191                // https://www.w3.org/TR/1999/REC-xpath-19991116/#function-id
192                // > When the argument to id is of type node-set, then the result is the union of the result
193                // > of applying id to the string-value of each of the nodes in the argument node-set.
194                let mut extend_result_with_matching_nodes = |cx: &mut D::Context, input: &str| {
195                    for id in normalize_space(input).split(' ') {
196                        result.extend(
197                            document
198                                .get_elements_with_id(cx, id)
199                                .map(|element| element.as_node()),
200                        );
201                    }
202                };
203
204                if let Value::NodeSet(node_set) = argument {
205                    for node in node_set.iter() {
206                        extend_result_with_matching_nodes(cx, &node.text_content())
207                    }
208                } else {
209                    let conversion_to_string = argument.convert_to_string(cx);
210                    extend_result_with_matching_nodes(cx, &conversion_to_string)
211                }
212
213                result.sort(cx);
214                Ok(Value::NodeSet(result))
215            },
216            CoreFunction::LocalName(expr_opt) => {
217                let node = match expr_opt {
218                    Some(expr) => expr
219                        .evaluate(cx, context)
220                        .and_then(try_extract_nodeset)?
221                        .first(cx),
222                    None => Some(context.context_node.clone()),
223                };
224                let name = node.and_then(|n| local_name(&n)).unwrap_or_default();
225                Ok(Value::String(name))
226            },
227            CoreFunction::NamespaceUri(expr_opt) => {
228                let node = match expr_opt {
229                    Some(expr) => expr
230                        .evaluate(cx, context)
231                        .and_then(try_extract_nodeset)?
232                        .first(cx),
233                    None => Some(context.context_node.clone()),
234                };
235                let ns = node.and_then(|n| namespace_uri(&n)).unwrap_or_default();
236                Ok(Value::String(ns))
237            },
238            CoreFunction::Name(expr_opt) => {
239                let node = match expr_opt {
240                    Some(expr) => expr
241                        .evaluate(cx, context)
242                        .and_then(try_extract_nodeset)?
243                        .first(cx),
244                    None => Some(context.context_node.clone()),
245                };
246                let name = node.and_then(|n| name(&n)).unwrap_or_default();
247                Ok(Value::String(name))
248            },
249            CoreFunction::StartsWith(str1, str2) => {
250                let s1 = str1.evaluate(cx, context)?.convert_to_string(cx);
251                let s2 = str2.evaluate(cx, context)?.convert_to_string(cx);
252                Ok(Value::Boolean(s1.starts_with(&s2)))
253            },
254            CoreFunction::Contains(str1, str2) => {
255                let s1 = str1.evaluate(cx, context)?.convert_to_string(cx);
256                let s2 = str2.evaluate(cx, context)?.convert_to_string(cx);
257                Ok(Value::Boolean(s1.contains(&s2)))
258            },
259            CoreFunction::SubstringBefore(str1, str2) => {
260                let s1 = str1.evaluate(cx, context)?.convert_to_string(cx);
261                let s2 = str2.evaluate(cx, context)?.convert_to_string(cx);
262                Ok(Value::String(substring_before(&s1, &s2)))
263            },
264            CoreFunction::SubstringAfter(str1, str2) => {
265                let s1 = str1.evaluate(cx, context)?.convert_to_string(cx);
266                let s2 = str2.evaluate(cx, context)?.convert_to_string(cx);
267                Ok(Value::String(substring_after(&s1, &s2)))
268            },
269            CoreFunction::Substring(source_expression, start, length) => {
270                let source = source_expression
271                    .evaluate(cx, context)?
272                    .convert_to_string(cx);
273                let start_idx =
274                    start.evaluate(cx, context)?.convert_to_number(cx).round() as isize - 1;
275                let result = if let Some(length_expression) = length {
276                    let length = length_expression
277                        .evaluate(cx, context)?
278                        .convert_to_number(cx)
279                        .round() as isize;
280                    substring(&source, start_idx, Some(length))
281                } else {
282                    substring(&source, start_idx, None)
283                };
284                Ok(Value::String(result))
285            },
286            CoreFunction::StringLength(expr_opt) => {
287                let string = match expr_opt {
288                    Some(expr) => expr.evaluate(cx, context)?.convert_to_string(cx),
289                    None => context.context_node.text_content(),
290                };
291                Ok(Value::Number(string.chars().count() as f64))
292            },
293            CoreFunction::NormalizeSpace(expr_opt) => {
294                let string = match expr_opt {
295                    Some(expr) => expr.evaluate(cx, context)?.convert_to_string(cx),
296                    None => context.context_node.text_content(),
297                };
298
299                Ok(Value::String(normalize_space(&string)))
300            },
301            CoreFunction::Translate(str1, str2, str3) => {
302                let string = str1.evaluate(cx, context)?.convert_to_string(cx);
303                let from = str2.evaluate(cx, context)?.convert_to_string(cx);
304                let to = str3.evaluate(cx, context)?.convert_to_string(cx);
305                Ok(Value::String(translate(&string, &from, &to)))
306            },
307            CoreFunction::Number(expr_opt) => {
308                let val = match expr_opt {
309                    Some(expr) => expr.evaluate(cx, context)?,
310                    None => Value::String(context.context_node.text_content()),
311                };
312                Ok(Value::Number(val.convert_to_number(cx)))
313            },
314            CoreFunction::Sum(expr) => {
315                let nodes = expr.evaluate(cx, context).and_then(try_extract_nodeset)?;
316                let sum = nodes
317                    .iter()
318                    .map(|node| parse_number_from_string(&node.text_content()))
319                    .sum();
320                Ok(Value::Number(sum))
321            },
322            CoreFunction::Floor(expr) => {
323                let num = expr.evaluate(cx, context)?.convert_to_number(cx);
324                Ok(Value::Number(num.floor()))
325            },
326            CoreFunction::Ceiling(expr) => {
327                let num = expr.evaluate(cx, context)?.convert_to_number(cx);
328                Ok(Value::Number(num.ceil()))
329            },
330            CoreFunction::Round(expr) => {
331                let num = expr.evaluate(cx, context)?.convert_to_number(cx);
332                Ok(Value::Number(num.round()))
333            },
334            CoreFunction::Boolean(expr) => Ok(Value::Boolean(
335                expr.evaluate(cx, context)?.convert_to_boolean(),
336            )),
337            CoreFunction::Not(expr) => Ok(Value::Boolean(
338                !expr.evaluate(cx, context)?.convert_to_boolean(),
339            )),
340            CoreFunction::True => Ok(Value::Boolean(true)),
341            CoreFunction::False => Ok(Value::Boolean(false)),
342            CoreFunction::Lang(expr) => {
343                let context_lang = context.context_node.language();
344                let lang = expr.evaluate(cx, context)?.convert_to_string(cx);
345                Ok(Value::Boolean(lang_matches(context_lang.as_deref(), &lang)))
346            },
347        }
348    }
349}
350#[cfg(test)]
351mod tests {
352    use super::{lang_matches, substring, substring_after, substring_before};
353    use crate::functions::{normalize_space, translate};
354
355    #[test]
356    fn test_substring_before() {
357        assert_eq!(substring_before("hello world", "world"), "hello ");
358        assert_eq!(substring_before("prefix:name", ":"), "prefix");
359        assert_eq!(substring_before("no-separator", "xyz"), "");
360        assert_eq!(substring_before("", "anything"), "");
361        assert_eq!(substring_before("multiple:colons:here", ":"), "multiple");
362        assert_eq!(substring_before("start-match-test", "start"), "");
363    }
364
365    #[test]
366    fn test_substring_after() {
367        assert_eq!(substring_after("hello world", "hello "), "world");
368        assert_eq!(substring_after("prefix:name", ":"), "name");
369        assert_eq!(substring_after("no-separator", "xyz"), "");
370        assert_eq!(substring_after("", "anything"), "");
371        assert_eq!(substring_after("multiple:colons:here", ":"), "colons:here");
372        assert_eq!(substring_after("test-end-match", "match"), "");
373    }
374
375    #[test]
376    fn test_substring() {
377        assert_eq!(substring("hello world", 0, Some(5)), "hello");
378        assert_eq!(substring("hello world", 6, Some(5)), "world");
379        assert_eq!(substring("hello", 1, Some(3)), "ell");
380        assert_eq!(substring("hello", -5, Some(2)), "he");
381        assert_eq!(substring("hello", 0, None), "hello");
382        assert_eq!(substring("hello", 2, Some(10)), "llo");
383        assert_eq!(substring("hello", 5, Some(1)), "");
384        assert_eq!(substring("", 0, Some(5)), "");
385        assert_eq!(substring("hello", 0, Some(0)), "");
386        assert_eq!(substring("hello", 0, Some(-5)), "");
387    }
388
389    #[test]
390    fn test_substring_with_out_of_bounds_index() {
391        assert_eq!(substring("Servo", 42, None), "");
392    }
393
394    #[test]
395    fn test_substring_with_multi_byte_characters() {
396        assert_eq!(substring("๐Ÿฆž๐Ÿฆž๐Ÿฆž", 1, None), "๐Ÿฆž๐Ÿฆž");
397    }
398
399    #[test]
400    fn test_lang_matches() {
401        assert!(lang_matches(Some("en"), "en"));
402        assert!(lang_matches(Some("EN"), "en"));
403        assert!(lang_matches(Some("en"), "EN"));
404        assert!(lang_matches(Some("en-US"), "en"));
405        assert!(lang_matches(Some("en-GB"), "en"));
406
407        assert!(!lang_matches(Some("eng"), "en"));
408        assert!(!lang_matches(Some("fr"), "en"));
409        assert!(!lang_matches(Some("fr-en"), "en"));
410        assert!(!lang_matches(None, "en"));
411    }
412
413    #[test]
414    fn test_normalize_space() {
415        assert_eq!(normalize_space(" "), "");
416        assert_eq!(normalize_space("\n\t\r "), "");
417        assert_eq!(normalize_space("no-space"), "no-space");
418        assert_eq!(normalize_space("one space"), "one space");
419        assert_eq!(normalize_space("more    whitespace"), "more whitespace");
420        assert_eq!(
421            normalize_space("  \t leading  and trailing\n"),
422            "leading and trailing"
423        );
424    }
425
426    #[test]
427    fn test_translate() {
428        assert_eq!(translate("", "", ""), "");
429        assert_eq!(translate("", "abc", ""), "");
430        assert_eq!(translate("abcd", "abc", ""), "d");
431        assert_eq!(translate("abcd", "abc", "cba"), "cbad");
432        assert_eq!(translate("abc", "", "abc"), "abc");
433    }
434
435    #[test]
436    fn test_translate_with_multi_byte_characters() {
437        assert_eq!(translate("a๐Ÿฆžb๐Ÿ˜c๐Ÿฆžd", "๐Ÿ˜c", "๐Ÿคจ๐Ÿค–"), "a๐Ÿฆžb๐Ÿคจ๐Ÿค–๐Ÿฆžd");
438    }
439}