1mod export;
41pub use export::export;
42
43use std::fmt::Write as _;
44
45#[derive(Debug, thiserror::Error)]
47pub enum XPathError {
48 #[error("XPath syntax error at offset {0}: {1}")]
49 Syntax(usize, String),
50 #[error("unsupported XPath construct: {0}")]
51 Unsupported(String),
52}
53
54#[derive(Debug)]
57pub struct Translation {
58 pub query: String,
59 pub notes: Vec<String>,
60}
61
62pub fn translate(xpath: &str) -> Result<Translation, XPathError> {
64 let tokens = lex(xpath)?;
65 let mut parser = Parser {
66 tokens,
67 pos: 0,
68 notes: Vec::new(),
69 };
70 let query = parser.top()?;
71 if parser.pos < parser.tokens.len() {
72 return Err(XPathError::Syntax(
73 parser.tokens[parser.pos].1,
74 format!("unexpected trailing '{}'", parser.tokens[parser.pos].0),
75 ));
76 }
77 let mut notes = parser.notes;
78 notes.dedup();
79 Ok(Translation { query, notes })
80}
81
82#[derive(Debug, Clone, PartialEq)]
85enum Tok {
86 Slash,
87 SlashSlash,
88 Union,
89 LBracket,
90 RBracket,
91 LParen,
92 RParen,
93 At,
94 Comma,
95 Dot,
96 DotDot,
97 Star,
98 Minus,
99 Cmp(&'static str),
100 Number(String),
101 Literal(String),
102 Name(String, bool),
106 Axis(String),
107}
108
109impl std::fmt::Display for Tok {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Tok::Slash => write!(f, "/"),
113 Tok::SlashSlash => write!(f, "//"),
114 Tok::Union => write!(f, "|"),
115 Tok::LBracket => write!(f, "["),
116 Tok::RBracket => write!(f, "]"),
117 Tok::LParen => write!(f, "("),
118 Tok::RParen => write!(f, ")"),
119 Tok::At => write!(f, "@"),
120 Tok::Comma => write!(f, ","),
121 Tok::Dot => write!(f, "."),
122 Tok::DotDot => write!(f, ".."),
123 Tok::Star => write!(f, "*"),
124 Tok::Minus => write!(f, "-"),
125 Tok::Cmp(op) => write!(f, "{op}"),
126 Tok::Number(n) => write!(f, "{n}"),
127 Tok::Literal(s) => write!(f, "'{s}'"),
128 Tok::Name(n, _) => write!(f, "{n}"),
129 Tok::Axis(a) => write!(f, "{a}::"),
130 }
131 }
132}
133
134fn is_name_start(c: char) -> bool {
135 c.is_alphabetic() || c == '_'
136}
137
138fn is_name_char(c: char) -> bool {
139 c.is_alphanumeric() || matches!(c, '.' | '-' | '_')
140}
141
142fn lex(input: &str) -> Result<Vec<(Tok, usize)>, XPathError> {
143 let chars: Vec<char> = input.chars().collect();
144 let mut tokens = Vec::new();
145 let mut i = 0;
146 while i < chars.len() {
147 let c = chars[i];
148 let at = i;
149 match c {
150 c if c.is_whitespace() => i += 1,
151 '/' => {
152 if chars.get(i + 1) == Some(&'/') {
153 tokens.push((Tok::SlashSlash, at));
154 i += 2;
155 } else {
156 tokens.push((Tok::Slash, at));
157 i += 1;
158 }
159 }
160 '|' => {
161 tokens.push((Tok::Union, at));
162 i += 1;
163 }
164 '[' => {
165 tokens.push((Tok::LBracket, at));
166 i += 1;
167 }
168 ']' => {
169 tokens.push((Tok::RBracket, at));
170 i += 1;
171 }
172 '(' => {
173 tokens.push((Tok::LParen, at));
174 i += 1;
175 }
176 ')' => {
177 tokens.push((Tok::RParen, at));
178 i += 1;
179 }
180 '@' => {
181 tokens.push((Tok::At, at));
182 i += 1;
183 }
184 ',' => {
185 tokens.push((Tok::Comma, at));
186 i += 1;
187 }
188 '*' => {
189 tokens.push((Tok::Star, at));
190 i += 1;
191 }
192 '-' => {
193 tokens.push((Tok::Minus, at));
194 i += 1;
195 }
196 '.' => {
197 if chars.get(i + 1) == Some(&'.') {
198 tokens.push((Tok::DotDot, at));
199 i += 2;
200 } else if chars.get(i + 1).is_some_and(|c| c.is_ascii_digit()) {
201 let mut text = String::from("0.");
203 i += 1;
204 while chars.get(i).is_some_and(char::is_ascii_digit) {
205 text.push(chars[i]);
206 i += 1;
207 }
208 tokens.push((Tok::Number(text), at));
209 } else {
210 tokens.push((Tok::Dot, at));
211 i += 1;
212 }
213 }
214 '=' => {
215 tokens.push((Tok::Cmp("="), at));
216 i += 1;
217 }
218 '!' => {
219 if chars.get(i + 1) == Some(&'=') {
220 tokens.push((Tok::Cmp("!="), at));
221 i += 2;
222 } else {
223 return Err(XPathError::Syntax(at, "lone '!'".into()));
224 }
225 }
226 '<' => {
227 if chars.get(i + 1) == Some(&'=') {
228 tokens.push((Tok::Cmp("<="), at));
229 i += 2;
230 } else {
231 tokens.push((Tok::Cmp("<"), at));
232 i += 1;
233 }
234 }
235 '>' => {
236 if chars.get(i + 1) == Some(&'=') {
237 tokens.push((Tok::Cmp(">="), at));
238 i += 2;
239 } else {
240 tokens.push((Tok::Cmp(">"), at));
241 i += 1;
242 }
243 }
244 '\'' | '"' => {
245 let mut text = String::new();
246 i += 1;
247 while i < chars.len() && chars[i] != c {
248 text.push(chars[i]);
249 i += 1;
250 }
251 if i == chars.len() {
252 return Err(XPathError::Syntax(at, "unterminated string literal".into()));
253 }
254 i += 1;
255 tokens.push((Tok::Literal(text), at));
256 }
257 c if c.is_ascii_digit() => {
258 let mut text = String::new();
259 while chars.get(i).is_some_and(char::is_ascii_digit) {
260 text.push(chars[i]);
261 i += 1;
262 }
263 if chars.get(i) == Some(&'.') {
264 text.push('.');
265 i += 1;
266 while chars.get(i).is_some_and(char::is_ascii_digit) {
267 text.push(chars[i]);
268 i += 1;
269 }
270 }
271 tokens.push((Tok::Number(text), at));
272 }
273 c if is_name_start(c) => {
274 let mut text = String::new();
275 while chars.get(i).is_some_and(|&c| is_name_char(c)) {
276 text.push(chars[i]);
277 i += 1;
278 }
279 if chars.get(i) == Some(&':')
281 && chars.get(i + 1) != Some(&':')
282 && chars.get(i + 1).is_some_and(|&c| is_name_start(c))
283 {
284 text.push(':');
285 i += 1;
286 while chars.get(i).is_some_and(|&c| is_name_char(c)) {
287 text.push(chars[i]);
288 i += 1;
289 }
290 }
291 if chars.get(i) == Some(&':') && chars.get(i + 1) == Some(&':') {
292 tokens.push((Tok::Axis(text), at));
293 i += 2;
294 } else {
295 let calls = chars.get(i) == Some(&'(');
296 tokens.push((Tok::Name(text, calls), at));
297 }
298 }
299 other => {
300 return Err(XPathError::Syntax(at, format!("unexpected '{other}'")));
301 }
302 }
303 }
304 Ok(tokens)
305}
306
307const ABBREV_INDEX_NOTE: &str = "[n] on //: XPath's abbreviated // positions within each \
312 parent; Quarb positions within the hop's whole result list \
313 (equal when all matches share one parent)";
314
315struct Parser {
316 tokens: Vec<(Tok, usize)>,
317 pos: usize,
318 notes: Vec<String>,
319}
320
321impl Parser {
322 fn peek(&self) -> Option<&Tok> {
323 self.tokens.get(self.pos).map(|(t, _)| t)
324 }
325
326 fn peek2(&self) -> Option<&Tok> {
327 self.tokens.get(self.pos + 1).map(|(t, _)| t)
328 }
329
330 fn bump(&mut self) -> Option<Tok> {
331 let t = self.tokens.get(self.pos).map(|(t, _)| t.clone());
332 self.pos += 1;
333 t
334 }
335
336 fn expect(&mut self, tok: Tok, what: &str) -> Result<(), XPathError> {
337 if self.peek() == Some(&tok) {
338 self.pos += 1;
339 Ok(())
340 } else {
341 let at = self
342 .tokens
343 .get(self.pos)
344 .map(|(_, at)| *at)
345 .unwrap_or_default();
346 Err(XPathError::Syntax(at, format!("expected {what}")))
347 }
348 }
349
350 fn top(&mut self) -> Result<String, XPathError> {
353 if let Some(Tok::Name(name, true)) = self.peek() {
354 let func = name.clone();
355 match func.as_str() {
356 "count" => {
357 self.pos += 1;
358 self.expect(Tok::LParen, "'(' after count")?;
359 let inner = self.union()?;
360 self.expect(Tok::RParen, "')' to close count")?;
361 return Ok(format!("{inner} @| count"));
362 }
363 "sum" => {
364 self.pos += 1;
365 self.expect(Tok::LParen, "'(' after sum")?;
366 let (inner, projected) = self.path()?;
367 self.expect(Tok::RParen, "')' to close sum")?;
368 let projected = if projected {
371 inner
372 } else {
373 format!("{inner}::")
374 };
375 return Ok(format!("{projected} @| sum"));
376 }
377 _ => {}
378 }
379 }
380 self.union()
381 }
382
383 fn union(&mut self) -> Result<String, XPathError> {
384 let mut parts = vec![self.path()?.0];
385 while self.peek() == Some(&Tok::Union) {
386 self.pos += 1;
387 parts.push(self.path()?.0);
388 }
389 Ok(parts.join(" || "))
390 }
391
392 fn path(&mut self) -> Result<(String, bool), XPathError> {
395 let mut out = String::new();
396 let mut projection: Option<String> = None;
397
398 let mut sep = match self.peek() {
399 Some(Tok::Slash) => "/",
400 Some(Tok::SlashSlash) => "//",
401 _ => {
402 return Err(XPathError::Unsupported(
403 "relative paths (Quarb queries are root-anchored; start with / or //)".into(),
404 ));
405 }
406 };
407 self.pos += 1;
408
409 if self.peek().is_none() || self.peek() == Some(&Tok::Union) {
411 if sep == "/" {
412 return Ok(("^".into(), false));
413 }
414 return Err(XPathError::Syntax(0, "'//' needs a step".into()));
415 }
416
417 loop {
418 if projection.is_some() {
419 return Err(XPathError::Unsupported(
420 "steps after an attribute or text() step".into(),
421 ));
422 }
423 match self.step(sep, &mut out)? {
424 StepOut::Element => {}
425 StepOut::Projection(p) => projection = Some(p),
426 }
427 sep = match self.peek() {
428 Some(Tok::Slash) => "/",
429 Some(Tok::SlashSlash) => "//",
430 _ => break,
431 };
432 self.pos += 1;
433 }
434
435 let projected = projection.is_some();
436 if let Some(p) = projection {
437 out.push_str(&p);
438 }
439 Ok((out, projected))
440 }
441
442 fn step(&mut self, sep: &str, out: &mut String) -> Result<StepOut, XPathError> {
445 let mut abbreviated_descendant = sep == "//";
450 let (axis, sep) = match self.peek() {
451 Some(Tok::Axis(a)) => {
452 let a = a.clone();
453 self.pos += 1;
454 abbreviated_descendant = false;
455 match a.as_str() {
456 "child" => ("child", sep),
457 "descendant" | "descendant-or-self" => ("child", "//"),
458 "parent" => ("parent", sep),
459 "ancestor" | "ancestor-or-self" => ("ancestor", sep),
460 "self" => ("self", sep),
461 "attribute" => ("attribute", sep),
462 "following-sibling" => ("following-sibling", sep),
463 "preceding-sibling" => ("preceding-sibling", sep),
464 other => {
465 return Err(XPathError::Unsupported(format!(
466 "the {other}:: axis (no Quarb equivalent)"
467 )));
468 }
469 }
470 }
471 Some(Tok::At) => {
472 self.pos += 1;
473 ("attribute", sep)
474 }
475 _ => ("child", sep),
476 };
477
478 match axis {
479 "attribute" => {
480 let name = match self.bump() {
481 Some(Tok::Name(n, false)) => n,
482 Some(Tok::Star) => {
483 return Err(XPathError::Unsupported(
484 "@* (Quarb projects one named property at a time)".into(),
485 ));
486 }
487 _ => return Err(XPathError::Syntax(0, "expected an attribute name".into())),
488 };
489 return Ok(StepOut::Projection(format!("::{}", quarb_name(&name))));
490 }
491 "self" | "parent" | "ancestor" | "following-sibling" | "preceding-sibling" => {
492 let hop = match axis {
493 "parent" => "\\",
494 "ancestor" => "\\\\",
495 "following-sibling" => ">>",
496 "preceding-sibling" => "<<",
497 _ => "",
498 };
499 match self.bump() {
500 Some(Tok::Name(n, false)) if axis == "self" => {
502 write!(out, "[:::name = {}]", quarb_literal(&n)?).unwrap();
503 }
504 Some(Tok::Star) if axis == "self" => {}
505 Some(Tok::Name(n, false)) => {
506 write!(out, "{hop}{}", quarb_name(&n)).unwrap();
507 }
508 Some(Tok::Star) => {
509 write!(out, "{hop}*").unwrap();
510 }
511 Some(Tok::Name(n, true)) if n == "node" => {
512 self.expect(Tok::LParen, "'('")?;
513 self.expect(Tok::RParen, "')'")?;
514 if axis != "self" {
515 write!(out, "{hop}*").unwrap();
516 }
517 }
518 _ => {
519 return Err(XPathError::Syntax(0, format!("bad {axis}:: node test")));
520 }
521 }
522 return Ok(StepOut::Element);
523 }
524 _ => {}
525 }
526
527 match self.bump() {
529 Some(Tok::Name(n, false)) => {
530 write!(out, "{sep}{}", quarb_name(&n)).unwrap();
531 }
532 Some(Tok::Star) => {
533 write!(out, "{sep}*").unwrap();
534 }
535 Some(Tok::Dot) => return Ok(StepOut::Element),
536 Some(Tok::DotDot) => {
537 write!(out, "\\*").unwrap();
538 return Ok(StepOut::Element);
539 }
540 Some(Tok::Name(n, true)) if n == "text" => {
541 self.expect(Tok::LParen, "'('")?;
542 self.expect(Tok::RParen, "')'")?;
543 self.notes.push(
544 "text(): Quarb ::text is the concatenated descendant text; \
545 XPath text() selects immediate text nodes (equal on leaf elements)"
546 .into(),
547 );
548 return Ok(StepOut::Projection("::text".into()));
549 }
550 Some(Tok::Name(n, true)) if n == "node" => {
551 return Err(XPathError::Unsupported(
552 "node() (Quarb navigates elements only)".into(),
553 ));
554 }
555 Some(Tok::Name(n, true)) => {
556 return Err(XPathError::Unsupported(format!("the {n}() node test")));
557 }
558 other => {
559 return Err(XPathError::Syntax(
560 0,
561 format!(
562 "expected a step, found {}",
563 other.map(|t| t.to_string()).unwrap_or_else(|| "end".into())
564 ),
565 ));
566 }
567 }
568
569 while self.peek() == Some(&Tok::LBracket) {
571 self.pos += 1;
572 self.predicate(abbreviated_descendant, out)?;
573 self.expect(Tok::RBracket, "']' to close the predicate")?;
574 }
575 Ok(StepOut::Element)
576 }
577
578 fn predicate(
583 &mut self,
584 abbreviated_descendant: bool,
585 out: &mut String,
586 ) -> Result<(), XPathError> {
587 match (self.peek(), self.peek2()) {
589 (Some(Tok::Number(n)), Some(Tok::RBracket)) => {
590 let n = n.clone();
591 if n.contains('.') {
592 return Err(XPathError::Unsupported(format!(
593 "the non-integer position [{n}]"
594 )));
595 }
596 if abbreviated_descendant {
597 self.notes.push(ABBREV_INDEX_NOTE.into());
598 }
599 self.pos += 1;
600 write!(out, "[{n}]").unwrap();
601 return Ok(());
602 }
603 (Some(Tok::Name(f, true)), _) if f == "last" => {
605 self.pos += 1;
606 self.expect(Tok::LParen, "'('")?;
607 self.expect(Tok::RParen, "')'")?;
608 let back: i64 = if self.peek() == Some(&Tok::Minus) {
609 self.pos += 1;
610 match self.bump() {
611 Some(Tok::Number(k)) => k.parse().map_err(|_| {
612 XPathError::Unsupported(format!("last() - {k} (non-integer)"))
613 })?,
614 _ => {
615 return Err(XPathError::Syntax(
616 0,
617 "expected a number after last() -".into(),
618 ));
619 }
620 }
621 } else {
622 0
623 };
624 if abbreviated_descendant {
625 self.notes.push(ABBREV_INDEX_NOTE.into());
626 }
627 write!(out, "[-{}]", back + 1).unwrap();
628 return Ok(());
629 }
630 (Some(Tok::Name(f, true)), _) if f == "position" => {
632 self.pos += 1;
633 self.expect(Tok::LParen, "'('")?;
634 self.expect(Tok::RParen, "')'")?;
635 let op = match self.bump() {
636 Some(Tok::Cmp(op)) => op,
637 _ => {
638 return Err(XPathError::Syntax(
639 0,
640 "expected a comparison after position()".into(),
641 ));
642 }
643 };
644 let n: u64 = match self.bump() {
645 Some(Tok::Number(n)) => n.parse().map_err(|_| {
646 XPathError::Unsupported(format!("position() {op} {n} (non-integer)"))
647 })?,
648 _ => {
649 return Err(XPathError::Syntax(
650 0,
651 "expected a number after position() comparison".into(),
652 ));
653 }
654 };
655 if abbreviated_descendant {
656 self.notes.push(ABBREV_INDEX_NOTE.into());
657 }
658 match op {
659 "=" => write!(out, "[{n}]").unwrap(),
660 ">" => write!(out, "[{}..]", n + 1).unwrap(),
661 ">=" => write!(out, "[{n}..]").unwrap(),
662 "<" => write!(out, "[..{}]", n.saturating_sub(1)).unwrap(),
663 "<=" => write!(out, "[..{n}]").unwrap(),
664 other => {
665 return Err(XPathError::Unsupported(format!("position() {other} {n}")));
666 }
667 }
668 return Ok(());
669 }
670 _ => {}
671 }
672
673 let expr = self.pred_or()?;
674 write!(out, "[{expr}]").unwrap();
675 Ok(())
676 }
677
678 fn pred_or(&mut self) -> Result<String, XPathError> {
679 let mut left = self.pred_and()?;
680 while matches!(self.peek(), Some(Tok::Name(n, false)) if n == "or") {
681 self.pos += 1;
682 let right = self.pred_and()?;
683 left = format!("{left} or {right}");
684 }
685 Ok(left)
686 }
687
688 fn pred_and(&mut self) -> Result<String, XPathError> {
689 let mut left = self.pred_cmp()?;
690 while matches!(self.peek(), Some(Tok::Name(n, false)) if n == "and") {
691 self.pos += 1;
692 let right = self.pred_cmp()?;
693 left = format!("{left} and {right}");
694 }
695 Ok(left)
696 }
697
698 fn pred_cmp(&mut self) -> Result<String, XPathError> {
699 let (left, left_bare) = self.pred_primary()?;
700 if let Some(Tok::Cmp(op)) = self.peek() {
701 let op = *op;
702 self.pos += 1;
703 let (right, right_bare) = self.pred_primary()?;
704 let left = if left_bare { format!("{left}::") } else { left };
706 let right = if right_bare {
707 format!("{right}::")
708 } else {
709 right
710 };
711 return Ok(format!("{left} {op} {right}"));
712 }
713 Ok(left)
715 }
716
717 fn pred_primary(&mut self) -> Result<(String, bool), XPathError> {
721 match self.peek() {
722 Some(Tok::LParen) => {
723 self.pos += 1;
724 let inner = self.pred_or()?;
725 self.expect(Tok::RParen, "')' to close the group")?;
726 Ok((format!("({inner})"), false))
727 }
728 Some(Tok::Name(f, true)) if f == "not" => {
729 self.pos += 1;
730 self.expect(Tok::LParen, "'(' after not")?;
731 let inner = self.pred_or()?;
732 self.expect(Tok::RParen, "')' to close not")?;
733 Ok((format!("not ({inner})"), false))
734 }
735 Some(Tok::Name(f, true)) if f == "contains" => {
736 self.pos += 1;
737 self.expect(Tok::LParen, "'(' after contains")?;
738 let (hay, hay_bare) = self.pred_primary()?;
739 let hay = if hay_bare { format!("{hay}::") } else { hay };
740 self.expect(Tok::Comma, "',' between contains arguments")?;
741 let (needle, needle_bare) = self.pred_primary()?;
742 let needle = if needle_bare {
743 format!("{needle}::")
744 } else {
745 needle
746 };
747 self.expect(Tok::RParen, "')' to close contains")?;
748 Ok((format!("{hay} *= {needle}"), false))
749 }
750 Some(Tok::Name(f, true)) if f == "starts-with" => {
751 self.pos += 1;
752 self.expect(Tok::LParen, "'(' after starts-with")?;
753 let (subject, bare) = self.pred_primary()?;
754 let subject = if bare {
755 format!("{subject}::")
756 } else {
757 subject
758 };
759 self.expect(Tok::Comma, "',' between starts-with arguments")?;
760 let Some(Tok::Literal(prefix)) = self.bump() else {
761 return Err(XPathError::Unsupported(
762 "starts-with with a non-literal prefix".into(),
763 ));
764 };
765 self.expect(Tok::RParen, "')' to close starts-with")?;
766 if prefix.contains(['(', ')']) {
767 return Err(XPathError::Unsupported(
768 "starts-with prefix containing parentheses".into(),
769 ));
770 }
771 self.expect_nothing_weird(&prefix)?;
772 Ok((format!("{subject} =~ ~(^{})", regex_escape(&prefix)), false))
773 }
774 Some(Tok::Name(f, true)) if f == "text" => self.pred_path(),
775 Some(Tok::Name(f, true)) => Err(XPathError::Unsupported(format!(
776 "the {f}() function in a predicate"
777 ))),
778 Some(Tok::Literal(s)) => {
779 let s = s.clone();
780 self.pos += 1;
781 Ok((quarb_literal(&s)?, false))
782 }
783 Some(Tok::Number(n)) => {
784 let n = n.clone();
785 self.pos += 1;
786 Ok((n, false))
787 }
788 _ => self.pred_path(),
789 }
790 }
791
792 fn pred_path(&mut self) -> Result<(String, bool), XPathError> {
796 let mut out = String::new();
797 let mut sep = "/";
798 if self.peek() == Some(&Tok::Dot) {
800 self.pos += 1;
801 match self.peek() {
802 Some(Tok::Slash) => {
803 self.pos += 1;
804 }
805 Some(Tok::SlashSlash) => {
806 sep = "//";
807 self.pos += 1;
808 }
809 _ => return Ok(("::".into(), false)),
811 }
812 } else if self.peek() == Some(&Tok::SlashSlash) || self.peek() == Some(&Tok::Slash) {
813 return Err(XPathError::Unsupported(
814 "absolute paths inside predicates (Quarb predicate paths are \
815 relative to the candidate node)"
816 .into(),
817 ));
818 }
819
820 loop {
821 match self.peek() {
822 Some(Tok::At) => {
823 self.pos += 1;
824 let Some(Tok::Name(name, false)) = self.bump() else {
825 return Err(XPathError::Syntax(0, "expected an attribute name".into()));
826 };
827 write!(out, "::{}", quarb_name(&name)).unwrap();
828 return Ok((out, false));
829 }
830 Some(Tok::Name(f, true)) if f == "text" => {
831 self.pos += 1;
832 self.expect(Tok::LParen, "'('")?;
833 self.expect(Tok::RParen, "')'")?;
834 out.push_str("::text");
835 return Ok((out, false));
836 }
837 Some(Tok::Name(n, false)) => {
838 let n = n.clone();
839 self.pos += 1;
840 write!(out, "{sep}{}", quarb_name(&n)).unwrap();
841 }
842 Some(Tok::Star) => {
843 self.pos += 1;
844 write!(out, "{sep}*").unwrap();
845 }
846 Some(Tok::DotDot) | Some(Tok::Axis(_)) => {
847 return Err(XPathError::Unsupported(
848 "upward navigation inside predicates (Quarb predicate \
849 paths descend from the candidate node)"
850 .into(),
851 ));
852 }
853 other => {
854 return Err(XPathError::Syntax(
855 0,
856 format!(
857 "expected a predicate operand, found {}",
858 other.map(|t| t.to_string()).unwrap_or_else(|| "end".into())
859 ),
860 ));
861 }
862 }
863 sep = match self.peek() {
864 Some(Tok::Slash) => "/",
865 Some(Tok::SlashSlash) => "//",
866 _ => break,
867 };
868 self.pos += 1;
869 }
870 Ok((out, true))
871 }
872
873 fn expect_nothing_weird(&self, literal: &str) -> Result<(), XPathError> {
874 if literal.contains(['\'', '"']) {
875 return Err(XPathError::Unsupported(
876 "a literal containing quote characters".into(),
877 ));
878 }
879 Ok(())
880 }
881}
882
883enum StepOut {
884 Element,
885 Projection(String),
886}
887
888fn quarb_name(name: &str) -> String {
892 let bare = name
893 .chars()
894 .all(|c| c.is_alphanumeric() || matches!(c, '.' | '-' | '_'));
895 if bare {
896 name.to_string()
897 } else {
898 format!("'{name}'")
899 }
900}
901
902fn quarb_literal(s: &str) -> Result<String, XPathError> {
904 if !s.contains('"') {
905 Ok(format!("\"{s}\""))
906 } else if !s.contains('\'') {
907 Ok(format!("'{s}'"))
908 } else {
909 Err(XPathError::Unsupported(
910 "a literal containing both quote characters".into(),
911 ))
912 }
913}
914
915fn regex_escape(s: &str) -> String {
917 let mut out = String::with_capacity(s.len());
918 for c in s.chars() {
919 if "\\^$.|?*+[]{}".contains(c) {
920 out.push('\\');
921 }
922 out.push(c);
923 }
924 out
925}
926
927#[cfg(test)]
928mod tests {
929 use super::*;
930
931 fn t(xpath: &str) -> String {
932 translate(xpath).unwrap().query
933 }
934
935 fn unsupported(xpath: &str) -> String {
936 match translate(xpath) {
937 Err(XPathError::Unsupported(msg)) => msg,
938 other => panic!("expected Unsupported, got {other:?}"),
939 }
940 }
941
942 #[test]
943 fn plain_paths() {
944 assert_eq!(t("/EXAMPLE"), "/EXAMPLE");
945 assert_eq!(t("/EXAMPLE/head/title"), "/EXAMPLE/head/title");
946 assert_eq!(t("//p"), "//p");
947 assert_eq!(t("/a//b/c"), "/a//b/c");
948 assert_eq!(t("/*"), "/*");
949 assert_eq!(t("/"), "^");
950 }
951
952 #[test]
953 fn explicit_axes() {
954 assert_eq!(t("/child::EXAMPLE/child::head"), "/EXAMPLE/head");
955 assert_eq!(t("/descendant::title"), "//title");
956 assert_eq!(t("/descendant::p/ancestor::chapter"), "//p\\\\chapter");
957 assert_eq!(t("//image/parent::chapter"), "//image\\chapter");
958 assert_eq!(t("//image/.."), "//image\\*");
959 assert_eq!(t("//image/../title"), "//image\\*/title");
960 assert_eq!(t("//p/self::p"), "//p[:::name = \"p\"]");
961 }
962
963 #[test]
964 fn attributes_and_text() {
965 assert_eq!(t("//chapter/@id"), "//chapter::id");
966 assert_eq!(t("/EXAMPLE/attribute::prop1"), "/EXAMPLE::prop1");
967 assert_eq!(t("//title/text()"), "//title::text");
968 }
969
970 #[test]
971 fn predicates() {
972 assert_eq!(t("//chapter[2]"), "//chapter[2]");
973 assert_eq!(t("//*[2]"), "//*[2]");
974 assert_eq!(
975 t("//chapter[@id='chapter2']"),
976 "//chapter[::id = \"chapter2\"]"
977 );
978 assert_eq!(t("//chapter[title]"), "//chapter[/title]");
979 assert_eq!(
980 t("//chapter[title='Chapter 2']"),
981 "//chapter[/title:: = \"Chapter 2\"]"
982 );
983 assert_eq!(t("//chapter[@id][image]"), "//chapter[::id][/image]");
984 assert_eq!(
985 t("//chapter[image/@href='linus.gif']/title"),
986 "//chapter[/image::href = \"linus.gif\"]/title"
987 );
988 assert_eq!(t("//chapter[.//image]"), "//chapter[//image]");
989 assert_eq!(t("//p[.='...']"), "//p[:: = \"...\"]");
990 assert_eq!(t("//chapter[text()='x']"), "//chapter[::text = \"x\"]");
991 assert_eq!(
992 t("//chapter[@id='a' or @id='b']"),
993 "//chapter[::id = \"a\" or ::id = \"b\"]"
994 );
995 assert_eq!(
996 t("//chapter[not(image) and title]"),
997 "//chapter[not (/image) and /title]"
998 );
999 assert_eq!(
1000 t("//chapter[contains(@id, 'apt')]"),
1001 "//chapter[::id *= \"apt\"]"
1002 );
1003 assert_eq!(
1004 t("//chapter[starts-with(@id, 'chap')]"),
1005 "//chapter[::id =~ ~(^chap)]"
1006 );
1007 assert_eq!(
1008 t("//city[population > 100000]"),
1009 "//city[/population:: > 100000]"
1010 );
1011 }
1012
1013 #[test]
1014 fn positional_predicates() {
1015 assert_eq!(t("//chapter[position() = 2]"), "//chapter[2]");
1016 assert_eq!(t("//chapter[last()]"), "//chapter[-1]");
1017 assert_eq!(t("//chapter[last()]/@id"), "//chapter[-1]::id");
1018 assert_eq!(t("//chapter[last()]/title"), "//chapter[-1]/title");
1019 assert_eq!(t("//chapter[last()-1]"), "//chapter[-2]");
1020 assert_eq!(t("//chapter[position() > 2]"), "//chapter[3..]");
1021 assert_eq!(t("//chapter[position() >= 2]"), "//chapter[2..]");
1022 assert_eq!(t("//chapter[position() < 3]"), "//chapter[..2]");
1023 assert_eq!(t("//chapter[position() <= 3]"), "//chapter[..3]");
1024 assert_eq!(t("//chapter[1]/p[last()]/img"), "//chapter[1]/p[-1]/img");
1026 }
1027
1028 #[test]
1029 fn unions_and_aggregates() {
1030 assert_eq!(t("//title | //p"), "//title || //p");
1031 assert_eq!(t("count(//chapter)"), "//chapter @| count");
1032 assert_eq!(t("sum(//book/@pages)"), "//book::pages @| sum");
1033 assert_eq!(t("sum(//population)"), "//population:: @| sum");
1034 }
1035
1036 #[test]
1037 fn qualified_names_quote() {
1038 assert_eq!(t("//dc:title"), "//'dc:title'");
1039 assert_eq!(t("//dc:title/text()"), "//'dc:title'::text");
1040 }
1041
1042 #[test]
1043 fn unsupported_constructs() {
1044 assert!(unsupported("chapter/title").contains("relative"));
1045 assert!(unsupported("/following::p").contains("following"));
1046 assert!(unsupported("//node()").contains("elements only"));
1047 assert!(unsupported("//p[../title]").contains("upward"));
1048 assert!(unsupported("//chapter/@id/x").contains("after an attribute"));
1049 }
1050
1051 #[test]
1052 fn notes_flag_divergences() {
1053 let tr = translate("//p[1]").unwrap();
1055 assert_eq!(tr.query, "//p[1]");
1056 assert_eq!(tr.notes.len(), 1);
1057 assert!(tr.notes[0].contains("within each parent"));
1058 assert_eq!(translate("//*[2]").unwrap().notes.len(), 1);
1059 assert!(translate("/a/b[1]").unwrap().notes.is_empty());
1061 assert!(translate("/a/*[2]").unwrap().notes.is_empty());
1062 assert!(translate("/descendant::p[1]").unwrap().notes.is_empty());
1063 assert_eq!(translate("/descendant::p[1]").unwrap().query, "//p[1]");
1064 assert!(!translate("//title/text()").unwrap().notes.is_empty());
1065 }
1066}