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