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.
204/// A host's reader for a document leaf: the path in, its text-level
205/// model out, or `None` to leave the leaf to the default grafting.
206pub type DocumentReader = std::rc::Rc<dyn Fn(&std::path::Path) -> Option<quarb_text::TextModel>>;
207
208pub struct ComposeAdapter<A: AstAdapter> {
209    outer: A,
210    grafts: RefCell<Vec<Graft>>,
211    /// Outer leaf → its graft index (`None`: probed, not
212    /// parseable).
213    probed: RefCell<HashMap<NodeId, Option<usize>>>,
214    /// (graft, inner id) → interned composite id, and the reverse.
215    interned: RefCell<HashMap<(usize, NodeId), NodeId>>,
216    reverse: RefCell<Vec<(usize, NodeId)>>,
217    /// Maps an outer leaf to a filesystem path, when the outer
218    /// substrate has one. Enables archive grafts, which are
219    /// binary and open by path rather than parsing from text.
220    source_path: Option<fn(&A, NodeId) -> Option<PathBuf>>,
221    /// The level source-file leaves graft at.
222    source_graft: SourceGraft,
223    /// The level marked-up documents graft at.
224    document_graft: DocumentGraft,
225    /// Maps an outer leaf to the URL its document is served
226    /// from, so a text-level graft joins relative references
227    /// against it and `-->` can land on a sibling document.
228    document_url: Option<fn(&A, NodeId) -> Option<String>>,
229    /// URL → outer leaf, built on first `document_by_ref`.
230    urls: RefCell<Option<HashMap<String, NodeId>>>,
231    /// The host's own reading of a document leaf, by path: when it
232    /// answers, its model is the graft (a corpus reading with its
233    /// tokens, a format this crate does not parse); when it
234    /// declines, the leaf grafts as it otherwise would.
235    document_reader: Option<DocumentReader>,
236}
237
238impl<A: AstAdapter> ComposeAdapter<A> {
239    pub fn new(outer: A) -> Self {
240        ComposeAdapter {
241            outer,
242            grafts: RefCell::new(Vec::new()),
243            probed: RefCell::new(HashMap::new()),
244            interned: RefCell::new(HashMap::new()),
245            reverse: RefCell::new(Vec::new()),
246            source_path: None,
247            source_graft: SourceGraft::default(),
248            document_graft: DocumentGraft::default(),
249            document_url: None,
250            urls: RefCell::new(None),
251            document_reader: None,
252        }
253    }
254
255    /// Hand document leaves to the host's reader first
256    /// (builder-style); needs the outer leaves' paths
257    /// ([`with_source_paths`](Self::with_source_paths)).
258    pub fn with_document_reader(mut self, reader: DocumentReader) -> Self {
259        self.document_reader = Some(reader);
260        self
261    }
262
263    /// Declare where each outer leaf's document is served from
264    /// (builder-style): a text-level graft joins its relative
265    /// references against that URL, and `-->` from any graft
266    /// lands on the leaf holding the referenced document (its
267    /// fragment's bearer when named) without a host registration.
268    pub fn with_document_urls(mut self, document_url: fn(&A, NodeId) -> Option<String>) -> Self {
269        self.document_url = Some(document_url);
270        self
271    }
272
273    /// The outer leaf serving `url` (fragment-stripped), from the
274    /// declared URLs; the whole outer tree is walked once.
275    fn leaf_by_url(&self, url: &str) -> Option<NodeId> {
276        let f = self.document_url?;
277        if self.urls.borrow().is_none() {
278            let mut map = HashMap::new();
279            let mut stack = vec![self.outer.root()];
280            while let Some(n) = stack.pop() {
281                let kids = self.outer.children(n);
282                if kids.is_empty() {
283                    if let Some(u) = f(&self.outer, n) {
284                        map.entry(u.split('#').next().unwrap_or(&u).to_string())
285                            .or_insert(n);
286                    }
287                } else {
288                    stack.extend(kids);
289                }
290            }
291            *self.urls.borrow_mut() = Some(map);
292        }
293        self.urls.borrow().as_ref()?.get(url).copied()
294    }
295
296    /// Choose the level source-file leaves graft at
297    /// (builder-style; the default is the syntax level).
298    pub fn with_source_graft(mut self, graft: SourceGraft) -> Self {
299        self.source_graft = graft;
300        self
301    }
302
303    /// Choose the level marked-up documents graft at
304    /// (builder-style; the default is the DOM).
305    pub fn with_document_graft(mut self, graft: DocumentGraft) -> Self {
306        self.document_graft = graft;
307        self
308    }
309
310    /// Like [`new`](Self::new), with a hook mapping outer leaves
311    /// to filesystem paths, so archive leaves (`.zip`,
312    /// `.tar[.gz]`) graft too. The grafted archive composes in
313    /// turn: its own parseable entries graft, and one path walks
314    /// filesystem → archive → document.
315    pub fn with_source_paths(outer: A, source_path: fn(&A, NodeId) -> Option<PathBuf>) -> Self {
316        ComposeAdapter {
317            source_path: Some(source_path),
318            ..Self::new(outer)
319        }
320    }
321
322    /// The wrapped adapter (for outer-specific calls).
323    pub fn outer(&self) -> &A {
324        &self.outer
325    }
326
327    /// A combined locator: `outer_locator(leaf)!inner-path` for
328    /// grafted nodes.
329    pub fn locator(&self, node: NodeId, outer_locator: impl Fn(NodeId) -> String) -> String {
330        match self.split(node) {
331            None => outer_locator(node),
332            Some((g, inner)) => {
333                let grafts = self.grafts.borrow();
334                let graft = &grafts[g];
335                format!(
336                    "{}!{}",
337                    outer_locator(graft.outer),
338                    graft.inner.locator(inner)
339                )
340            }
341        }
342    }
343
344    /// Decode a composite id.
345    /// The graft's fill for a node inside a grafted document: the
346    /// outer leaf's provenance (the file's path and mtime) with the
347    /// mtime re-labelled as the source rung, and the node's path
348    /// inside the document.
349    fn graft_fill(&self, outer_leaf: NodeId, inner_path: String) -> quarb::Provenance {
350        let mut fill = self.outer.provenance(outer_leaf).as_source_rung();
351        fill.path = Some(inner_path);
352        fill
353    }
354
355    fn split(&self, node: NodeId) -> Option<(usize, NodeId)> {
356        if node.0 & GRAFT_BIT == 0 {
357            return None;
358        }
359        self.reverse
360            .borrow()
361            .get((node.0 & !GRAFT_BIT) as usize)
362            .copied()
363    }
364
365    fn intern(&self, graft: usize, inner: NodeId) -> NodeId {
366        if let Some(&id) = self.interned.borrow().get(&(graft, inner)) {
367            return id;
368        }
369        let mut rev = self.reverse.borrow_mut();
370        let id = NodeId(GRAFT_BIT | rev.len() as u64);
371        rev.push((graft, inner));
372        self.interned.borrow_mut().insert((graft, inner), id);
373        id
374    }
375
376    /// The graft at an outer leaf, probing (read + parse) on first
377    /// touch. Only childless outer nodes with text content are
378    /// candidates.
379    fn graft_at(&self, node: NodeId) -> Option<usize> {
380        if let Some(&g) = self.probed.borrow().get(&node) {
381            return g;
382        }
383        let g = (|| {
384            if !self.outer.children(node).is_empty() {
385                return None;
386            }
387            let name = self.outer.name(node)?;
388            if archive_name(&name)
389                && let Some(path_of) = self.source_path
390                && let Some(path) = path_of(&self.outer, node)
391                && let Ok(a) = quarb_archive::ArchiveAdapter::open(&path)
392            {
393                let inner = Inner::Archive(Box::new(
394                    ComposeAdapter::new(a)
395                        .with_source_graft(self.source_graft)
396                        .with_document_graft(self.document_graft),
397                ));
398                let mut grafts = self.grafts.borrow_mut();
399                grafts.push(Graft { outer: node, inner });
400                return Some(grafts.len() - 1);
401            }
402            if let Some(read) = &self.document_reader
403                && let Some(path_of) = self.source_path
404                && let Some(path) = path_of(&self.outer, node)
405                && let Some(model) = read(&path)
406            {
407                let mut grafts = self.grafts.borrow_mut();
408                grafts.push(Graft {
409                    outer: node,
410                    inner: Inner::Text(model),
411                });
412                return Some(grafts.len() - 1);
413            }
414            let content = match self.outer.default_value(node)? {
415                Value::Str(s) => s,
416                _ => return None,
417            };
418            let mut inner = parse_inner(&name, &content, self.source_graft, self.document_graft)?;
419            if let (Inner::Text(model), Some(f)) = (&mut inner, self.document_url)
420                && let Some(u) = f(&self.outer, node)
421            {
422                model.set_document_url(&u);
423            }
424            let mut grafts = self.grafts.borrow_mut();
425            grafts.push(Graft { outer: node, inner });
426            Some(grafts.len() - 1)
427        })();
428        self.probed.borrow_mut().insert(node, g);
429        g
430    }
431
432    /// Map an inner node up: the inner root becomes the outer
433    /// leaf.
434    fn wrap(&self, graft: usize, inner: NodeId) -> NodeId {
435        let grafts = self.grafts.borrow();
436        if inner == grafts[graft].inner.adapter().root() {
437            grafts[graft].outer
438        } else {
439            drop(grafts);
440            self.intern(graft, inner)
441        }
442    }
443}
444
445impl<A: AstAdapter> AstAdapter for ComposeAdapter<A> {
446    fn root(&self) -> NodeId {
447        self.outer.root()
448    }
449
450    fn children(&self, node: NodeId) -> Vec<NodeId> {
451        match self.split(node) {
452            Some((g, inner)) => {
453                let ids: Vec<NodeId> = {
454                    let grafts = self.grafts.borrow();
455                    grafts[g].inner.adapter().children(inner)
456                };
457                ids.into_iter().map(|c| self.wrap(g, c)).collect()
458            }
459            None => {
460                let outer = self.outer.children(node);
461                if !outer.is_empty() {
462                    return outer;
463                }
464                match self.graft_at(node) {
465                    Some(g) => {
466                        let ids: Vec<NodeId> = {
467                            let grafts = self.grafts.borrow();
468                            let a = grafts[g].inner.adapter();
469                            a.children(a.root())
470                        };
471                        ids.into_iter().map(|c| self.wrap(g, c)).collect()
472                    }
473                    None => Vec::new(),
474                }
475            }
476        }
477    }
478
479    fn name(&self, node: NodeId) -> Option<String> {
480        match self.split(node) {
481            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().name(inner),
482            None => self.outer.name(node),
483        }
484    }
485
486    fn parent(&self, node: NodeId) -> Option<NodeId> {
487        match self.split(node) {
488            Some((g, inner)) => {
489                let p = self.grafts.borrow()[g].inner.adapter().parent(inner)?;
490                Some(self.wrap(g, p))
491            }
492            None => self.outer.parent(node),
493        }
494    }
495
496    fn child_host(&self, node: NodeId) -> NodeId {
497        match self.split(node) {
498            Some((g, inner)) => {
499                let h = self.grafts.borrow()[g].inner.adapter().child_host(inner);
500                self.wrap(g, h)
501            }
502            None => self.outer.child_host(node),
503        }
504    }
505
506    fn properties(&self, node: NodeId) -> Vec<(String, Value)> {
507        match self.split(node) {
508            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().properties(inner),
509            None => self.outer.properties(node),
510        }
511    }
512
513    /// The outer tree's non-tree nodes, then each graft's (the
514    /// grafts already read).
515    fn non_tree_nodes(&self) -> Vec<NodeId> {
516        let mut out = self.outer.non_tree_nodes();
517        let grafts = self.grafts.borrow();
518        for (g, graft) in grafts.iter().enumerate() {
519            out.extend(
520                graft
521                    .inner
522                    .adapter()
523                    .non_tree_nodes()
524                    .into_iter()
525                    .map(|n| self.wrap(g, n)),
526            );
527        }
528        out
529    }
530
531    /// A written name reaches a node by the owning adapter's rule
532    /// (its aliases included).
533    fn answers_to(&self, node: NodeId, name: &str) -> bool {
534        match self.split(node) {
535            Some((g, inner)) => self.grafts.borrow()[g]
536                .inner
537                .adapter()
538                .answers_to(inner, name),
539            None => self.outer.answers_to(node, name),
540        }
541    }
542
543    /// The graft root IS the outer leaf, so the leaf wears the
544    /// grafted document's own traits too — a page's declared tags
545    /// on the file that holds it.
546    fn traits(&self, node: NodeId) -> Vec<String> {
547        match self.split(node) {
548            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().traits(inner),
549            None => {
550                let mut out = self.outer.traits(node);
551                if let Some(g) = self.graft_at(node) {
552                    let grafts = self.grafts.borrow();
553                    let a = grafts[g].inner.adapter();
554                    for t in a.traits(a.root()) {
555                        if !out.contains(&t) {
556                            out.push(t);
557                        }
558                    }
559                }
560                out
561            }
562        }
563    }
564
565    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
566        match self.split(node) {
567            Some((g, inner)) => self.grafts.borrow()[g]
568                .inner
569                .adapter()
570                .property(inner, name),
571            // The graft root IS the outer leaf, so a property the
572            // outer can't answer falls through to the grafted
573            // document's root — `::key` dual exposure works *at*
574            // the graft, not only one level in.
575            None => self.outer.property(node, name).or_else(|| {
576                let g = self.graft_at(node)?;
577                let grafts = self.grafts.borrow();
578                let a = grafts[g].inner.adapter();
579                a.property(a.root(), name)
580            }),
581        }
582    }
583
584    fn default_value(&self, node: NodeId) -> Option<Value> {
585        match self.split(node) {
586            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().default_value(inner),
587            None => self.outer.default_value(node),
588        }
589    }
590
591    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
592        match self.split(node) {
593            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().metadata(inner, key),
594            // Like properties: what the outer cannot answer, the
595            // grafted document's root may (`::::"og:type"` on the
596            // file that holds the page).
597            None => self.outer.metadata(node, key).or_else(|| {
598                let g = self.graft_at(node)?;
599                let grafts = self.grafts.borrow();
600                let a = grafts[g].inner.adapter();
601                a.metadata(a.root(), key)
602            }),
603        }
604    }
605
606    /// A graft's own aliases are not merged: a node either belongs
607    /// to the outer document or to a graft, and the alias list is
608    /// consulted only after the owning adapter's `property` misses.
609    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
610        match self.split(node) {
611            Some((g, inner)) => self.grafts.borrow()[g]
612                .inner
613                .adapter()
614                .aliased_metadata(inner),
615            None => self.outer.aliased_metadata(node),
616        }
617    }
618
619    /// A graft is a provenance layer: the grafted document's own
620    /// components win, and the outer *leaf* it was parsed from fills
621    /// the rest (with an fs outer: the file's path and mtime). The
622    /// jar-style composite locator is deliberately not the source —
623    /// `?src` names a document, and the intra-document address is
624    /// dpid's job.
625    fn provenance(&self, node: NodeId) -> quarb::Provenance {
626        match self.split(node) {
627            Some((g, inner)) => {
628                // Clone what we need out of the borrow before calling
629                // into `self.outer` (which may re-enter the graft
630                // tables).
631                let (inner_prov, outer_leaf, inner_path) = {
632                    let grafts = self.grafts.borrow();
633                    let a = grafts[g].inner.adapter();
634                    (
635                        a.provenance(inner),
636                        grafts[g].outer,
637                        quarb::name_path(a, inner),
638                    )
639                };
640                inner_prov.or(self.graft_fill(outer_leaf, inner_path))
641            }
642            None => self.outer.provenance(node),
643        }
644    }
645    /// The same layering, entry by entry.
646    fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
647        match self.split(node) {
648            Some((g, inner)) => {
649                let (inner_list, outer_leaf, inner_path) = {
650                    let grafts = self.grafts.borrow();
651                    let a = grafts[g].inner.adapter();
652                    (
653                        a.provenance_list(inner),
654                        grafts[g].outer,
655                        quarb::name_path(a, inner),
656                    )
657                };
658                inner_list.or(self.graft_fill(outer_leaf, inner_path))
659            }
660            None => self.outer.provenance_list(node),
661        }
662    }
663
664    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
665        match self.split(node) {
666            Some((g, inner)) => {
667                let t = self.grafts.borrow()[g]
668                    .inner
669                    .adapter()
670                    .resolve(inner, property, hint)?;
671                Some(self.wrap(g, t))
672            }
673            None => self.outer.resolve(node, property, hint),
674        }
675    }
676
677    /// A trait test reaches the graft — and, on the outer leaf,
678    /// the graft's root, as `traits` does.
679    fn has_trait(&self, node: NodeId, name: &str) -> bool {
680        match self.split(node) {
681            Some((g, inner)) => self.grafts.borrow()[g]
682                .inner
683                .adapter()
684                .has_trait(inner, name),
685            None => {
686                self.outer.has_trait(node, name)
687                    || self.graft_at(node).is_some_and(|g| {
688                        let grafts = self.grafts.borrow();
689                        let a = grafts[g].inner.adapter();
690                        a.has_trait(a.root(), name)
691                    })
692            }
693        }
694    }
695
696    fn ref_property(&self, node: NodeId) -> Option<String> {
697        match self.split(node) {
698            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().ref_property(inner),
699            None => self.outer.ref_property(node),
700        }
701    }
702
703    /// A graft's external references (a text-level ref leaving
704    /// its document) come through; an outer leaf's own do too.
705    fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
706        match self.split(node) {
707            Some((g, inner)) => self.grafts.borrow()[g]
708                .inner
709                .adapter()
710                .external_ref(inner, property, hint),
711            None => self.outer.external_ref(node, property, hint),
712        }
713    }
714
715    fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
716        match self.split(node) {
717            Some((g, inner)) => self.grafts.borrow()[g]
718                .inner
719                .adapter()
720                .ref_label(inner, property),
721            None => self.outer.ref_label(node, property),
722        }
723    }
724
725    /// A fragment lands inside the document: on an outer leaf
726    /// that holds a graft, inside that graft.
727    fn resolve_fragment(&self, node: NodeId, fragment: &str) -> Option<NodeId> {
728        match self.split(node) {
729            Some((g, inner)) => {
730                let t = self.grafts.borrow()[g]
731                    .inner
732                    .adapter()
733                    .resolve_fragment(inner, fragment)?;
734                Some(self.wrap(g, t))
735            }
736            None => match self.graft_at(node) {
737                Some(g) => {
738                    let t = {
739                        let grafts = self.grafts.borrow();
740                        let a = grafts[g].inner.adapter();
741                        a.resolve_fragment(a.root(), fragment)?
742                    };
743                    Some(self.wrap(g, t))
744                }
745                None => self.outer.resolve_fragment(node, fragment),
746            },
747        }
748    }
749
750    /// The leaf serving a URL, from the declared document URLs;
751    /// else whatever the outer answers.
752    fn document_by_ref(&self, id: &str) -> Option<NodeId> {
753        if let Some(leaf) = self.leaf_by_url(id) {
754            // The graft is parsed here so a fragment can land.
755            let _ = self.graft_at(leaf);
756            return Some(leaf);
757        }
758        self.outer.document_by_ref(id)
759    }
760
761    /// A reverse index answers on one side of the boundary only:
762    /// inside a graft, or on the outer tree when no graft could
763    /// hold a referrer (the outer's answer cannot see grafts).
764    fn reverse_resolve(
765        &self,
766        node: NodeId,
767        property: Option<&str>,
768        hint: Option<&str>,
769    ) -> Option<Vec<NodeId>> {
770        match self.split(node) {
771            Some((g, inner)) => {
772                let v = self.grafts.borrow()[g]
773                    .inner
774                    .adapter()
775                    .reverse_resolve(inner, property, hint)?;
776                Some(v.into_iter().map(|t| self.wrap(g, t)).collect())
777            }
778            None => None,
779        }
780    }
781
782    /// Inside a graft the graft's index answers; on the outer tree
783    /// a name may also live inside grafts, so the walk stays.
784    fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
785        let (g, inner) = self.split(node)?;
786        let v = self.grafts.borrow()[g]
787            .inner
788            .adapter()
789            .descendants_named(inner, name)?;
790        Some(v.into_iter().map(|(t, d)| (self.wrap(g, t), d)).collect())
791    }
792
793    fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
794        let outer: Vec<NodeId> = nodes
795            .iter()
796            .copied()
797            .filter(|n| self.split(*n).is_none())
798            .collect();
799        if !outer.is_empty() {
800            self.outer.prefetch_links(&outer, dir);
801        }
802    }
803
804    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
805        match self.split(node) {
806            Some((g, inner)) => {
807                let ls: Vec<(String, NodeId)> = {
808                    let grafts = self.grafts.borrow();
809                    grafts[g].inner.adapter().links(inner)
810                };
811                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
812            }
813            None => self.outer.links(node),
814        }
815    }
816
817    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
818        match self.split(node) {
819            Some((g, inner)) => {
820                let ls: Vec<(String, NodeId)> = {
821                    let grafts = self.grafts.borrow();
822                    grafts[g].inner.adapter().backlinks(inner)
823                };
824                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
825            }
826            None => self.outer.backlinks(node),
827        }
828    }
829
830    fn link_property(
831        &self,
832        source: NodeId,
833        label: &str,
834        target: NodeId,
835        name: &str,
836    ) -> Option<Value> {
837        // An edge lives on one side of the graft boundary: both
838        // endpoints are inner (same graft) or both outer.
839        match (self.split(source), self.split(target)) {
840            (Some((g, src)), Some((gt, tgt))) if g == gt => self.grafts.borrow()[g]
841                .inner
842                .adapter()
843                .link_property(src, label, tgt, name),
844            (None, None) => self.outer.link_property(source, label, target, name),
845            _ => None,
846        }
847    }
848}