1use 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
32fn 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
37fn 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
48fn 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
57fn 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
65fn 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
73fn 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 source.chars().skip(start_index).take(length).collect()
84}
85
86pub(crate) fn normalize_space(input: &str) -> String {
88 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
106fn 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 if context_lower.starts_with(&target_lower) {
121 if let Some(next_char) = context_lower.chars().nth(target_lower.len()) {
124 return next_char == '-';
125 }
126 }
127
128 false
129}
130
131fn 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 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}