Skip to main content

quarb_compose/
lib.rs

1//! Adapter composition: descend *through* a leaf into its content.
2//!
3//! A filesystem's `.json` file, an archive's `config.xml` entry, a
4//! message's HTML part — trees of files are full of leaves whose
5//! *content* is itself a tree in another notation. Composition
6//! grafts that inner tree onto the outer one: wrap any adapter in
7//! a [`ComposeAdapter`] and a leaf whose name or content says
8//! "parse me" gains the parsed arbor as its children:
9//!
10//! ```text
11//! /data/store.json/books/*[/price:: < 12]/title::
12//!       └ outer (fs) ┘└──── inner (json) ────┘
13//! ```
14//!
15//! The graft happens on first touch, lazily: a leaf is only read
16//! and parsed when navigation actually enters it. Detection is by
17//! extension (`.json`, `.xml`, `.html`/`.htm`/`.xhtml`/`.svg`,
18//! `.csv`/`.tsv`), else by sniffing the content's first
19//! character; a parse failure simply leaves the leaf a leaf.
20//! Archive leaves (`.zip`, `.tar`, `.tar.gz`) are binary, so they
21//! graft by *path* rather than content — available when the outer
22//! substrate has real paths (see
23//! [`ComposeAdapter::with_source_paths`]) — and the grafted
24//! archive composes in turn, so one path runs filesystem → tar →
25//! JSON without a seam. The
26//! inner root is *identified with* the outer leaf node — the leaf
27//! itself answers the inner root's children — so there is no
28//! phantom node between the file and its content.
29//!
30//! Locators show the boundary with a bang, jar-URL style:
31//! `store.json!/books/1`.
32
33use quarb::{AstAdapter, NodeId, Value};
34use std::cell::RefCell;
35use std::collections::HashMap;
36use std::path::PathBuf;
37
38/// A parsed inner arbor.
39enum Inner {
40    Json(quarb_json::JsonAdapter),
41    Xml(quarb_xml::XmlAdapter),
42    Html(quarb_html::HtmlAdapter),
43    Text(quarb_text::TextModel),
44    Csv(quarb_csv::CsvAdapter),
45    #[cfg(feature = "source")]
46    Syntax(quarb_tree_sitter::TreeSitterAdapter),
47    #[cfg(feature = "source")]
48    Code(quarb_code::CodeModel),
49    /// A grafted archive, itself composed so its own parseable
50    /// entries graft in turn.
51    Archive(Box<ComposeAdapter<quarb_archive::ArchiveAdapter>>),
52}
53
54impl Inner {
55    fn adapter(&self) -> &dyn AstAdapter {
56        match self {
57            Inner::Json(a) => a,
58            Inner::Xml(a) => a,
59            Inner::Html(a) => a,
60            Inner::Text(a) => a,
61            Inner::Csv(a) => a,
62            #[cfg(feature = "source")]
63            Inner::Syntax(a) => a,
64            #[cfg(feature = "source")]
65            Inner::Code(a) => a,
66            Inner::Archive(a) => &**a,
67        }
68    }
69
70    fn locator(&self, node: NodeId) -> String {
71        match self {
72            Inner::Json(a) => a.pointer(node),
73            Inner::Xml(a) => a.locator(node),
74            Inner::Html(a) => a.locator(node),
75            Inner::Text(a) => a.locator(node),
76            Inner::Csv(a) => a.locator(node),
77            #[cfg(feature = "source")]
78            Inner::Syntax(a) => a.locator(node),
79            #[cfg(feature = "source")]
80            Inner::Code(a) => a.locator(node),
81            Inner::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
82        }
83    }
84}
85
86/// The level source-file leaves graft at: the syntax level (the
87/// literal parse — the default) or the code level (identifiers
88/// as names). Chosen per mount by the `code:` target prefix, and
89/// inherited by nested composes (an archive inside a `code:`
90/// mount grafts its source entries at the code level too) — the
91/// filesystem-into-declarations namespace has no seam.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum SourceGraft {
94    #[default]
95    Syntax,
96    Code,
97}
98
99/// The level marked-up documents (`.html`, `.htm`, `.md`) graft
100/// at: their DOM by default, or the text level — sections,
101/// paragraphs, lists, and what the page declares about itself
102/// in its head — when the mount asks for the reader's model
103/// (`text:` on an archive or directory).
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum DocumentGraft {
106    #[default]
107    Dom,
108    Text,
109}
110
111/// Names [`quarb_archive::ArchiveAdapter::open`] can take: the
112/// zip family, tar, and gzip (`.gz` alone included — the open
113/// checks magic bytes, and a failure leaves the leaf a leaf).
114fn archive_name(name: &str) -> bool {
115    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
116    matches!(ext.as_str(), "zip" | "tar" | "tgz" | "gz")
117}
118
119/// Parse `content` by `name`'s extension, else by sniffing.
120fn parse_inner(name: &str, content: &str, graft: SourceGraft, doc: DocumentGraft) -> Option<Inner> {
121    #[cfg(not(feature = "source"))]
122    let _ = graft;
123    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
124    match ext.as_str() {
125        "json" => {
126            return quarb_json::JsonAdapter::parse(content)
127                .ok()
128                .map(Inner::Json);
129        }
130        "jsonl" | "ndjson" => {
131            return quarb_json::JsonAdapter::parse_lines(content)
132                .ok()
133                .map(Inner::Json);
134        }
135        "xml" | "svg" | "xhtml" => {
136            return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
137        }
138        "html" | "htm" => {
139            return Some(match doc {
140                DocumentGraft::Dom => Inner::Html(quarb_html::HtmlAdapter::parse(content)),
141                DocumentGraft::Text => Inner::Text(quarb_text_html::parse(content)),
142            });
143        }
144        "yaml" | "yml" => return quarb_yaml::parse(content).ok().map(Inner::Json),
145        "toml" => return quarb_toml::parse(content).ok().map(Inner::Json),
146        "md" | "markdown" => {
147            return Some(match doc {
148                DocumentGraft::Dom => Inner::Html(quarb_markdown::parse(content)),
149                DocumentGraft::Text => Inner::Text(quarb_text_markdown::parse(content)),
150            });
151        }
152        // Plain text grafts at the text level (blank-line
153        // paragraphs); html/md keep their DOM-level graft.
154        "txt" => return Some(Inner::Text(quarb_text::TextModel::parse_plain(content))),
155        "csv" => return quarb_csv::CsvAdapter::parse(content).ok().map(Inner::Csv),
156        "tsv" => {
157            return quarb_csv::CsvAdapter::parse_with_delimiter(content, b'\t')
158                .ok()
159                .map(Inner::Csv);
160        }
161        #[cfg(feature = "source")]
162        ext if quarb_tree_sitter::supported(ext) => {
163            return match graft {
164                SourceGraft::Syntax => quarb_tree_sitter::TreeSitterAdapter::parse(content, ext)
165                    .ok()
166                    .map(Inner::Syntax),
167                SourceGraft::Code => quarb_code::CodeModel::parse(content, ext)
168                    .ok()
169                    .map(Inner::Code),
170            };
171        }
172        _ => {}
173    }
174    // Content sniff for extensionless names.
175    let t = content.trim_start();
176    if t.starts_with('{') || t.starts_with('[') {
177        return quarb_json::JsonAdapter::parse(content)
178            .ok()
179            .map(Inner::Json);
180    }
181    if t.starts_with("<?xml") {
182        return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
183    }
184    None
185}
186
187/// One graft: an inner arbor mounted at an outer leaf.
188struct Graft {
189    outer: NodeId,
190    inner: Inner,
191}
192
193/// Grafted node ids carry this tag bit; the rest indexes the intern
194/// table. The bit sits at the top of the 56-bit id window that
195/// `MountAdapter` leaves an inner adapter — it reserves the high byte
196/// (bits 56–63) for the mount index — so a grafted id survives being
197/// packed into a mount and still round-trips. It must stay below bit
198/// 56 for that reason; the top bit (1 << 63) collided with the mount
199/// byte. Outer adapters use small sequential indices, far below this
200/// bit, so a set bit 55 unambiguously marks a graft.
201const GRAFT_BIT: u64 = 1 << 55;
202
203/// Any adapter, with parseable leaf content grafted as subtrees.
204pub struct ComposeAdapter<A: AstAdapter> {
205    outer: A,
206    grafts: RefCell<Vec<Graft>>,
207    /// Outer leaf → its graft index (`None`: probed, not
208    /// parseable).
209    probed: RefCell<HashMap<NodeId, Option<usize>>>,
210    /// (graft, inner id) → interned composite id, and the reverse.
211    interned: RefCell<HashMap<(usize, NodeId), NodeId>>,
212    reverse: RefCell<Vec<(usize, NodeId)>>,
213    /// Maps an outer leaf to a filesystem path, when the outer
214    /// substrate has one. Enables archive grafts, which are
215    /// binary and open by path rather than parsing from text.
216    source_path: Option<fn(&A, NodeId) -> Option<PathBuf>>,
217    /// The level source-file leaves graft at.
218    source_graft: SourceGraft,
219    /// The level marked-up documents graft at.
220    document_graft: DocumentGraft,
221    /// Maps an outer leaf to the URL its document is served
222    /// from, so a text-level graft joins relative references
223    /// against it and `-->` can land on a sibling document.
224    document_url: Option<fn(&A, NodeId) -> Option<String>>,
225    /// URL → outer leaf, built on first `document_by_ref`.
226    urls: RefCell<Option<HashMap<String, NodeId>>>,
227}
228
229impl<A: AstAdapter> ComposeAdapter<A> {
230    pub fn new(outer: A) -> Self {
231        ComposeAdapter {
232            outer,
233            grafts: RefCell::new(Vec::new()),
234            probed: RefCell::new(HashMap::new()),
235            interned: RefCell::new(HashMap::new()),
236            reverse: RefCell::new(Vec::new()),
237            source_path: None,
238            source_graft: SourceGraft::default(),
239            document_graft: DocumentGraft::default(),
240            document_url: None,
241            urls: RefCell::new(None),
242        }
243    }
244
245    /// Declare where each outer leaf's document is served from
246    /// (builder-style): a text-level graft joins its relative
247    /// references against that URL, and `-->` from any graft
248    /// lands on the leaf holding the referenced document (its
249    /// fragment's bearer when named) without a host registration.
250    pub fn with_document_urls(mut self, document_url: fn(&A, NodeId) -> Option<String>) -> Self {
251        self.document_url = Some(document_url);
252        self
253    }
254
255    /// The outer leaf serving `url` (fragment-stripped), from the
256    /// declared URLs; the whole outer tree is walked once.
257    fn leaf_by_url(&self, url: &str) -> Option<NodeId> {
258        let f = self.document_url?;
259        if self.urls.borrow().is_none() {
260            let mut map = HashMap::new();
261            let mut stack = vec![self.outer.root()];
262            while let Some(n) = stack.pop() {
263                let kids = self.outer.children(n);
264                if kids.is_empty() {
265                    if let Some(u) = f(&self.outer, n) {
266                        map.entry(u.split('#').next().unwrap_or(&u).to_string()).or_insert(n);
267                    }
268                } else {
269                    stack.extend(kids);
270                }
271            }
272            *self.urls.borrow_mut() = Some(map);
273        }
274        self.urls.borrow().as_ref()?.get(url).copied()
275    }
276
277    /// Choose the level source-file leaves graft at
278    /// (builder-style; the default is the syntax level).
279    pub fn with_source_graft(mut self, graft: SourceGraft) -> Self {
280        self.source_graft = graft;
281        self
282    }
283
284    /// Choose the level marked-up documents graft at
285    /// (builder-style; the default is the DOM).
286    pub fn with_document_graft(mut self, graft: DocumentGraft) -> Self {
287        self.document_graft = graft;
288        self
289    }
290
291    /// Like [`new`](Self::new), with a hook mapping outer leaves
292    /// to filesystem paths, so archive leaves (`.zip`,
293    /// `.tar[.gz]`) graft too. The grafted archive composes in
294    /// turn: its own parseable entries graft, and one path walks
295    /// filesystem → archive → document.
296    pub fn with_source_paths(outer: A, source_path: fn(&A, NodeId) -> Option<PathBuf>) -> Self {
297        ComposeAdapter {
298            source_path: Some(source_path),
299            ..Self::new(outer)
300        }
301    }
302
303    /// The wrapped adapter (for outer-specific calls).
304    pub fn outer(&self) -> &A {
305        &self.outer
306    }
307
308    /// A combined locator: `outer_locator(leaf)!inner-path` for
309    /// grafted nodes.
310    pub fn locator(&self, node: NodeId, outer_locator: impl Fn(NodeId) -> String) -> String {
311        match self.split(node) {
312            None => outer_locator(node),
313            Some((g, inner)) => {
314                let grafts = self.grafts.borrow();
315                let graft = &grafts[g];
316                format!(
317                    "{}!{}",
318                    outer_locator(graft.outer),
319                    graft.inner.locator(inner)
320                )
321            }
322        }
323    }
324
325    /// Decode a composite id.
326    /// The graft's fill for a node inside a grafted document: the
327    /// outer leaf's provenance (the file's path and mtime) with the
328    /// mtime re-labelled as the source rung, and the node's path
329    /// inside the document.
330    fn graft_fill(&self, outer_leaf: NodeId, inner_path: String) -> quarb::Provenance {
331        let mut fill = self.outer.provenance(outer_leaf).as_source_rung();
332        fill.path = Some(inner_path);
333        fill
334    }
335
336    fn split(&self, node: NodeId) -> Option<(usize, NodeId)> {
337        if node.0 & GRAFT_BIT == 0 {
338            return None;
339        }
340        self.reverse
341            .borrow()
342            .get((node.0 & !GRAFT_BIT) as usize)
343            .copied()
344    }
345
346    fn intern(&self, graft: usize, inner: NodeId) -> NodeId {
347        if let Some(&id) = self.interned.borrow().get(&(graft, inner)) {
348            return id;
349        }
350        let mut rev = self.reverse.borrow_mut();
351        let id = NodeId(GRAFT_BIT | rev.len() as u64);
352        rev.push((graft, inner));
353        self.interned.borrow_mut().insert((graft, inner), id);
354        id
355    }
356
357    /// The graft at an outer leaf, probing (read + parse) on first
358    /// touch. Only childless outer nodes with text content are
359    /// candidates.
360    fn graft_at(&self, node: NodeId) -> Option<usize> {
361        if let Some(&g) = self.probed.borrow().get(&node) {
362            return g;
363        }
364        let g = (|| {
365            if !self.outer.children(node).is_empty() {
366                return None;
367            }
368            let name = self.outer.name(node)?;
369            if archive_name(&name)
370                && let Some(path_of) = self.source_path
371                && let Some(path) = path_of(&self.outer, node)
372                && let Ok(a) = quarb_archive::ArchiveAdapter::open(&path)
373            {
374                let inner = Inner::Archive(Box::new(
375                    ComposeAdapter::new(a)
376                        .with_source_graft(self.source_graft)
377                        .with_document_graft(self.document_graft),
378                ));
379                let mut grafts = self.grafts.borrow_mut();
380                grafts.push(Graft { outer: node, inner });
381                return Some(grafts.len() - 1);
382            }
383            let content = match self.outer.default_value(node)? {
384                Value::Str(s) => s,
385                _ => return None,
386            };
387            let mut inner = parse_inner(&name, &content, self.source_graft, self.document_graft)?;
388            if let (Inner::Text(model), Some(f)) = (&mut inner, self.document_url)
389                && let Some(u) = f(&self.outer, node)
390            {
391                model.set_document_url(&u);
392            }
393            let mut grafts = self.grafts.borrow_mut();
394            grafts.push(Graft { outer: node, inner });
395            Some(grafts.len() - 1)
396        })();
397        self.probed.borrow_mut().insert(node, g);
398        g
399    }
400
401    /// Map an inner node up: the inner root becomes the outer
402    /// leaf.
403    fn wrap(&self, graft: usize, inner: NodeId) -> NodeId {
404        let grafts = self.grafts.borrow();
405        if inner == grafts[graft].inner.adapter().root() {
406            grafts[graft].outer
407        } else {
408            drop(grafts);
409            self.intern(graft, inner)
410        }
411    }
412}
413
414impl<A: AstAdapter> AstAdapter for ComposeAdapter<A> {
415    fn root(&self) -> NodeId {
416        self.outer.root()
417    }
418
419    fn children(&self, node: NodeId) -> Vec<NodeId> {
420        match self.split(node) {
421            Some((g, inner)) => {
422                let ids: Vec<NodeId> = {
423                    let grafts = self.grafts.borrow();
424                    grafts[g].inner.adapter().children(inner)
425                };
426                ids.into_iter().map(|c| self.wrap(g, c)).collect()
427            }
428            None => {
429                let outer = self.outer.children(node);
430                if !outer.is_empty() {
431                    return outer;
432                }
433                match self.graft_at(node) {
434                    Some(g) => {
435                        let ids: Vec<NodeId> = {
436                            let grafts = self.grafts.borrow();
437                            let a = grafts[g].inner.adapter();
438                            a.children(a.root())
439                        };
440                        ids.into_iter().map(|c| self.wrap(g, c)).collect()
441                    }
442                    None => Vec::new(),
443                }
444            }
445        }
446    }
447
448    fn name(&self, node: NodeId) -> Option<String> {
449        match self.split(node) {
450            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().name(inner),
451            None => self.outer.name(node),
452        }
453    }
454
455    fn parent(&self, node: NodeId) -> Option<NodeId> {
456        match self.split(node) {
457            Some((g, inner)) => {
458                let p = self.grafts.borrow()[g].inner.adapter().parent(inner)?;
459                Some(self.wrap(g, p))
460            }
461            None => self.outer.parent(node),
462        }
463    }
464
465    /// A written name reaches a node by the owning adapter's rule
466    /// (its aliases included).
467    fn answers_to(&self, node: NodeId, name: &str) -> bool {
468        match self.split(node) {
469            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().answers_to(inner, name),
470            None => self.outer.answers_to(node, name),
471        }
472    }
473
474    /// The graft root IS the outer leaf, so the leaf wears the
475    /// grafted document's own traits too — a page's declared tags
476    /// on the file that holds it.
477    fn traits(&self, node: NodeId) -> Vec<String> {
478        match self.split(node) {
479            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().traits(inner),
480            None => {
481                let mut out = self.outer.traits(node);
482                if let Some(g) = self.graft_at(node) {
483                    let grafts = self.grafts.borrow();
484                    let a = grafts[g].inner.adapter();
485                    for t in a.traits(a.root()) {
486                        if !out.contains(&t) {
487                            out.push(t);
488                        }
489                    }
490                }
491                out
492            }
493        }
494    }
495
496    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
497        match self.split(node) {
498            Some((g, inner)) => self.grafts.borrow()[g]
499                .inner
500                .adapter()
501                .property(inner, name),
502            // The graft root IS the outer leaf, so a property the
503            // outer can't answer falls through to the grafted
504            // document's root — `::key` dual exposure works *at*
505            // the graft, not only one level in.
506            None => self.outer.property(node, name).or_else(|| {
507                let g = self.graft_at(node)?;
508                let grafts = self.grafts.borrow();
509                let a = grafts[g].inner.adapter();
510                a.property(a.root(), name)
511            }),
512        }
513    }
514
515    fn default_value(&self, node: NodeId) -> Option<Value> {
516        match self.split(node) {
517            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().default_value(inner),
518            None => self.outer.default_value(node),
519        }
520    }
521
522    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
523        match self.split(node) {
524            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().metadata(inner, key),
525            // Like properties: what the outer cannot answer, the
526            // grafted document's root may (`::::"og:type"` on the
527            // file that holds the page).
528            None => self.outer.metadata(node, key).or_else(|| {
529                let g = self.graft_at(node)?;
530                let grafts = self.grafts.borrow();
531                let a = grafts[g].inner.adapter();
532                a.metadata(a.root(), key)
533            }),
534        }
535    }
536
537    /// A graft's own aliases are not merged: a node either belongs
538    /// to the outer document or to a graft, and the alias list is
539    /// consulted only after the owning adapter's `property` misses.
540    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
541        match self.split(node) {
542            Some((g, inner)) => self.grafts.borrow()[g]
543                .inner
544                .adapter()
545                .aliased_metadata(inner),
546            None => self.outer.aliased_metadata(node),
547        }
548    }
549
550    /// A graft is a provenance layer: the grafted document's own
551    /// components win, and the outer *leaf* it was parsed from fills
552    /// the rest (with an fs outer: the file's path and mtime). The
553    /// jar-style composite locator is deliberately not the source —
554    /// `?src` names a document, and the intra-document address is
555    /// dpid's job.
556    fn provenance(&self, node: NodeId) -> quarb::Provenance {
557        match self.split(node) {
558            Some((g, inner)) => {
559                // Clone what we need out of the borrow before calling
560                // into `self.outer` (which may re-enter the graft
561                // tables).
562                let (inner_prov, outer_leaf, inner_path) = {
563                    let grafts = self.grafts.borrow();
564                    let a = grafts[g].inner.adapter();
565                    (a.provenance(inner), grafts[g].outer, quarb::name_path(a, inner))
566                };
567                inner_prov.or(self.graft_fill(outer_leaf, inner_path))
568            }
569            None => self.outer.provenance(node),
570        }
571    }
572    /// The same layering, entry by entry.
573    fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
574        match self.split(node) {
575            Some((g, inner)) => {
576                let (inner_list, outer_leaf, inner_path) = {
577                    let grafts = self.grafts.borrow();
578                    let a = grafts[g].inner.adapter();
579                    (a.provenance_list(inner), grafts[g].outer, quarb::name_path(a, inner))
580                };
581                inner_list.or(self.graft_fill(outer_leaf, inner_path))
582            }
583            None => self.outer.provenance_list(node),
584        }
585    }
586
587    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
588        match self.split(node) {
589            Some((g, inner)) => {
590                let t = self.grafts.borrow()[g]
591                    .inner
592                    .adapter()
593                    .resolve(inner, property, hint)?;
594                Some(self.wrap(g, t))
595            }
596            None => self.outer.resolve(node, property, hint),
597        }
598    }
599
600    /// A trait test reaches the graft — and, on the outer leaf,
601    /// the graft's root, as `traits` does.
602    fn has_trait(&self, node: NodeId, name: &str) -> bool {
603        match self.split(node) {
604            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().has_trait(inner, name),
605            None => {
606                self.outer.has_trait(node, name)
607                    || self.graft_at(node).is_some_and(|g| {
608                        let grafts = self.grafts.borrow();
609                        let a = grafts[g].inner.adapter();
610                        a.has_trait(a.root(), name)
611                    })
612            }
613        }
614    }
615
616    fn ref_property(&self, node: NodeId) -> Option<String> {
617        match self.split(node) {
618            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().ref_property(inner),
619            None => self.outer.ref_property(node),
620        }
621    }
622
623    /// A graft's external references (a text-level ref leaving
624    /// its document) come through; an outer leaf's own do too.
625    fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
626        match self.split(node) {
627            Some((g, inner)) => self.grafts.borrow()[g]
628                .inner
629                .adapter()
630                .external_ref(inner, property, hint),
631            None => self.outer.external_ref(node, property, hint),
632        }
633    }
634
635    fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
636        match self.split(node) {
637            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().ref_label(inner, property),
638            None => self.outer.ref_label(node, property),
639        }
640    }
641
642    /// A fragment lands inside the document: on an outer leaf
643    /// that holds a graft, inside that graft.
644    fn resolve_fragment(&self, node: NodeId, fragment: &str) -> Option<NodeId> {
645        match self.split(node) {
646            Some((g, inner)) => {
647                let t = self.grafts.borrow()[g].inner.adapter().resolve_fragment(inner, fragment)?;
648                Some(self.wrap(g, t))
649            }
650            None => match self.graft_at(node) {
651                Some(g) => {
652                    let t = {
653                        let grafts = self.grafts.borrow();
654                        let a = grafts[g].inner.adapter();
655                        a.resolve_fragment(a.root(), fragment)?
656                    };
657                    Some(self.wrap(g, t))
658                }
659                None => self.outer.resolve_fragment(node, fragment),
660            },
661        }
662    }
663
664    /// The leaf serving a URL, from the declared document URLs;
665    /// else whatever the outer answers.
666    fn document_by_ref(&self, id: &str) -> Option<NodeId> {
667        if let Some(leaf) = self.leaf_by_url(id) {
668            // The graft is parsed here so a fragment can land.
669            let _ = self.graft_at(leaf);
670            return Some(leaf);
671        }
672        self.outer.document_by_ref(id)
673    }
674
675    /// A reverse index answers on one side of the boundary only:
676    /// inside a graft, or on the outer tree when no graft could
677    /// hold a referrer (the outer's answer cannot see grafts).
678    fn reverse_resolve(&self, node: NodeId, property: Option<&str>, hint: Option<&str>) -> Option<Vec<NodeId>> {
679        match self.split(node) {
680            Some((g, inner)) => {
681                let v = self.grafts.borrow()[g].inner.adapter().reverse_resolve(inner, property, hint)?;
682                Some(v.into_iter().map(|t| self.wrap(g, t)).collect())
683            }
684            None => None,
685        }
686    }
687
688    /// Inside a graft the graft's index answers; on the outer tree
689    /// a name may also live inside grafts, so the walk stays.
690    fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
691        let (g, inner) = self.split(node)?;
692        let v = self.grafts.borrow()[g].inner.adapter().descendants_named(inner, name)?;
693        Some(v.into_iter().map(|(t, d)| (self.wrap(g, t), d)).collect())
694    }
695
696    fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
697        let outer: Vec<NodeId> = nodes.iter().copied().filter(|n| self.split(*n).is_none()).collect();
698        if !outer.is_empty() {
699            self.outer.prefetch_links(&outer, dir);
700        }
701    }
702
703    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
704        match self.split(node) {
705            Some((g, inner)) => {
706                let ls: Vec<(String, NodeId)> = {
707                    let grafts = self.grafts.borrow();
708                    grafts[g].inner.adapter().links(inner)
709                };
710                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
711            }
712            None => self.outer.links(node),
713        }
714    }
715
716    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
717        match self.split(node) {
718            Some((g, inner)) => {
719                let ls: Vec<(String, NodeId)> = {
720                    let grafts = self.grafts.borrow();
721                    grafts[g].inner.adapter().backlinks(inner)
722                };
723                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
724            }
725            None => self.outer.backlinks(node),
726        }
727    }
728
729    fn link_property(
730        &self,
731        source: NodeId,
732        label: &str,
733        target: NodeId,
734        name: &str,
735    ) -> Option<Value> {
736        // An edge lives on one side of the graft boundary: both
737        // endpoints are inner (same graft) or both outer.
738        match (self.split(source), self.split(target)) {
739            (Some((g, src)), Some((gt, tgt))) if g == gt => self.grafts.borrow()[g]
740                .inner
741                .adapter()
742                .link_property(src, label, tgt, name),
743            (None, None) => self.outer.link_property(source, label, target, name),
744            _ => None,
745        }
746    }
747}