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(_)) => self.boolean() == other.boolean(),
62            (&Value::Number(_), _) | (_, &Value::Number(_)) => self.number() == other.number(),
63            _ => self.string() == other.string(),
64        }
65    }
66}
67
68impl<N: Node> Value<N> {
69    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-boolean>
70    pub(crate) fn boolean(&self) -> bool {
71        match self {
72            Value::Boolean(boolean) => *boolean,
73            Value::Number(number) => *number != 0.0 && !number.is_nan(),
74            Value::String(string) => !string.is_empty(),
75            Value::Nodeset(nodeset) => !nodeset.is_empty(),
76        }
77    }
78
79    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-number>
80    pub(crate) fn number(&self) -> f64 {
81        match self {
82            Value::Boolean(boolean) => {
83                if *boolean {
84                    1.0
85                } else {
86                    0.0
87                }
88            },
89            Value::Number(number) => *number,
90            Value::String(string) => parse_number_from_string(string),
91            Value::Nodeset(_) => parse_number_from_string(&self.string()),
92        }
93    }
94
95    /// <https://www.w3.org/TR/1999/REC-xpath-19991116/#function-string>
96    pub(crate) fn string(&self) -> String {
97        match self {
98            Value::Boolean(value) => value.to_string(),
99            Value::Number(number) => {
100                if number.is_infinite() {
101                    if number.is_sign_negative() {
102                        "-Infinity".to_owned()
103                    } else {
104                        "Infinity".to_owned()
105                    }
106                } else if *number == 0.0 {
107                    // catches -0.0 also
108                    "0".into()
109                } else {
110                    number.to_string()
111                }
112            },
113            Value::String(string) => string.to_owned(),
114            Value::Nodeset(nodes) => nodes
115                .document_order_first()
116                .as_ref()
117                .map(Node::text_content)
118                .unwrap_or_default(),
119        }
120    }
121}
122
123macro_rules! from_impl {
124    ($raw:ty, $variant:expr) => {
125        impl<N: Node> From<$raw> for Value<N> {
126            fn from(other: $raw) -> Self {
127                $variant(other)
128            }
129        }
130    };
131}
132
133from_impl!(bool, Value::Boolean);
134from_impl!(f64, Value::Number);
135from_impl!(String, Value::String);
136impl<'a, N: Node> From<&'a str> for Value<N> {
137    fn from(other: &'a str) -> Self {
138        Value::String(other.into())
139    }
140}
141from_impl!(Vec<N>, Value::Nodeset);
142
143macro_rules! partial_eq_impl {
144    ($raw:ty, $variant:pat => $b:expr) => {
145        impl<N: Node> PartialEq<$raw> for Value<N> {
146            fn eq(&self, other: &$raw) -> bool {
147                match *self {
148                    $variant => $b == other,
149                    _ => false,
150                }
151            }
152        }
153
154        impl<N: Node> PartialEq<Value<N>> for $raw {
155            fn eq(&self, other: &Value<N>) -> bool {
156                match *other {
157                    $variant => $b == self,
158                    _ => false,
159                }
160            }
161        }
162    };
163}
164
165partial_eq_impl!(bool, Value::Boolean(ref v) => v);
166partial_eq_impl!(f64, Value::Number(ref v) => v);
167partial_eq_impl!(String, Value::String(ref v) => v);
168partial_eq_impl!(&str, Value::String(ref v) => v);
169partial_eq_impl!(Vec<N>, Value::Nodeset(ref v) => v);
170
171pub trait NodesetHelpers<N: Node> {
172    /// Returns the node that occurs first in [document order]
173    ///
174    /// [document order]: https://www.w3.org/TR/xpath/#dt-document-order
175    fn document_order_first(&self) -> Option<N>;
176    fn document_order(&self) -> Vec<N>;
177    fn document_order_unique(&self) -> Vec<N>;
178}
179
180impl<N: Node> NodesetHelpers<N> for Vec<N> {
181    fn document_order_first(&self) -> Option<N> {
182        self.iter().min_by(|a, b| a.compare_tree_order(b)).cloned()
183    }
184
185    fn document_order(&self) -> Vec<N> {
186        let mut nodes: Vec<N> = self.clone();
187        if nodes.len() <= 1 {
188            return nodes;
189        }
190
191        nodes.sort_by(|a, b| a.compare_tree_order(b));
192
193        nodes
194    }
195
196    fn document_order_unique(&self) -> Vec<N> {
197        let mut seen = HashSet::new();
198        let unique_nodes: Vec<N> = self
199            .iter()
200            .filter(|node| seen.insert(node.to_opaque()))
201            .cloned()
202            .collect();
203
204        unique_nodes.document_order()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use std::f64;
211
212    use crate::dummy_implementation;
213
214    type Value = super::Value<dummy_implementation::DummyNode>;
215
216    #[test]
217    fn string_value_to_number() {
218        assert_eq!(Value::String("42.123".into()).number(), 42.123);
219        assert_eq!(Value::String(" 42\n".into()).number(), 42.);
220        assert!(Value::String("totally-invalid".into()).number().is_nan());
221
222        // U+2004 is non-ascii whitespace, which should be rejected
223        assert!(Value::String("\u{2004}42".into()).number().is_nan());
224    }
225
226    #[test]
227    fn number_value_to_string() {
228        assert_eq!(Value::Number(f64::NAN).string(), "NaN");
229        assert_eq!(Value::Number(0.).string(), "0");
230        assert_eq!(Value::Number(-0.).string(), "0");
231        assert_eq!(Value::Number(f64::INFINITY).string(), "Infinity");
232        assert_eq!(Value::Number(f64::NEG_INFINITY).string(), "-Infinity");
233        assert_eq!(Value::Number(42.0).string(), "42");
234        assert_eq!(Value::Number(-42.0).string(), "-42");
235        assert_eq!(Value::Number(0.75).string(), "0.75");
236        assert_eq!(Value::Number(-0.75).string(), "-0.75");
237    }
238
239    #[test]
240    fn boolean_value_to_string() {
241        assert_eq!(Value::Boolean(false).string(), "false");
242        assert_eq!(Value::Boolean(true).string(), "true");
243    }
244}