Skip to main content

Query

Struct Query 

Source
pub struct Query { /* private fields */ }
Expand description

A compiled query: a small, schema-agnostic query language over Nodes.

Query::parse compiles a query string once into an AST; Query::matches evaluates it against one node without allocating, and Query::find_paths returns the index paths of every match in document (pre-)order.

§Grammar

query   := or
or      := and (("||" | "or") and)*
and     := unary (("&&" | "and")? unary)*      adjacent terms are ANDed
unary   := ("!" | "not") unary | "(" or ")" | term
term    := WORD | STRING                       bare word / quoted string
         | "type" OP value
         | "text" [OP value]
         | "attrs" | "attrs." PATH [OP value]
         | "marks" | "marks." NAME ["." PATH] [OP value]
         | "mark" OP value
         | "has" "mark" value | "has" "attr" PATH
         | "children" [OP NUMBER]
         | "depth" OP NUMBER
OP      := "==" | "=" | "!=" | "<" | "<=" | ">" | ">=" | "~"
value   := WORD | STRING        WORD: number, true, false, null, or text

Semantics:

  • A bare word or quoted string matches when it is a case-insensitive substring of the node’s own text or of its type.
  • ~ is a case-insensitive substring test; == is exact (case-sensitive). </>… compare numbers numerically and strings lexicographically.
  • != is always the negation of ==, so a missing attr is != anything.
  • A field without an operator tests existence: attrs.level (attr present), text (has own text), children (has children), marks.link (has a link mark), attrs / marks (non-empty).
  • Attr paths descend into objects (attrs.meta.source) and arrays by index (attrs.tags.0); comparing against an array matches if any element does.
  • depth is the length of the node’s index path (root = 0). It needs context, so it is only meaningful through Query::find_paths or Query::matches_at; Query::matches evaluates it at depth 0.
  • Keywords (type, text, has, and…) are fields, not bare words: quote them to search for the word itself ("type").
use tiptap_rusty_parser::{Document, Query};

let doc = Document::from_json_str(r#"{"type":"doc","content":[
  {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Title"}]},
  {"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Usage"}]}
]}"#).unwrap();

let q = Query::parse("type == heading && attrs.level > 1").unwrap();
assert_eq!(q.find_paths(doc.root()), vec![vec![1]]);

§Examples per construct

Bare words and quoted strings (own text or type, case-insensitive):

use tiptap_rusty_parser::{Node, Query};
let n = Node::text("The diff engine");
assert!(Query::parse("ENGINE").unwrap().matches(&n));
assert!(Query::parse("\"diff engine\"").unwrap().matches(&n));
assert!(Query::parse("tex").unwrap().matches(&n)); // type "text"
assert!(!Query::parse("\"engine diff\"").unwrap().matches(&n));

type:

use tiptap_rusty_parser::{Node, Query};
let n = Node::element("bulletList");
assert!(Query::parse("type == bulletList").unwrap().matches(&n));
assert!(Query::parse("type != text").unwrap().matches(&n));
assert!(Query::parse("type ~ list").unwrap().matches(&n));

attrs.KEY comparisons, existence and dotted paths:

use tiptap_rusty_parser::{Node, Query};
use serde_json::json;
let n = Node::element("callout")
    .with_attr("priority", 2)
    .with_attr("collapsed", false)
    .with_attr("variant", "info")
    .with_attr("meta", json!({"source": "import", "tags": ["docs"]}));
let m = |q: &str| Query::parse(q).unwrap().matches(&n);
assert!(m("attrs.priority >= 2") && m("attrs.priority < 3") && !m("attrs.priority != 2"));
assert!(m("attrs.collapsed == false") && m("attrs.variant ~ NF"));
assert!(m("attrs.meta.source == import") && m("attrs.meta.tags == docs"));
assert!(m("attrs.variant") && !m("attrs.missing"));

text, marks:

