/* 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::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::element::Element; use crate::dom::node::Node; /// Returns e.g. "rect" for `` fn local_name(node: &Node) -> Option { if matches!(Node::type_id(node), NodeTypeId::Element(_)) { let element = node.downcast::().unwrap(); Some(element.local_name().to_string()) } else { None } } /// Returns e.g. "svg:rect" for `` fn name(node: &Node) -> Option { if matches!(Node::type_id(node), NodeTypeId::Element(_)) { let element = node.downcast::().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 `` fn namespace_uri(node: &Node) -> Option { if matches!(Node::type_id(node), NodeTypeId::Element(_)) { let element = node.downcast::().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) -> 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() } /// pub(crate) 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 { 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, _> = 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)), ""); } }