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 textSemantics:
- A bare word or quoted string matches when it is a case-insensitive
substring of the node’s own
textor of itstype. ~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 alinkmark),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. depthis the length of the node’s index path (root = 0). It needs context, so it is only meaningful throughQuery::find_pathsorQuery::matches_at;Query::matchesevaluates 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 ANDErrors 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
impl Query
Sourcepub fn parse(src: &str) -> Result<Query, QueryError>
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 { .. }));Sourcepub fn matches(&self, node: &Node) -> bool
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")));Sourcepub fn matches_at(&self, node: &Node, depth: usize) -> bool
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));Sourcepub fn find_paths(&self, root: &Node) -> Vec<Vec<usize>>
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]]);