use tiptap_rusty_parser::{Mark, Node, Query};
let n = Node::text_with_marks(
    "see repo",
    [Mark::new("bold"), Mark::new("link").attr("href", "https://github.com/x")],
);
let m = |q: &str| Query::parse(q).unwrap().matches(&n);
assert!(m("text ~ \"REPO\"") && m("text == \"see repo\"") && m("text"));
assert!(m("has mark bold") && m("mark == link") && !m("mark == italic"));
assert!(m("marks.link") && m("marks.link.href ~ github"));

children and depth:

use tiptap_rusty_parser::{Document, Node, Query};
let list = Node::element("bulletList")
    .with_children((0..3).map(|_| Node::element("listItem")));
assert!(Query::parse("children > 2").unwrap().matches(&list));
assert!(Query::parse("children").unwrap().matches(&list));

let doc = Document::new(tiptap_rusty_parser::doc([list]));
let q = Query::parse("depth == 2").unwrap();
assert_eq!(q.find_paths(doc.root()), vec![vec![0, 0], vec![0, 1], vec![0, 2]]);

Boolean logic: &&/and, ||/or, !/not, parentheses:

use tiptap_rusty_parser::{Node, Query};
let n = Node::element("heading").with_attr("level", 2);
let m = |q: &str| Query::parse(q).unwrap().matches(&n);
assert!(m("type == heading && attrs.level == 2"));
assert!(m("type == paragraph or type == heading"));
assert!(m("not (type == text || attrs.level == 1)"));
assert!(m("!text and heading")); // adjacent terms AND

Errors carry a byte offset:

use tiptap_rusty_parser::Query;
let e = Query::parse("type == heading &&").unwrap_err();
assert_eq!(e.offset, 18);
assert_eq!(Query::parse("a & b").unwrap_err().offset, 2);

Implementations§

Source§

impl Query

Source

pub fn parse(src: &str) -> Result<Query, QueryError>

Parse src into a reusable query.

use tiptap_rusty_parser::{Query, QueryErrorKind};
assert!(Query::parse("type == heading").is_ok());
let err = Query::parse("attrs.level >").unwrap_err();
assert_eq!(err.offset, 13);
assert!(matches!(err.kind, QueryErrorKind::UnexpectedEnd { .. }));
Source

pub fn matches(&self, node: &Node) -> bool

Does node match? depth predicates are evaluated at depth 0.

use tiptap_rusty_parser::{Node, Query};
let q = Query::parse("roundtrip").unwrap();
assert!(q.matches(&Node::text("Faithful RoundTrip")));
assert!(!q.matches(&Node::text("nope")));
Source

pub fn matches_at(&self, node: &Node, depth: usize) -> bool

Does node, found at index-path length depth, match?

use tiptap_rusty_parser::{Node, Query};
let q = Query::parse("depth >= 2").unwrap();
assert!(q.matches_at(&Node::text("x"), 2));
assert!(!q.matches_at(&Node::text("x"), 1));
Source

pub fn find_paths(&self, root: &Node) -> Vec<Vec<usize>>

Index paths (root = []) of every node under root (incl. itself) that matches, in document order.

use tiptap_rusty_parser::{Document, Query};
let doc = Document::from_json_str(r#"{"type":"doc","content":[
  {"type":"paragraph","content":[{"type":"text","text":"a"}]},
  {"type":"paragraph"}]}"#).unwrap();
let q = Query::parse("type == paragraph").unwrap();
assert_eq!(q.find_paths(doc.root()), vec![vec![0], vec![1]]);

Trait Implementations§

Source§

impl Clone for Query

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Query

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for Query

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Query

Auto Trait Implementations§

§

impl Freeze for Query

§

impl RefUnwindSafe for Query

§

impl Send for Query

§

impl Sync for Query

§

impl Unpin for Query

§

impl UnsafeUnpin for Query

§

impl UnwindSafe for Query

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.