Skip to main content

tiptap_rusty_parser/
query_lang.rs

1//! Query language: see [`Query`](crate::Query).
2
3use std::cmp::Ordering;
4use std::fmt;
5
6use serde_json::Value;
7
8use crate::node::Node;
9
10/// A compiled query: a small, schema-agnostic query language over [`Node`]s.
11///
12/// [`Query::parse`] compiles a query string once into an AST; [`Query::matches`]
13/// evaluates it against one node without allocating, and [`Query::find_paths`]
14/// returns the index paths of every match in document (pre-)order.
15///
16/// # Grammar
17///
18/// ```text
19/// query   := or
20/// or      := and (("||" | "or") and)*
21/// and     := unary (("&&" | "and")? unary)*      adjacent terms are ANDed
22/// unary   := ("!" | "not") unary | "(" or ")" | term
23/// term    := WORD | STRING                       bare word / quoted string
24///          | "type" OP value
25///          | "text" [OP value]
26///          | "attrs" | "attrs." PATH [OP value]
27///          | "marks" | "marks." NAME ["." PATH] [OP value]
28///          | "mark" OP value
29///          | "has" "mark" value | "has" "attr" PATH
30///          | "children" [OP NUMBER]
31///          | "depth" OP NUMBER
32/// OP      := "==" | "=" | "!=" | "<" | "<=" | ">" | ">=" | "~"
33/// value   := WORD | STRING        WORD: number, true, false, null, or text
34/// ```
35///
36/// Semantics:
37///
38/// - A bare word or quoted string matches when it is a case-insensitive
39///   substring of the node's own `text` or of its `type`.
40/// - `~` is a case-insensitive substring test; `==` is exact (case-sensitive).
41///   `<`/`>`… compare numbers numerically and strings lexicographically.
42/// - `!=` is always the negation of `==`, so a missing attr is `!=` anything.
43/// - A field without an operator tests existence: `attrs.level` (attr present),
44///   `text` (has own text), `children` (has children), `marks.link` (has a
45///   `link` mark), `attrs` / `marks` (non-empty).
46/// - Attr paths descend into objects (`attrs.meta.source`) and arrays by index
47///   (`attrs.tags.0`); comparing against an array matches if any element does.
48/// - `depth` is the length of the node's index path (root = 0). It needs
49///   context, so it is only meaningful through [`Query::find_paths`] or
50///   [`Query::matches_at`]; [`Query::matches`] evaluates it at depth 0.
51/// - Keywords (`type`, `text`, `has`, `and`…) are fields, not bare words:
52///   quote them to search for the word itself (`"type"`).
53///
54/// ```
55/// use tiptap_rusty_parser::{Document, Query};
56///
57/// let doc = Document::from_json_str(r#"{"type":"doc","content":[
58///   {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Title"}]},
59///   {"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Usage"}]}
60/// ]}"#).unwrap();
61///
62/// let q = Query::parse("type == heading && attrs.level > 1").unwrap();
63/// assert_eq!(q.find_paths(doc.root()), vec![vec![1]]);
64/// ```
65///
66/// # Examples per construct
67///
68/// Bare words and quoted strings (own text or type, case-insensitive):
69///
70/// ```
71/// use tiptap_rusty_parser::{Node, Query};
72/// let n = Node::text("The diff engine");
73/// assert!(Query::parse("ENGINE").unwrap().matches(&n));
74/// assert!(Query::parse("\"diff engine\"").unwrap().matches(&n));
75/// assert!(Query::parse("tex").unwrap().matches(&n)); // type "text"
76/// assert!(!Query::parse("\"engine diff\"").unwrap().matches(&n));
77/// ```
78///
79/// `type`:
80///
81/// ```
82/// use tiptap_rusty_parser::{Node, Query};
83/// let n = Node::element("bulletList");
84/// assert!(Query::parse("type == bulletList").unwrap().matches(&n));
85/// assert!(Query::parse("type != text").unwrap().matches(&n));
86/// assert!(Query::parse("type ~ list").unwrap().matches(&n));
87/// ```
88///
89/// `attrs.KEY` comparisons, existence and dotted paths:
90///
91/// ```
92/// use tiptap_rusty_parser::{Node, Query};
93/// use serde_json::json;
94/// let n = Node::element("callout")
95///     .with_attr("priority", 2)
96///     .with_attr("collapsed", false)
97///     .with_attr("variant", "info")
98///     .with_attr("meta", json!({"source": "import", "tags": ["docs"]}));
99/// let m = |q: &str| Query::parse(q).unwrap().matches(&n);
100/// assert!(m("attrs.priority >= 2") && m("attrs.priority < 3") && !m("attrs.priority != 2"));
101/// assert!(m("attrs.collapsed == false") && m("attrs.variant ~ NF"));
102/// assert!(m("attrs.meta.source == import") && m("attrs.meta.tags == docs"));
103/// assert!(m("attrs.variant") && !m("attrs.missing"));
104/// ```
105///
106/// `text`, marks:
107///
108/// ```
109/// use tiptap_rusty_parser::{Mark, Node, Query};
110/// let n = Node::text_with_marks(
111///     "see repo",
112///     [Mark::new("bold"), Mark::new("link").attr("href", "https://github.com/x")],
113/// );
114/// let m = |q: &str| Query::parse(q).unwrap().matches(&n);
115/// assert!(m("text ~ \"REPO\"") && m("text == \"see repo\"") && m("text"));
116/// assert!(m("has mark bold") && m("mark == link") && !m("mark == italic"));
117/// assert!(m("marks.link") && m("marks.link.href ~ github"));
118/// ```
119///
120/// `children` and `depth`:
121///
122/// ```
123/// use tiptap_rusty_parser::{Document, Node, Query};
124/// let list = Node::element("bulletList")
125///     .with_children((0..3).map(|_| Node::element("listItem")));
126/// assert!(Query::parse("children > 2").unwrap().matches(&list));
127/// assert!(Query::parse("children").unwrap().matches(&list));
128///
129/// let doc = Document::new(tiptap_rusty_parser::doc([list]));
130/// let q = Query::parse("depth == 2").unwrap();
131/// assert_eq!(q.find_paths(doc.root()), vec![vec![0, 0], vec![0, 1], vec![0, 2]]);
132/// ```
133///
134/// Boolean logic: `&&`/`and`, `||`/`or`, `!`/`not`, parentheses:
135///
136/// ```
137/// use tiptap_rusty_parser::{Node, Query};
138/// let n = Node::element("heading").with_attr("level", 2);
139/// let m = |q: &str| Query::parse(q).unwrap().matches(&n);
140/// assert!(m("type == heading && attrs.level == 2"));
141/// assert!(m("type == paragraph or type == heading"));
142/// assert!(m("not (type == text || attrs.level == 1)"));
143/// assert!(m("!text and heading")); // adjacent terms AND
144/// ```
145///
146/// Errors carry a byte offset:
147///
148/// ```
149/// use tiptap_rusty_parser::Query;
150/// let e = Query::parse("type == heading &&").unwrap_err();
151/// assert_eq!(e.offset, 18);
152/// assert_eq!(Query::parse("a & b").unwrap_err().offset, 2);
153/// ```
154#[derive(Debug, Clone, PartialEq)]
155pub struct Query {
156    expr: Expr,
157}
158
159/// A parse error with the byte offset in the query string where it occurred.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct QueryError {
162    /// Byte offset into the query string.
163    pub offset: usize,
164    /// What went wrong.
165    pub kind: QueryErrorKind,
166}
167
168/// Kinds of [`QueryError`].
169#[derive(Debug, Clone, PartialEq, Eq)]
170#[non_exhaustive]
171pub enum QueryErrorKind {
172    /// The query is empty or only whitespace.
173    Empty,
174    /// The query ended where `expected` was required.
175    UnexpectedEnd {
176        /// What the parser was looking for.
177        expected: &'static str,
178    },
179    /// A token appeared where `expected` was required.
180    UnexpectedToken {
181        /// The offending token text.
182        found: String,
183        /// What the parser was looking for.
184        expected: &'static str,
185    },
186    /// A quoted string has no closing quote.
187    UnterminatedString,
188    /// A single `&` or `|`.
189    LoneOperator(char),
190    /// A word followed by an operator that is not a known field.
191    UnknownField(String),
192    /// The field takes a number (`children`, `depth`).
193    ExpectedNumber,
194    /// The operator is not valid for this field (e.g. `children ~ 2`).
195    InvalidOperator,
196    /// An empty segment in a dotted path (`attrs.`, `attrs..x`).
197    EmptyPath,
198}
199
200impl fmt::Display for QueryError {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match &self.kind {
203            QueryErrorKind::Empty => write!(f, "empty query"),
204            QueryErrorKind::UnexpectedEnd { expected } => {
205                write!(f, "expected {expected} at end of query")
206            }
207            QueryErrorKind::UnexpectedToken { found, expected } => {
208                write!(f, "expected {expected}, found `{found}`")
209            }
210            QueryErrorKind::UnterminatedString => write!(f, "unterminated string"),
211            QueryErrorKind::LoneOperator(c) => write!(f, "expected `{c}{c}`"),
212            QueryErrorKind::UnknownField(w) => {
213                write!(f, "unknown field `{w}` (try attrs.{w})")
214            }
215            QueryErrorKind::ExpectedNumber => write!(f, "expected a number"),
216            QueryErrorKind::InvalidOperator => write!(f, "operator not valid here"),
217            QueryErrorKind::EmptyPath => write!(f, "empty path segment"),
218        }?;
219        write!(f, " (at byte {})", self.offset)
220    }
221}
222
223impl std::error::Error for QueryError {}
224
225// ---- AST ------------------------------------------------------------------------
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228enum Op {
229    Eq,
230    Ne,
231    Lt,
232    Le,
233    Gt,
234    Ge,
235    Tilde,
236}
237
238impl Op {
239    #[inline]
240    fn ord(self, o: Ordering) -> bool {
241        match self {
242            Op::Eq => o == Ordering::Equal,
243            Op::Ne => o != Ordering::Equal,
244            Op::Lt => o == Ordering::Less,
245            Op::Le => o != Ordering::Greater,
246            Op::Gt => o == Ordering::Greater,
247            Op::Ge => o != Ordering::Less,
248            Op::Tilde => false,
249        }
250    }
251}
252
253#[derive(Debug, Clone, PartialEq)]
254enum LitKind {
255    Num(f64),
256    Bool(bool),
257    Null,
258    Str,
259}
260
261#[derive(Debug, Clone, PartialEq)]
262struct Lit {
263    raw: Box<str>,
264    lower: Box<str>,
265    kind: LitKind,
266}
267
268impl Lit {
269    fn new(raw: String, quoted: bool) -> Self {
270        let kind = if quoted {
271            LitKind::Str
272        } else {
273            match raw.as_str() {
274                "true" => LitKind::Bool(true),
275                "false" => LitKind::Bool(false),
276                "null" => LitKind::Null,
277                s => s.parse::<f64>().map(LitKind::Num).unwrap_or(LitKind::Str),
278            }
279        };
280        Lit {
281            lower: raw.to_lowercase().into_boxed_str(),
282            raw: raw.into_boxed_str(),
283            kind,
284        }
285    }
286}
287
288#[derive(Debug, Clone, PartialEq)]
289struct Seg {
290    key: Box<str>,
291    idx: Option<usize>,
292}
293
294type Cmp = Option<(Op, Lit)>;
295
296#[derive(Debug, Clone, PartialEq)]
297enum Pred {
298    Word(Box<str>),
299    Type(Op, Lit),
300    Text(Cmp),
301    AnyAttrs,
302    Attr(Vec<Seg>, Cmp),
303    AnyMarks,
304    Mark(Op, Lit),
305    MarkAttr(Box<str>, Vec<Seg>, Cmp),
306    Children(Option<(Op, f64)>),
307    Depth(Op, f64),
308}
309
310#[derive(Debug, Clone, PartialEq)]
311enum Expr {
312    And(Vec<Expr>),
313    Or(Vec<Expr>),
314    Not(Box<Expr>),
315    Pred(Pred),
316}
317
318// ---- matching ---------------------------------------------------------------------
319
320impl Query {
321    /// Parse `src` into a reusable query.
322    ///
323    /// ```
324    /// use tiptap_rusty_parser::{Query, QueryErrorKind};
325    /// assert!(Query::parse("type == heading").is_ok());
326    /// let err = Query::parse("attrs.level >").unwrap_err();
327    /// assert_eq!(err.offset, 13);
328    /// assert!(matches!(err.kind, QueryErrorKind::UnexpectedEnd { .. }));
329    /// ```
330    pub fn parse(src: &str) -> Result<Query, QueryError> {
331        let tokens = lex(src)?;
332        if tokens.is_empty() {
333            return Err(QueryError {
334                offset: 0,
335                kind: QueryErrorKind::Empty,
336            });
337        }
338        let mut p = Parser {
339            tokens: &tokens,
340            pos: 0,
341            end: src.len(),
342        };
343        let expr = p.or()?;
344        if let Some(t) = p.peek() {
345            return Err(p.unexpected(t, "an operator or end of query"));
346        }
347        Ok(Query { expr })
348    }
349
350    /// Does `node` match? `depth` predicates are evaluated at depth 0.
351    ///
352    /// ```
353    /// use tiptap_rusty_parser::{Node, Query};
354    /// let q = Query::parse("roundtrip").unwrap();
355    /// assert!(q.matches(&Node::text("Faithful RoundTrip")));
356    /// assert!(!q.matches(&Node::text("nope")));
357    /// ```
358    #[inline]
359    pub fn matches(&self, node: &Node) -> bool {
360        self.expr.eval(node, 0)
361    }
362
363    /// Does `node`, found at index-path length `depth`, match?
364    ///
365    /// ```
366    /// use tiptap_rusty_parser::{Node, Query};
367    /// let q = Query::parse("depth >= 2").unwrap();
368    /// assert!(q.matches_at(&Node::text("x"), 2));
369    /// assert!(!q.matches_at(&Node::text("x"), 1));
370    /// ```
371    #[inline]
372    pub fn matches_at(&self, node: &Node, depth: usize) -> bool {
373        self.expr.eval(node, depth)
374    }
375
376    /// Index paths (root = `[]`) of every node under `root` (incl. itself)
377    /// that matches, in document order.
378    ///
379    /// ```
380    /// use tiptap_rusty_parser::{Document, Query};
381    /// let doc = Document::from_json_str(r#"{"type":"doc","content":[
382    ///   {"type":"paragraph","content":[{"type":"text","text":"a"}]},
383    ///   {"type":"paragraph"}]}"#).unwrap();
384    /// let q = Query::parse("type == paragraph").unwrap();
385    /// assert_eq!(q.find_paths(doc.root()), vec![vec![0], vec![1]]);
386    /// ```
387    pub fn find_paths(&self, root: &Node) -> Vec<Vec<usize>> {
388        let mut out = Vec::new();
389        let mut path = Vec::new();
390        self.collect(root, &mut path, &mut out);
391        out
392    }
393
394    fn collect(&self, node: &Node, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
395        if self.expr.eval(node, path.len()) {
396            out.push(path.clone());
397        }
398        if let Some(children) = &node.content {
399            for (i, c) in children.iter().enumerate() {
400                path.push(i);
401                self.collect(c, path, out);
402                path.pop();
403            }
404        }
405    }
406}
407
408impl Expr {
409    fn eval(&self, n: &Node, depth: usize) -> bool {
410        match self {
411            Expr::And(xs) => xs.iter().all(|x| x.eval(n, depth)),
412            Expr::Or(xs) => xs.iter().any(|x| x.eval(n, depth)),
413            Expr::Not(x) => !x.eval(n, depth),
414            Expr::Pred(p) => p.eval(n, depth),
415        }
416    }
417}
418
419impl Pred {
420    fn eval(&self, n: &Node, depth: usize) -> bool {
421        match self {
422            Pred::Word(w) => {
423                n.node_type.as_deref().is_some_and(|t| contains_ci(t, w))
424                    || n.text.as_deref().is_some_and(|t| contains_ci(t, w))
425            }
426            Pred::Type(op, lit) => opt_str(n.node_type.as_deref(), *op, lit),
427            Pred::Text(None) => n.text.is_some(),
428            Pred::Text(Some((op, lit))) => opt_str(n.text.as_deref(), *op, lit),
429            Pred::AnyAttrs => n.attrs.as_ref().is_some_and(|a| !a.is_empty()),
430            Pred::Attr(path, cmp) => {
431                let v = n.attrs.as_ref().and_then(|a| {
432                    let (first, rest) = path.split_first()?;
433                    resolve(a.get(&*first.key)?, rest)
434                });
435                opt_value(v, cmp)
436            }
437            Pred::AnyMarks => n.marks.as_ref().is_some_and(|m| !m.is_empty()),
438            Pred::Mark(op, lit) => {
439                let marks = n.marks.as_deref().unwrap_or(&[]);
440                if *op == Op::Ne {
441                    !marks.iter().any(|m| cmp_str(&m.mark_type, Op::Eq, lit))
442                } else {
443                    marks.iter().any(|m| cmp_str(&m.mark_type, *op, lit))
444                }
445            }
446            Pred::MarkAttr(name, path, cmp) => {
447                let mut marks = n
448                    .marks
449                    .as_deref()
450                    .unwrap_or(&[])
451                    .iter()
452                    .filter(|m| *m.mark_type == **name);
453                if path.is_empty() {
454                    return marks.next().is_some();
455                }
456                let hit = marks.any(|m| {
457                    let v = m.attrs.as_ref().and_then(|a| {
458                        let (first, rest) = path.split_first()?;
459                        resolve(a.get(&*first.key)?, rest)
460                    });
461                    match cmp {
462                        Some((Op::Ne, lit)) => v.is_some_and(|v| cmp_value(v, Op::Eq, lit)),
463                        _ => opt_value(v, cmp),
464                    }
465                });
466                // `!=` = no `link` mark whose attr equals the value.
467                if matches!(cmp, Some((Op::Ne, _))) {
468                    !hit
469                } else {
470                    hit
471                }
472            }
473            Pred::Children(None) => n.content.as_ref().is_some_and(|c| !c.is_empty()),
474            Pred::Children(Some((op, x))) => cmp_num(n.child_count() as f64, *op, *x),
475            Pred::Depth(op, x) => cmp_num(depth as f64, *op, *x),
476        }
477    }
478}
479
480fn resolve<'a>(mut v: &'a Value, rest: &[Seg]) -> Option<&'a Value> {
481    for seg in rest {
482        v = match v {
483            Value::Object(m) => m.get(&*seg.key)?,
484            Value::Array(a) => a.get(seg.idx?)?,
485            _ => return None,
486        };
487    }
488    Some(v)
489}
490
491#[inline]
492fn opt_value(v: Option<&Value>, cmp: &Cmp) -> bool {
493    match (v, cmp) {
494        (None, None) => false,
495        (Some(_), None) => true,
496        (None, Some((op, _))) => *op == Op::Ne,
497        (Some(v), Some((op, lit))) => cmp_value(v, *op, lit),
498    }
499}
500
501#[inline]
502fn opt_str(s: Option<&str>, op: Op, lit: &Lit) -> bool {
503    match s {
504        Some(s) => cmp_str(s, op, lit),
505        None => op == Op::Ne,
506    }
507}
508
509#[inline]
510fn cmp_num(a: f64, op: Op, b: f64) -> bool {
511    a.partial_cmp(&b).is_some_and(|o| op.ord(o))
512}
513
514#[inline]
515fn cmp_str(s: &str, op: Op, lit: &Lit) -> bool {
516    match op {
517        Op::Tilde => contains_ci(s, &lit.lower),
518        _ => op.ord(s.cmp(&lit.raw)),
519    }
520}
521
522fn cmp_value(v: &Value, op: Op, lit: &Lit) -> bool {
523    if op == Op::Ne {
524        return !cmp_value(v, Op::Eq, lit);
525    }
526    match v {
527        Value::Array(a) => a.iter().any(|x| cmp_value(x, op, lit)),
528        Value::String(s) => cmp_str(s, op, lit),
529        Value::Number(x) => match (&lit.kind, x.as_f64()) {
530            (LitKind::Num(b), Some(a)) if op != Op::Tilde => cmp_num(a, op, *b),
531            _ => false,
532        },
533        Value::Bool(b) => op == Op::Eq && lit.kind == LitKind::Bool(*b),
534        Value::Null => op == Op::Eq && lit.kind == LitKind::Null,
535        Value::Object(_) => false,
536    }
537}
538
539/// Case-insensitive substring test; `needle` is already lowercase. No allocation.
540fn contains_ci(hay: &str, needle: &str) -> bool {
541    if needle.is_empty() {
542        return true;
543    }
544    if needle.is_ascii() {
545        let (h, n) = (hay.as_bytes(), needle.as_bytes());
546        if n.len() > h.len() {
547            return false;
548        }
549        let first = n[0];
550        return h
551            .windows(n.len())
552            .any(|w| w[0].to_ascii_lowercase() == first && w.eq_ignore_ascii_case(n));
553    }
554    hay.char_indices()
555        .any(|(i, _)| starts_with_ci(&hay[i..], needle))
556}
557
558fn starts_with_ci(hay: &str, needle: &str) -> bool {
559    let mut n = needle.chars();
560    for c in hay.chars().flat_map(char::to_lowercase) {
561        match n.next() {
562            None => return true,
563            Some(x) if x == c => {}
564            Some(_) => return false,
565        }
566    }
567    n.next().is_none()
568}
569
570// ---- lexer ------------------------------------------------------------------------
571
572#[derive(Debug, Clone, PartialEq)]
573enum Tok {
574    Word(String),
575    Str(String),
576    Op(Op),
577    And,
578    Or,
579    Not,
580    LParen,
581    RParen,
582}
583
584#[derive(Debug)]
585struct Token {
586    tok: Tok,
587    start: usize,
588}
589
590const SPECIAL: &[u8] = b"()&|!=<>~\"'";
591
592fn lex(src: &str) -> Result<Vec<Token>, QueryError> {
593    let b = src.as_bytes();
594    let mut out = Vec::new();
595    let mut i = 0;
596    while i < b.len() {
597        let c = b[i];
598        if c.is_ascii_whitespace() {
599            i += 1;
600            continue;
601        }
602        let start = i;
603        let next = b.get(i + 1).copied();
604        let (tok, len) = match c {
605            b'(' => (Tok::LParen, 1),
606            b')' => (Tok::RParen, 1),
607            b'&' | b'|' if next == Some(c) => (if c == b'&' { Tok::And } else { Tok::Or }, 2),
608            b'&' | b'|' => {
609                return Err(QueryError {
610                    offset: start,
611                    kind: QueryErrorKind::LoneOperator(c as char),
612                })
613            }
614            b'!' if next == Some(b'=') => (Tok::Op(Op::Ne), 2),
615            b'!' => (Tok::Not, 1),
616            b'=' if next == Some(b'=') => (Tok::Op(Op::Eq), 2),
617            b'=' => (Tok::Op(Op::Eq), 1),
618            b'<' if next == Some(b'=') => (Tok::Op(Op::Le), 2),
619            b'<' => (Tok::Op(Op::Lt), 1),
620            b'>' if next == Some(b'=') => (Tok::Op(Op::Ge), 2),
621            b'>' => (Tok::Op(Op::Gt), 1),
622            b'~' => (Tok::Op(Op::Tilde), 1),
623            b'"' | b'\'' => {
624                let mut s = String::new();
625                let mut j = i + 1;
626                let mut seg = j;
627                loop {
628                    match b.get(j) {
629                        None => {
630                            return Err(QueryError {
631                                offset: start,
632                                kind: QueryErrorKind::UnterminatedString,
633                            })
634                        }
635                        Some(&q) if q == c => break,
636                        Some(b'\\') if j + 1 < b.len() => {
637                            s.push_str(&src[seg..j]);
638                            // Keep the escaped char (may be multi-byte).
639                            let ch = src[j + 1..].chars().next().unwrap_or('\\');
640                            s.push(ch);
641                            j += 1 + ch.len_utf8();
642                            seg = j;
643                        }
644                        Some(_) => j += 1,
645                    }
646                }
647                s.push_str(&src[seg..j]);
648                (Tok::Str(s), j + 1 - i)
649            }
650            _ => {
651                let mut j = i;
652                while j < b.len() && !b[j].is_ascii_whitespace() && !SPECIAL.contains(&b[j]) {
653                    j += 1;
654                }
655                let w = &src[i..j];
656                let tok = if w.eq_ignore_ascii_case("and") {
657                    Tok::And
658                } else if w.eq_ignore_ascii_case("or") {
659                    Tok::Or
660                } else if w.eq_ignore_ascii_case("not") {
661                    Tok::Not
662                } else {
663                    Tok::Word(w.to_string())
664                };
665                (tok, j - i)
666            }
667        };
668        i += len;
669        out.push(Token { tok, start });
670    }
671    Ok(out)
672}
673
674// ---- parser -----------------------------------------------------------------------
675
676struct Parser<'a> {
677    tokens: &'a [Token],
678    pos: usize,
679    end: usize,
680}
681
682impl<'a> Parser<'a> {
683    #[inline]
684    fn peek(&self) -> Option<&'a Token> {
685        self.tokens.get(self.pos)
686    }
687
688    fn bump(&mut self) -> Option<&'a Token> {
689        let t = self.tokens.get(self.pos);
690        self.pos += 1;
691        t
692    }
693
694    fn eof(&self, expected: &'static str) -> QueryError {
695        QueryError {
696            offset: self.end,
697            kind: QueryErrorKind::UnexpectedEnd { expected },
698        }
699    }
700
701    fn unexpected(&self, t: &Token, expected: &'static str) -> QueryError {
702        QueryError {
703            offset: t.start,
704            kind: QueryErrorKind::UnexpectedToken {
705                found: self.text(t),
706                expected,
707            },
708        }
709    }
710
711    fn text(&self, t: &Token) -> String {
712        match &t.tok {
713            Tok::Word(w) => w.clone(),
714            Tok::Str(s) => format!("\"{s}\""),
715            Tok::Op(op) => match op {
716                Op::Eq => "==",
717                Op::Ne => "!=",
718                Op::Lt => "<",
719                Op::Le => "<=",
720                Op::Gt => ">",
721                Op::Ge => ">=",
722                Op::Tilde => "~",
723            }
724            .into(),
725            Tok::And => "&&".into(),
726            Tok::Or => "||".into(),
727            Tok::Not => "!".into(),
728            Tok::LParen => "(".into(),
729            Tok::RParen => ")".into(),
730        }
731    }
732
733    fn or(&mut self) -> Result<Expr, QueryError> {
734        let mut xs = vec![self.and()?];
735        while matches!(self.peek(), Some(Token { tok: Tok::Or, .. })) {
736            self.pos += 1;
737            xs.push(self.and()?);
738        }
739        Ok(if xs.len() == 1 {
740            xs.pop().unwrap()
741        } else {
742            Expr::Or(xs)
743        })
744    }
745
746    fn and(&mut self) -> Result<Expr, QueryError> {
747        let mut xs = vec![self.unary()?];
748        loop {
749            match self.peek().map(|t| &t.tok) {
750                Some(Tok::And) => {
751                    self.pos += 1;
752                    xs.push(self.unary()?);
753                }
754                // Implicit AND between adjacent terms.
755                Some(Tok::Word(_) | Tok::Str(_) | Tok::Not | Tok::LParen) => xs.push(self.unary()?),
756                _ => break,
757            }
758        }
759        Ok(if xs.len() == 1 {
760            xs.pop().unwrap()
761        } else {
762            Expr::And(xs)
763        })
764    }
765
766    fn unary(&mut self) -> Result<Expr, QueryError> {
767        let Some(t) = self.bump() else {
768            return Err(self.eof("a term"));
769        };
770        match &t.tok {
771            Tok::Not => Ok(Expr::Not(Box::new(self.unary()?))),
772            Tok::LParen => {
773                let e = self.or()?;
774                match self.bump() {
775                    Some(Token {
776                        tok: Tok::RParen, ..
777                    }) => Ok(e),
778                    Some(t) => Err(self.unexpected(t, "`)`")),
779                    None => Err(self.eof("`)`")),
780                }
781            }
782            Tok::Str(s) => Ok(Expr::Pred(Pred::Word(s.to_lowercase().into()))),
783            Tok::Word(w) => self.term(w, t).map(Expr::Pred),
784            _ => Err(self.unexpected(t, "a term")),
785        }
786    }
787
788    fn peek_op(&self) -> Option<Op> {
789        match self.peek() {
790            Some(Token {
791                tok: Tok::Op(op), ..
792            }) => Some(*op),
793            _ => None,
794        }
795    }
796
797    /// Optional `OP value`.
798    fn cmp(&mut self) -> Result<Cmp, QueryError> {
799        let Some(op) = self.peek_op() else {
800            return Ok(None);
801        };
802        self.pos += 1;
803        Ok(Some((op, self.value()?)))
804    }
805
806    /// Required `OP value`.
807    fn cmp_req(&mut self) -> Result<(Op, Lit), QueryError> {
808        match self.cmp()? {
809            Some(c) => Ok(c),
810            None => Err(match self.peek() {
811                Some(t) => self.unexpected(t, "an operator"),
812                None => self.eof("an operator"),
813            }),
814        }
815    }
816
817    fn value(&mut self) -> Result<Lit, QueryError> {
818        match self.bump() {
819            Some(Token {
820                tok: Tok::Word(w), ..
821            }) => Ok(Lit::new(w.clone(), false)),
822            Some(Token {
823                tok: Tok::Str(s), ..
824            }) => Ok(Lit::new(s.clone(), true)),
825            Some(t) => Err(self.unexpected(t, "a value")),
826            None => Err(self.eof("a value")),
827        }
828    }
829
830    fn num_cmp(&mut self, required: bool) -> Result<Option<(Op, f64)>, QueryError> {
831        let op_tok = self.peek();
832        let Some(op) = self.peek_op() else {
833            if required {
834                return Err(match op_tok {
835                    Some(t) => self.unexpected(t, "an operator"),
836                    None => self.eof("an operator"),
837                });
838            }
839            return Ok(None);
840        };
841        let op_tok = op_tok.unwrap();
842        if op == Op::Tilde {
843            return Err(QueryError {
844                offset: op_tok.start,
845                kind: QueryErrorKind::InvalidOperator,
846            });
847        }
848        self.pos += 1;
849        let at = self.peek().map_or(self.end, |t| t.start);
850        let lit = self.value()?;
851        match lit.kind {
852            LitKind::Num(x) => Ok(Some((op, x))),
853            _ => Err(QueryError {
854                offset: at,
855                kind: QueryErrorKind::ExpectedNumber,
856            }),
857        }
858    }
859
860    fn path(&self, s: &str, offset: usize) -> Result<Vec<Seg>, QueryError> {
861        let mut segs = Vec::new();
862        let mut at = offset;
863        for part in s.split('.') {
864            if part.is_empty() {
865                return Err(QueryError {
866                    offset: at,
867                    kind: QueryErrorKind::EmptyPath,
868                });
869            }
870            segs.push(Seg {
871                key: part.into(),
872                idx: part.parse().ok(),
873            });
874            at += part.len() + 1;
875        }
876        Ok(segs)
877    }
878
879    fn term(&mut self, w: &str, t: &Token) -> Result<Pred, QueryError> {
880        match w {
881            "type" => {
882                let (op, lit) = self.cmp_req()?;
883                Ok(Pred::Type(op, lit))
884            }
885            "text" => Ok(Pred::Text(self.cmp()?)),
886            "mark" => {
887                let (op, lit) = self.cmp_req()?;
888                Ok(Pred::Mark(op, lit))
889            }
890            "children" => Ok(Pred::Children(self.num_cmp(false)?)),
891            "depth" => {
892                let (op, x) = self.num_cmp(true)?.expect("required");
893                Ok(Pred::Depth(op, x))
894            }
895            "attrs" if self.peek_op().is_none() => Ok(Pred::AnyAttrs),
896            "marks" if self.peek_op().is_none() => Ok(Pred::AnyMarks),
897            "has" => {
898                let what = match self.bump() {
899                    Some(Token {
900                        tok: Tok::Word(k), ..
901                    }) if k == "mark" || k == "attr" => k.as_str(),
902                    Some(t) => return Err(self.unexpected(t, "`mark` or `attr`")),
903                    None => return Err(self.eof("`mark` or `attr`")),
904                };
905                if what == "mark" {
906                    Ok(Pred::Mark(Op::Eq, self.value()?))
907                } else {
908                    match self.bump() {
909                        Some(
910                            t @ Token {
911                                tok: Tok::Word(k), ..
912                            },
913                        ) => Ok(Pred::Attr(self.path(k, t.start)?, None)),
914                        Some(t) => Err(self.unexpected(t, "an attr name")),
915                        None => Err(self.eof("an attr name")),
916                    }
917                }
918            }
919            _ => {
920                if let Some(rest) = w.strip_prefix("attrs.") {
921                    let path = self.path(rest, t.start + 6)?;
922                    return Ok(Pred::Attr(path, self.cmp()?));
923                }
924                if let Some(rest) = w.strip_prefix("marks.") {
925                    let (name, path) = match rest.split_once('.') {
926                        Some((n, p)) => (n, self.path(p, t.start + 7 + n.len())?),
927                        None => (rest, Vec::new()),
928                    };
929                    if name.is_empty() {
930                        return Err(QueryError {
931                            offset: t.start + 6,
932                            kind: QueryErrorKind::EmptyPath,
933                        });
934                    }
935                    let cmp = self.cmp()?;
936                    if path.is_empty() && cmp.is_some() {
937                        return Err(QueryError {
938                            offset: self.tokens[self.pos - 2].start,
939                            kind: QueryErrorKind::InvalidOperator,
940                        });
941                    }
942                    return Ok(Pred::MarkAttr(name.into(), path, cmp));
943                }
944                if self.peek_op().is_some() {
945                    return Err(QueryError {
946                        offset: t.start,
947                        kind: QueryErrorKind::UnknownField(w.to_string()),
948                    });
949                }
950                Ok(Pred::Word(w.to_lowercase().into()))
951            }
952        }
953    }
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959
960    #[test]
961    fn contains_ci_paths() {
962        assert!(contains_ci("Hello World", "world"));
963        assert!(!contains_ci("Hello", "world"));
964        assert!(contains_ci("ÉCOLE", "école"));
965        assert!(contains_ci("x", ""));
966        assert!(!contains_ci("ab", "abc"));
967    }
968
969    #[test]
970    fn lexes_escapes_and_offsets() {
971        let t = lex(r#"a "b\"c" =="#).unwrap();
972        assert_eq!(t[1].tok, Tok::Str("b\"c".into()));
973        assert_eq!((t[1].start, t[2].start), (2, 9));
974        assert_eq!(t[2].tok, Tok::Op(Op::Eq));
975    }
976}