xpath/
value.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 std::borrow::ToOwned;
6use std::collections::HashSet;
7
8use crate::Node;
9
10/// The primary types of values that an XPath expression returns as a result.
11#[derive(Debug)]
12pub enum Value<N: Node> {
13    Boolean(bool),
14    /// A IEEE-754 double-precision floating point number.
15    Number(f64),
16    String(String),
17    /// A collection of not-necessarily-unique nodes.
18    Nodeset(Vec<N>),
19}
20
21pub(crate) fn parse_number_from_string(string: &str) -> f64 {
22    // https://www.w3.org/TR/1999/REC-xpath-19991116/#function-number:
23    // > a string that consists of optional whitespace followed by an optional minus sign followed
24    // > by a Number followed by whitespace is converted to the IEEE 754 number that is nearest
25    // > (according to the IEEE 754 round-to-nearest rule) to the mathematical value represented
26    // > by the string; any other string is converted to NaN
27
28    // The specification does not define what "whitespace" means exactly, we choose to trim only ascii whitespace,
29    // as that seems to be what other browsers do.
30    string.trim_ascii().parse().unwrap_or(f64::NAN)
31}
32
33/// Helper for `PartialEq<Value>` implementations
34fn num_vals<N: Node>(nodes: &[N]) -> Vec<f64> {
35    nodes
36        .iter()
37        .map(|node| parse_number_from_string(&node.text_content()))
38        .collect()
39}
40
41impl<N: Node> PartialEq<Value<N>> for Value<N> {
42    fn eq(&self, other: &Value<N>) -> bool {
43        match (self, other) {
44            (Value::Nodeset(left_nodes), Value::Nodeset(right_nodes)) => {
45                let left_strings: HashSet<String> =
46                    left_nodes.iter().map(|node| node.text_content()).collect();
47                let right_strings: HashSet<String> =
48                    right_nodes.iter().map(|node| node.text_content()).collect();
49                !left_strings.is_disjoint(&right_strings)
50            },
51            (&Value::Nodeset(ref nodes), &Value::Number(val)) |
52            (&Value::Number(val), &Value::Nodeset(ref nodes)) => {
53                let numbers = num_vals(nodes);
54                numbers.contains(&val)
55            },
56            (&Value::Nodeset(ref nodes), &Value::String(ref string)) |
57            (&Value::String(ref string), &Value::Nodeset(ref nodes)) => nodes
58                .iter()
59                .map(|node| node.text_content())
60                .any(|text_content| &text_content == string),
61            (&Value::Boolean(_), _) | (_, &Value::Boolean(_)) => {
62                self.convert_to_boolean() == other.convert_to_boolean()
63            },
64            (&Value::Number(_), _) | (_, &Value::Number(_)) => {
65                self.convert_to_number() == other.convert_to_number()
66            },
67            _ => self.convert_to_string() == other.convert_to_string(),
68        }
69    }
70}
71
72impl<N: Node> Value<N> {
73    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-boolean>
74    pub fn convert_to_boolean(&self) -> bool {
75        match self {
76            Value::Boolean(boolean) => *boolean,
77            Value::Number(number) => *number != 0.0 && !number.is_nan(),
78            Value::String(string) => !string.is_empty(),
79            Value::Nodeset(nodeset) => !nodeset.is_empty(),
80        }
81    }
82
83    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-number>
84    pub fn convert_to_number(&self) -> f64 {
85        match self {
86            Value::Boolean(boolean) => {
87                if *boolean {
88                    1.0
89                } else {
90                    0.0
91                }
92            },
93            Value::Number(number) => *number,
94            Value::String(string) => parse_number_from_string(string),
95            Value::Nodeset(_) => parse_number_from_string(&self.convert_to_string()),
96        }
97    }
98
99    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-string>
100    pub fn convert_to_string(&self) -> String {
101        match self {
102            Value::Boolean(value) => value.to_string(),
103            Value::Number(number) => {
104                if number.is_infinite() {
105                    if number.is_sign_negative() {
106                        "-Infinity".to_owned()
107                    } else {
108                        "Infinity".to_owned()
109                    }
110                } else if *number == 0.0 {
111                    // catches -0.0 also
112                    "0".into()
113                } else {
114                    number.to_string()
115                }
116            },
117            Value::String(string) => string.to_owned(),
118            Value::Nodeset(nodes) => nodes
119                .document_order_first()
120                .as_ref()
121                .map(Node::text_content)
122                .unwrap_or_default(),
123        }
124    }
125}
126
127macro_rules! from_impl {
128    ($raw:ty, $variant:expr) => {
129        impl<N: Node> From<$raw> for Value<N> {
130            fn from(other: $raw) -> Self {
131                $variant(other)
132            }
133        }
134    };
135}
136
137from_impl!(bool, Value::Boolean);
138from_impl!(f64, Value::Number);
139from_impl!(String, Value::String);
140impl<'a, N: Node> From<&'a str> for Value<N> {
141    fn from(other: &'a str) -> Self {
142        Value::String(other.into())
143    }
144}
145from_impl!(Vec<N>, Value::Nodeset);
146
147macro_rules! partial_eq_impl {
148    ($raw:ty, $variant:pat => $b:expr) => {
149        impl<N: Node> PartialEq<$raw> for Value<N> {
150            fn eq(&self, other: &$raw) -> bool {
151                match *self {
152                    $variant => $b == other,
153                    _ => false,
154                }
155            }
156        }
157
158        impl<N: Node> PartialEq<Value<N>> for $raw {
159            fn eq(&self, other: &Value<N>) -> bool {
160                match *other {
161                    $variant => $b == self,
162                    _ => false,
163                }
164            }
165        }
166    };
167}
168
169partial_eq_impl!(bool, Value::Boolean(ref v) => v);
170partial_eq_impl!(f64, Value::Number(ref v) => v);
171partial_eq_impl!(String, Value::String(ref v) => v);
172partial_eq_impl!(&str, Value::String(ref v) => v);
173partial_eq_impl!(Vec<N>, Value::Nodeset(ref v) => v);
174
175pub trait NodesetHelpers<N: Node> {
176    /// Returns the node that occurs first in [document order]
177    ///
178    /// [document order]: https://www.w3.org/TR/xpath/#dt-document-order
179    fn document_order_first(&self) -> Option<N>;
180    fn document_order(&self) -> Vec<N>;
181    fn document_order_unique(&self) -> Vec<N>;
182}
183
184impl<N: Node> NodesetHelpers<N> for Vec<N> {
185    fn document_order_first(&self) -> Option<N> {
186        self.iter().min_by(|a, b| a.compare_tree_order(b)).cloned()
187    }
188
189    fn document_order(&self) -> Vec<N> {
190        let mut nodes: Vec<N> = self.clone();
191        if nodes.len() <= 1 {
192            return nodes;
193        }
194
195        nodes.sort_by(|a, b| a.compare_tree_order(b));
196
197        nodes
198    }
199
200    fn document_order_unique(&self) -> Vec<N> {
201        let mut seen = HashSet::new();
202        let unique_nodes: Vec<N> = self
203            .iter()
204            .filter(|node| seen.insert(node.to_opaque()))
205            .cloned()
206            .collect();
207
208        unique_nodes.document_order()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use std::f64;
215
216    use crate::dummy_implementation;
217
218    type Value = super::Value<dummy_implementation::DummyNode>;
219
220    #[test]
221    fn string_value_to_number() {
222        assert_eq!(Value::String("42.123".into()).convert_to_number(), 42.123);
223        assert_eq!(Value::String(" 42\n".into()).convert_to_number(), 42.);
224        assert!(
225            Value::String("totally-invalid".into())
226                .convert_to_number()
227                .is_nan()
228        );
229
230        // U+2004 is non-ascii whitespace, which should be rejected
231        assert!(
232            Value::String("\u{2004}42".into())
233                .convert_to_number()
234                .is_nan()
235        );
236    }
237
238    #[test]
239    fn number_value_to_string() {
240        assert_eq!(Value::Number(f64::NAN).convert_to_string(), "NaN");
241        assert_eq!(Value::Number(0.).convert_to_string(), "0");
242        assert_eq!(Value::Number(-0.).convert_to_string(), "0");
243        assert_eq!(Value::Number(f64::INFINITY).convert_to_string(), "Infinity");
244        assert_eq!(
245            Value::Number(f64::NEG_INFINITY).convert_to_string(),
246            "-Infinity"
247        );
248        assert_eq!(Value::Number(42.0).convert_to_string(), "42");
249        assert_eq!(Value::Number(-42.0).convert_to_string(), "-42");
250        assert_eq!(Value::Number(0.75).convert_to_string(), "0.75");
251        assert_eq!(Value::Number(-0.75).convert_to_string(), "-0.75");
252    }
253
254    #[test]
255    fn boolean_value_to_string() {
256        assert_eq!(Value::Boolean(false).convert_to_string(), "false");
257        assert_eq!(Value::Boolean(true).convert_to_string(), "true");
258    }
259}