sup_xml_core/xpath/exslt/
sets.rs1use std::collections::HashSet;
24
25use crate::error::{ErrorDomain, ErrorLevel, XmlError};
26use crate::xpath::eval::Value;
27use crate::xpath::index::{DocIndexLike, NodeId};
28
29use super::Result;
30
31fn err(msg: impl Into<String>) -> XmlError {
32 XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
33}
34
35pub fn dispatch<I: DocIndexLike>(
36 name: &str, args: Vec<Value>, idx: &I,
37) -> Option<Result<Value>> {
38 let r: Result<Value> = match name {
39 "difference" => difference(&args),
40 "intersection" => intersection(&args),
41 "distinct" => distinct(&args, idx),
42 "has-same-node" => has_same_node(&args),
43 "leading" => leading(&args),
44 "trailing" => trailing(&args),
45 _ => return None,
46 };
47 Some(r)
48}
49
50fn two_nodesets<'a>(args: &'a [Value], name: &str) -> Result<(&'a [NodeId], &'a [NodeId])> {
53 if args.len() != 2 {
54 return Err(err(format!("set:{name} requires 2 nodeset arguments")));
55 }
56 let a = match &args[0] {
57 Value::NodeSet(ns) => ns.as_slice(),
58 _ => return Err(err(format!("set:{name} first arg must be a nodeset"))),
59 };
60 let b = match &args[1] {
61 Value::NodeSet(ns) => ns.as_slice(),
62 _ => return Err(err(format!("set:{name} second arg must be a nodeset"))),
63 };
64 Ok((a, b))
65}
66
67fn difference(args: &[Value]) -> Result<Value> {
70 let (a, b) = two_nodesets(args, "difference")?;
71 let b_set: HashSet<NodeId> = b.iter().copied().collect();
72 let out: Vec<NodeId> = a.iter().copied().filter(|id| !b_set.contains(id)).collect();
73 Ok(Value::NodeSet(out))
74}
75
76fn intersection(args: &[Value]) -> Result<Value> {
79 let (a, b) = two_nodesets(args, "intersection")?;
80 let b_set: HashSet<NodeId> = b.iter().copied().collect();
81 let out: Vec<NodeId> = a.iter().copied().filter(|id| b_set.contains(id)).collect();
82 Ok(Value::NodeSet(out))
83}
84
85fn has_same_node(args: &[Value]) -> Result<Value> {
88 let (a, b) = two_nodesets(args, "has-same-node")?;
89 let b_set: HashSet<NodeId> = b.iter().copied().collect();
90 Ok(Value::Boolean(a.iter().any(|id| b_set.contains(id))))
91}
92
93fn distinct<I: DocIndexLike>(args: &[Value], idx: &I) -> Result<Value> {
99 if args.len() != 1 {
100 return Err(err("set:distinct requires 1 nodeset argument"));
101 }
102 let ns = match &args[0] {
103 Value::NodeSet(ns) => ns,
104 _ => return Err(err("set:distinct requires a nodeset argument")),
105 };
106 let mut seen: HashSet<String> = HashSet::new();
107 let mut out: Vec<NodeId> = Vec::new();
108 for &id in ns {
109 let s = idx.string_value(id);
110 if seen.insert(s) {
111 out.push(id);
112 }
113 }
114 Ok(Value::NodeSet(out))
115}
116
117fn leading(args: &[Value]) -> Result<Value> {
125 let (a, b) = two_nodesets(args, "leading")?;
126 let b_set: HashSet<NodeId> = b.iter().copied().collect();
127 let mut out: Vec<NodeId> = Vec::new();
128 for &id in a {
129 if b_set.contains(&id) { break; }
130 out.push(id);
131 }
132 Ok(Value::NodeSet(out))
133}
134
135fn trailing(args: &[Value]) -> Result<Value> {
138 let (a, b) = two_nodesets(args, "trailing")?;
139 let b_set: HashSet<NodeId> = b.iter().copied().collect();
140 let mut cut = None;
146 for (i, &id) in a.iter().enumerate() {
147 if b_set.contains(&id) { cut = Some(i); }
148 }
149 let out: Vec<NodeId> = match cut {
150 Some(i) => a[i + 1..].to_vec(),
151 None => Vec::new(),
152 };
153 Ok(Value::NodeSet(out))
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::xpath::XPathContext;
160 use crate::{parse_str, ParseOptions};
161
162 fn ns(v: &Value) -> &[NodeId] {
163 if let Value::NodeSet(ns) = v { ns } else { panic!("expected nodeset, got {v:?}") }
164 }
165 fn b(v: &Value) -> bool {
166 if let Value::Boolean(b) = v { *b } else { panic!("expected boolean, got {v:?}") }
167 }
168
169 #[test]
170 fn difference_removes_overlap() {
171 let doc = parse_str("<r><a/><b/><c/><d/></r>", &ParseOptions::default()).unwrap();
172 let ctx = XPathContext::new(&doc);
173 let all = ctx.eval("/r/*").unwrap(); let bd = ctx.eval("/r/b | /r/d").unwrap(); let r = dispatch("difference", vec![all, bd], &ctx.index).unwrap().unwrap();
176 assert_eq!(ns(&r).len(), 2);
178 }
179
180 #[test]
181 fn intersection_keeps_overlap() {
182 let doc = parse_str("<r><a/><b/><c/><d/></r>", &ParseOptions::default()).unwrap();
183 let ctx = XPathContext::new(&doc);
184 let ac = ctx.eval("/r/a | /r/c").unwrap();
185 let bc = ctx.eval("/r/b | /r/c").unwrap();
186 let r = dispatch("intersection", vec![ac, bc], &ctx.index).unwrap().unwrap();
187 assert_eq!(ns(&r).len(), 1);
189 }
190
191 #[test]
192 fn has_same_node_detects_overlap() {
193 let doc = parse_str("<r><a/><b/><c/></r>", &ParseOptions::default()).unwrap();
194 let ctx = XPathContext::new(&doc);
195 let ab = ctx.eval("/r/a | /r/b").unwrap();
196 let bc = ctx.eval("/r/b | /r/c").unwrap();
197 assert!(b(&dispatch("has-same-node", vec![ab, bc], &ctx.index).unwrap().unwrap()));
198 let a = ctx.eval("/r/a").unwrap();
199 let c = ctx.eval("/r/c").unwrap();
200 assert!(!b(&dispatch("has-same-node", vec![a, c], &ctx.index).unwrap().unwrap()));
201 }
202
203 #[test]
204 fn distinct_dedups_by_string_value_not_identity() {
205 let doc = parse_str(
208 "<r><i>x</i><i>y</i><i>x</i><i>z</i></r>",
209 &ParseOptions::default(),
210 ).unwrap();
211 let ctx = XPathContext::new(&doc);
212 let all = ctx.eval("/r/i").unwrap();
213 let r = dispatch("distinct", vec![all], &ctx.index).unwrap().unwrap();
214 assert_eq!(ns(&r).len(), 3);
216 }
217
218 #[test]
219 fn leading_returns_prefix_before_first_overlap() {
220 let doc = parse_str("<r><a/><b/><c/><d/></r>", &ParseOptions::default()).unwrap();
221 let ctx = XPathContext::new(&doc);
222 let abcd = ctx.eval("/r/*").unwrap();
223 let c = ctx.eval("/r/c").unwrap();
224 let r = dispatch("leading", vec![abcd, c], &ctx.index).unwrap().unwrap();
225 assert_eq!(ns(&r).len(), 2);
227 }
228
229 #[test]
230 fn trailing_returns_suffix_after_last_overlap() {
231 let doc = parse_str("<r><a/><b/><c/><d/></r>", &ParseOptions::default()).unwrap();
232 let ctx = XPathContext::new(&doc);
233 let abcd = ctx.eval("/r/*").unwrap();
234 let b_ = ctx.eval("/r/b").unwrap();
235 let r = dispatch("trailing", vec![abcd, b_], &ctx.index).unwrap().unwrap();
236 assert_eq!(ns(&r).len(), 2);
238 }
239
240 #[test]
241 fn empty_inputs_are_handled() {
242 let doc = parse_str("<r><a/></r>", &ParseOptions::default()).unwrap();
243 let ctx = XPathContext::new(&doc);
244 let empty = Value::NodeSet(Vec::new());
245 let any = ctx.eval("/r/a").unwrap();
246 assert_eq!(ns(&dispatch("difference",
248 vec![empty.clone(), any.clone()], &ctx.index).unwrap().unwrap()).len(), 0);
249 assert_eq!(ns(&dispatch("difference",
251 vec![any.clone(), empty.clone()], &ctx.index).unwrap().unwrap()).len(), 1);
252 assert_eq!(ns(&dispatch("intersection",
254 vec![empty, any], &ctx.index).unwrap().unwrap()).len(), 0);
255 }
256
257 #[test]
258 fn unknown_function_returns_none() {
259 let doc = parse_str("<r/>", &ParseOptions::default()).unwrap();
260 let ctx = XPathContext::new(&doc);
261 assert!(dispatch("nonsense", vec![], &ctx.index).is_none());
262 }
263}