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::source::SourceDecoding;
34use quarb::{AstAdapter, NodeId, Value};
35use std::cell::RefCell;
36use std::collections::HashMap;
37use std::path::PathBuf;
38
39/// A parsed inner arbor.
40enum Inner {
41    Json(quarb_json::JsonAdapter),
42    Xml(quarb_xml::XmlAdapter),
43    Html(quarb_html::HtmlAdapter),
44    Text(quarb_text::TextModel),
45    Csv(quarb_csv::CsvAdapter),
46    #[cfg(feature = "source")]
47    Syntax(quarb_tree_sitter::TreeSitterAdapter),
48    #[cfg(feature = "source")]
49    Code(quarb_code::CodeModel),
50    /// A grafted archive, itself composed so its own parseable
51    /// entries graft in turn.
52    Archive(Box<ComposeAdapter<quarb_archive::ArchiveAdapter>>),
53}
54
55impl Inner {
56    fn adapter(&self) -> &dyn AstAdapter {
57        match self {
58            Inner::Json(a) => a,
59            Inner::Xml(a) => a,
60            Inner::Html(a) => a,
61            Inner::Text(a) => a,
62            Inner::Csv(a) => a,
63            #[cfg(feature = "source")]
64            Inner::Syntax(a) => a,
65            #[cfg(feature = "source")]
66            Inner::Code(a) => a,
67            Inner::Archive(a) => &**a,
68        }
69    }
70
71    fn locator(&self, node: NodeId) -> String {
72        match self {
73            Inner::Json(a) => a.pointer(node),
74            Inner::Xml(a) => a.locator(node),
75            Inner::Html(a) => a.locator(node),
76            Inner::Text(a) => a.locator(node),
77            Inner::Csv(a) => a.locator(node),
78            #[cfg(feature = "source")]
79            Inner::Syntax(a) => a.locator(node),
80            #[cfg(feature = "source")]
81            Inner::Code(a) => a.locator(node),
82            Inner::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
83        }
84    }
85}
86
87/// The level source-file leaves graft at: the syntax level (the
88/// literal parse — the default) or the code level (identifiers
89/// as names). Chosen per mount by the `code:` target prefix, and
90/// inherited by nested composes (an archive inside a `code:`
91/// mount grafts its source entries at the code level too) — the
92/// filesystem-into-declarations namespace has no seam.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum SourceGraft {
95    #[default]
96    Syntax,
97    Code,
98}
99
100/// The level marked-up documents (`.html`, `.htm`, `.md`) graft
101/// at: their DOM by default, or the text level — sections,
102/// paragraphs, lists, and what the page declares about itself
103/// in its head — when the mount asks for the reader's model
104/// (`text:` on an archive or directory).
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum DocumentGraft {
107    #[default]
108    Dom,
109    Text,
110}
111
112/// The declaration reporter a host hands the source boundary: by
113/// the source's name, the XML prologue for XML-family names, the
114/// HTML prescan for HTML names, both tried (prologue first) for a
115/// name that says nothing — a plain text file declares nothing
116/// either way.
117pub fn declared_encoding(name: &str, bytes: &[u8]) -> Option<String> {
118    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
119    match ext.as_str() {
120        "xml" | "svg" | "xhtml" | "fb2" | "tei" | "rss" | "atom" | "opml" | "xsd" | "xsl" => {
121            quarb_xml::declared_encoding(bytes)
122        }
123        "html" | "htm" => quarb_html::declared_encoding(bytes),
124        // RTF's header names its code page (`\ansicpg1251`).
125        "rtf" => quarb_text_rtf::declared_encoding(bytes),
126        _ => quarb_xml::declared_encoding(bytes).or_else(|| quarb_html::declared_encoding(bytes)),
127    }
128}
129
130/// The rule a host mount carries: `?encoding=` validated at open,
131/// the reporter above.
132pub fn source_decoding(
133    explicit: Option<&str>,
134) -> Result<SourceDecoding, quarb::source::SourceError> {
135    SourceDecoding::default()
136        .with_declared(declared_encoding)
137        .with_option(explicit)
138}
139
140/// Read one file at the source boundary under the host rule:
141/// `?encoding=`, then a byte-order mark, then what the file
142/// declares, then strict UTF-8.
143pub fn read_source(
144    path: &std::path::Path,
145    explicit: Option<&str>,
146) -> std::io::Result<Result<quarb::source::Decoded, quarb::source::SourceError>> {
147    let bytes = std::fs::read(path)?;
148    let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
149    Ok(source_decoding(explicit).and_then(|d| d.decode(name, &bytes)))
150}
151
152/// Whether a leaf's name says archive: the one list quarb-archive
153/// keeps.
154fn archive_name(name: &str) -> bool {
155    quarb_archive::archive_extension(name.rsplit('.').next().unwrap_or(""))
156}
157
158/// Parse `content` by `name`'s extension, else by sniffing.
159fn parse_inner(
160    name: &str,
161    content: &str,
162    graft: SourceGraft,
163    doc: DocumentGraft,
164    csv: quarb_csv::CsvOptions,
165) -> Option<Inner> {
166    #[cfg(not(feature = "source"))]
167    let _ = graft;
168    let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
169    match ext.as_str() {
170        "json" => {
171            return quarb_json::JsonAdapter::parse(content)
172                .ok()
173                .map(Inner::Json);
174        }
175        "jsonl" | "ndjson" => {
176            return quarb_json::JsonAdapter::parse_lines(content)
177                .ok()
178                .map(Inner::Json);
179        }
180        "xml" | "svg" | "xhtml" => {
181            // A FictionBook declared by its namespace grafts at the
182            // text level through the koine route when the level
183            // asks for it; the DOM otherwise, like any XML.
184            #[cfg(feature = "koine")]
185            if doc == DocumentGraft::Text
186                && quarb_text_koine::detect_xml_kind(content) == Some("fb2")
187                && let Ok(m) = quarb_text_koine::parse_xml_as(content, "fb2")
188            {
189                return Some(Inner::Text(m));
190            }
191            return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
192        }
193        "fb2" => {
194            #[cfg(feature = "koine")]
195            if doc == DocumentGraft::Text
196                && let Ok(m) = quarb_text_koine::parse_xml_as(content, "fb2")
197            {
198                return Some(Inner::Text(m));
199            }
200            return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
201        }
202        "html" | "htm" => {
203            return Some(match doc {
204                DocumentGraft::Dom => Inner::Html(quarb_html::HtmlAdapter::parse(content)),
205                DocumentGraft::Text => Inner::Text(quarb_text_html::parse(content)),
206            });
207        }
208        "yaml" | "yml" => return quarb_yaml::parse(content).ok().map(Inner::Json),
209        "toml" => return quarb_toml::parse(content).ok().map(Inner::Json),
210        "md" | "markdown" => {
211            return Some(match doc {
212                DocumentGraft::Dom => Inner::Html(quarb_markdown::parse(content)),
213                DocumentGraft::Text => Inner::Text(quarb_text_markdown::parse(content)),
214            });
215        }
216        // Plain text grafts at the text level (blank-line
217        // paragraphs); html/md keep their DOM-level graft.
218        "txt" => return Some(Inner::Text(quarb_text::TextModel::parse_plain(content))),
219        "csv" => {
220            return quarb_csv::CsvAdapter::parse_declared(content, b',', csv)
221                .ok()
222                .map(Inner::Csv);
223        }
224        "tsv" => {
225            return quarb_csv::CsvAdapter::parse_declared(content, b'\t', csv)
226                .ok()
227                .map(Inner::Csv);
228        }
229        #[cfg(feature = "source")]
230        ext if quarb_tree_sitter::supported(ext) => {
231            return match graft {
232                SourceGraft::Syntax => quarb_tree_sitter::TreeSitterAdapter::parse(content, ext)
233                    .ok()
234                    .map(Inner::Syntax),
235                SourceGraft::Code => quarb_code::CodeModel::parse(content, ext)
236                    .ok()
237                    .map(Inner::Code),
238            };
239        }
240        _ => {}
241    }
242    // Content sniff for extensionless names.
243    let t = content.trim_start();
244    if t.starts_with('{') || t.starts_with('[') {
245        return quarb_json::JsonAdapter::parse(content)
246            .ok()
247            .map(Inner::Json);
248    }
249    if t.starts_with("<?xml") {
250        return quarb_xml::XmlAdapter::parse(content).ok().map(Inner::Xml);
251    }
252    None
253}
254
255/// Whether `parse_inner` could take a leaf of this name: one of the
256/// extensions it reads, a source extension with a grammar, or no
257/// extension at all (then the content decides). The list mirrors
258/// the arms of `parse_inner`.
259fn grafts_by_name(name: &str) -> bool {
260    let Some((_, ext)) = name.rsplit_once('.') else {
261        return true;
262    };
263    let ext = ext.to_ascii_lowercase();
264    if matches!(
265        ext.as_str(),
266        "json"
267            | "jsonl"
268            | "ndjson"
269            | "xml"
270            | "svg"
271            | "xhtml"
272            | "fb2"
273            | "html"
274            | "htm"
275            | "yaml"
276            | "yml"
277            | "toml"
278            | "md"
279            | "markdown"
280            | "txt"
281            | "csv"
282            | "tsv"
283    ) {
284        return true;
285    }
286    #[cfg(feature = "source")]
287    if quarb_tree_sitter::supported(&ext) {
288        return true;
289    }
290    false
291}
292
293/// One graft: an inner arbor mounted at an outer leaf.
294struct Graft {
295    outer: NodeId,
296    inner: Inner,
297}
298
299/// Grafted node ids carry this tag bit; the rest indexes the intern
300/// table. The bit sits at the top of the 56-bit id window that
301/// `MountAdapter` leaves an inner adapter — it reserves the high byte
302/// (bits 56–63) for the mount index — so a grafted id survives being
303/// packed into a mount and still round-trips. It must stay below bit
304/// 56 for that reason; the top bit (1 << 63) collided with the mount
305/// byte. Outer adapters use small sequential indices, far below this
306/// bit, so a set bit 55 unambiguously marks a graft.
307const GRAFT_BIT: u64 = 1 << 55;
308
309/// Any adapter, with parseable leaf content grafted as subtrees.
310/// A host's reader for a document leaf: the path in, its text-level
311/// model out, or `None` to leave the leaf to the default grafting.
312pub type DocumentReader = std::rc::Rc<dyn Fn(&std::path::Path) -> Option<quarb_text::TextModel>>;
313
314pub struct ComposeAdapter<A: AstAdapter> {
315    outer: A,
316    grafts: RefCell<Vec<Graft>>,
317    /// Outer leaf → its graft index (`None`: probed, not
318    /// parseable).
319    probed: RefCell<HashMap<NodeId, Option<usize>>>,
320    /// (graft, inner id) → interned composite id, and the reverse.
321    interned: RefCell<HashMap<(usize, NodeId), NodeId>>,
322    reverse: RefCell<Vec<(usize, NodeId)>>,
323    /// Maps an outer leaf to a filesystem path, when the outer
324    /// substrate has one. Enables archive grafts, which are
325    /// binary and open by path rather than parsing from text.
326    source_path: Option<fn(&A, NodeId) -> Option<PathBuf>>,
327    /// The level source-file leaves graft at.
328    source_graft: SourceGraft,
329    /// The level marked-up documents graft at.
330    document_graft: DocumentGraft,
331    /// Maps an outer leaf to the URL its document is served
332    /// from, so a text-level graft joins relative references
333    /// against it and `-->` can land on a sibling document.
334    document_url: Option<fn(&A, NodeId) -> Option<String>>,
335    /// URL → outer leaf, built on first `document_by_ref`.
336    urls: RefCell<Option<HashMap<String, NodeId>>>,
337    /// The host's own reading of a document leaf, by path: when it
338    /// answers, its model is the graft (a corpus reading with its
339    /// tokens, a format this crate does not parse); when it
340    /// declines, the leaf grafts as it otherwise would.
341    document_reader: Option<DocumentReader>,
342    /// The mount's rule at the source boundary (`?encoding=` and
343    /// the declaration reporter): every leaf this layer reads by
344    /// path decodes under it, and the layer answers `::::encoding`
345    /// / `::::replaced` on the leaf it grafted. A leaf with no path
346    /// grafts from the outer adapter's text as before.
347    decoding: SourceDecoding,
348    /// Per graft: the record of its decoding, when this layer did
349    /// it.
350    decoded: RefCell<HashMap<usize, (String, usize)>>,
351    /// The mount's `?delimiter=` / `?decimal=`, for the delimited
352    /// leaves it grafts.
353    csv: quarb_csv::CsvOptions,
354}
355
356impl<A: AstAdapter> ComposeAdapter<A> {
357    pub fn new(outer: A) -> Self {
358        ComposeAdapter {
359            outer,
360            grafts: RefCell::new(Vec::new()),
361            probed: RefCell::new(HashMap::new()),
362            interned: RefCell::new(HashMap::new()),
363            reverse: RefCell::new(Vec::new()),
364            source_path: None,
365            source_graft: SourceGraft::default(),
366            document_graft: DocumentGraft::default(),
367            document_url: None,
368            urls: RefCell::new(None),
369            document_reader: None,
370            decoding: SourceDecoding::default(),
371            decoded: RefCell::new(HashMap::new()),
372            csv: quarb_csv::CsvOptions::default(),
373        }
374    }
375
376    /// The delimited-text options for this mount (builder-style).
377    pub fn with_csv_options(mut self, csv: quarb_csv::CsvOptions) -> Self {
378        self.csv = csv;
379        self
380    }
381
382    /// The source-boundary rule for this mount (builder-style):
383    /// the `?encoding=` option and the reporter the host chose.
384    /// Reaches grafted archives too.
385    pub fn with_decoding(mut self, decoding: SourceDecoding) -> Self {
386        self.decoding = decoding;
387        self
388    }
389
390    /// Hand document leaves to the host's reader first
391    /// (builder-style); needs the outer leaves' paths
392    /// ([`with_source_paths`](Self::with_source_paths)).
393    pub fn with_document_reader(mut self, reader: DocumentReader) -> Self {
394        self.document_reader = Some(reader);
395        self
396    }
397
398    /// Declare where each outer leaf's document is served from
399    /// (builder-style): a text-level graft joins its relative
400    /// references against that URL, and `-->` from any graft
401    /// lands on the leaf holding the referenced document (its
402    /// fragment's bearer when named) without a host registration.
403    pub fn with_document_urls(mut self, document_url: fn(&A, NodeId) -> Option<String>) -> Self {
404        self.document_url = Some(document_url);
405        self
406    }
407
408    /// The outer leaf serving `url` (fragment-stripped), from the
409    /// declared URLs; the whole outer tree is walked once.
410    fn leaf_by_url(&self, url: &str) -> Option<NodeId> {
411        let f = self.document_url?;
412        if self.urls.borrow().is_none() {
413            let mut map = HashMap::new();
414            let mut stack = vec![self.outer.root()];
415            while let Some(n) = stack.pop() {
416                let kids = self.outer.children(n);
417                if kids.is_empty() {
418                    if let Some(u) = f(&self.outer, n) {
419                        map.entry(u.split('#').next().unwrap_or(&u).to_string())
420                            .or_insert(n);
421                    }
422                } else {
423                    stack.extend(kids);
424                }
425            }
426            *self.urls.borrow_mut() = Some(map);
427        }
428        self.urls.borrow().as_ref()?.get(url).copied()
429    }
430
431    /// Choose the level source-file leaves graft at
432    /// (builder-style; the default is the syntax level).
433    pub fn with_source_graft(mut self, graft: SourceGraft) -> Self {
434        self.source_graft = graft;
435        self
436    }
437
438    /// Choose the level marked-up documents graft at
439    /// (builder-style; the default is the DOM).
440    pub fn with_document_graft(mut self, graft: DocumentGraft) -> Self {
441        self.document_graft = graft;
442        self
443    }
444
445    /// Like [`new`](Self::new), with a hook mapping outer leaves
446    /// to filesystem paths, so archive leaves (`.zip`,
447    /// `.tar[.gz]`) graft too. The grafted archive composes in
448    /// turn: its own parseable entries graft, and one path walks
449    /// filesystem → archive → document.
450    pub fn with_source_paths(outer: A, source_path: fn(&A, NodeId) -> Option<PathBuf>) -> Self {
451        ComposeAdapter {
452            source_path: Some(source_path),
453            ..Self::new(outer)
454        }
455    }
456
457    /// The wrapped adapter (for outer-specific calls).
458    pub fn outer(&self) -> &A {
459        &self.outer
460    }
461
462    /// A combined locator: `outer_locator(leaf)!inner-path` for
463    /// grafted nodes.
464    pub fn locator(&self, node: NodeId, outer_locator: impl Fn(NodeId) -> String) -> String {
465        match self.split(node) {
466            None => outer_locator(node),
467            Some((g, inner)) => {
468                let grafts = self.grafts.borrow();
469                let graft = &grafts[g];
470                format!(
471                    "{}!{}",
472                    outer_locator(graft.outer),
473                    graft.inner.locator(inner)
474                )
475            }
476        }
477    }
478
479    /// Decode a composite id.
480    /// The graft's fill for a node inside a grafted document: the
481    /// outer leaf's provenance (the file's path and mtime) with the
482    /// mtime re-labelled as the source rung, and the node's path
483    /// inside the document.
484    fn graft_fill(&self, outer_leaf: NodeId, inner_path: String) -> quarb::Provenance {
485        let mut fill = self.outer.provenance(outer_leaf).as_source_rung();
486        fill.path = Some(inner_path);
487        fill
488    }
489
490    fn split(&self, node: NodeId) -> Option<(usize, NodeId)> {
491        if node.0 & GRAFT_BIT == 0 {
492            return None;
493        }
494        self.reverse
495            .borrow()
496            .get((node.0 & !GRAFT_BIT) as usize)
497            .copied()
498    }
499
500    fn intern(&self, graft: usize, inner: NodeId) -> NodeId {
501        if let Some(&id) = self.interned.borrow().get(&(graft, inner)) {
502            return id;
503        }
504        let mut rev = self.reverse.borrow_mut();
505        let id = NodeId(GRAFT_BIT | rev.len() as u64);
506        rev.push((graft, inner));
507        self.interned.borrow_mut().insert((graft, inner), id);
508        id
509    }
510
511    /// The graft at an outer leaf, probing (read + parse) on first
512    /// touch. Only childless outer nodes with text content are
513    /// candidates.
514    fn graft_at(&self, node: NodeId) -> Option<usize> {
515        if let Some(&g) = self.probed.borrow().get(&node) {
516            return g;
517        }
518        let g = (|| {
519            if !self.outer.children(node).is_empty() {
520                return None;
521            }
522            let name = self.outer.name(node)?;
523            if archive_name(&name)
524                && let Some(path_of) = self.source_path
525                && let Some(path) = path_of(&self.outer, node)
526                && let Ok(a) =
527                    quarb_archive::ArchiveAdapter::open_with_decoding(&path, self.decoding.clone())
528            {
529                let inner = Inner::Archive(Box::new(
530                    ComposeAdapter::new(a)
531                        .with_source_graft(self.source_graft)
532                        .with_document_graft(self.document_graft)
533                        .with_decoding(self.decoding.clone())
534                        .with_csv_options(self.csv),
535                ));
536                let mut grafts = self.grafts.borrow_mut();
537                grafts.push(Graft { outer: node, inner });
538                return Some(grafts.len() - 1);
539            }
540            if let Some(read) = &self.document_reader
541                && let Some(path_of) = self.source_path
542                && let Some(path) = path_of(&self.outer, node)
543                && let Some(model) = read(&path)
544            {
545                let mut grafts = self.grafts.borrow_mut();
546                grafts.push(Graft {
547                    outer: node,
548                    inner: Inner::Text(model),
549                });
550                return Some(grafts.len() - 1);
551            }
552            // The name decides first: a leaf whose extension no
553            // reader here takes (an image, a binary, an unknown
554            // suffix) stays a leaf unread — only a known extension,
555            // or none (the content sniff), is worth the bytes.
556            if !grafts_by_name(&name) {
557                return None;
558            }
559            // Through the path when there is one: the bytes meet
560            // the boundary here, under the mount's rule, and the
561            // record stays with the graft. A leaf whose bytes refuse
562            // to decode (not UTF-8, nothing declared) stays a leaf.
563            let mut record = None;
564            let content = match self
565                .source_path
566                .and_then(|path_of| path_of(&self.outer, node))
567                .and_then(|p| std::fs::read(p).ok())
568            {
569                Some(bytes) => {
570                    let d = self.decoding.decode(&name, &bytes).ok()?;
571                    record = Some((d.encoding, d.replaced));
572                    d.text
573                }
574                None => match self.outer.default_value(node)? {
575                    Value::Str(s) => s,
576                    _ => return None,
577                },
578            };
579            let mut inner = parse_inner(
580                &name,
581                &content,
582                self.source_graft,
583                self.document_graft,
584                self.csv,
585            )?;
586            if let (Inner::Text(model), Some(f)) = (&mut inner, self.document_url)
587                && let Some(u) = f(&self.outer, node)
588            {
589                model.set_document_url(&u);
590            }
591            let mut grafts = self.grafts.borrow_mut();
592            grafts.push(Graft { outer: node, inner });
593            if let Some(r) = record {
594                self.decoded.borrow_mut().insert(grafts.len() - 1, r);
595            }
596            Some(grafts.len() - 1)
597        })();
598        self.probed.borrow_mut().insert(node, g);
599        g
600    }
601
602    /// Map an inner node up: the inner root becomes the outer
603    /// leaf.
604    fn wrap(&self, graft: usize, inner: NodeId) -> NodeId {
605        let grafts = self.grafts.borrow();
606        if inner == grafts[graft].inner.adapter().root() {
607            grafts[graft].outer
608        } else {
609            drop(grafts);
610            self.intern(graft, inner)
611        }
612    }
613}
614
615impl<A: AstAdapter> AstAdapter for ComposeAdapter<A> {
616    fn root(&self) -> NodeId {
617        self.outer.root()
618    }
619
620    fn children(&self, node: NodeId) -> Vec<NodeId> {
621        match self.split(node) {
622            Some((g, inner)) => {
623                let ids: Vec<NodeId> = {
624                    let grafts = self.grafts.borrow();
625                    grafts[g].inner.adapter().children(inner)
626                };
627                ids.into_iter().map(|c| self.wrap(g, c)).collect()
628            }
629            None => {
630                let outer = self.outer.children(node);
631                if !outer.is_empty() {
632                    return outer;
633                }
634                match self.graft_at(node) {
635                    Some(g) => {
636                        let ids: Vec<NodeId> = {
637                            let grafts = self.grafts.borrow();
638                            let a = grafts[g].inner.adapter();
639                            a.children(a.root())
640                        };
641                        ids.into_iter().map(|c| self.wrap(g, c)).collect()
642                    }
643                    None => Vec::new(),
644                }
645            }
646        }
647    }
648
649    /// A child by name: the outer source's own lookup first (a
650    /// database reads `/books/7` by key), and the listing only where
651    /// the node may hold a grafted document — a leaf with a path or
652    /// with text. A miss on a node that cannot hold one stays a
653    /// miss, without listing its children.
654    fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
655        match self.split(node) {
656            Some((g, inner)) => {
657                let ids: Vec<NodeId> = {
658                    let grafts = self.grafts.borrow();
659                    grafts[g].inner.adapter().children_named(inner, name)
660                };
661                ids.into_iter().map(|c| self.wrap(g, c)).collect()
662            }
663            None => {
664                let named = self.outer.children_named(node, name);
665                if !named.is_empty() {
666                    return named;
667                }
668                if self.source_path.is_none()
669                    && !matches!(self.outer.default_value(node), Some(Value::Str(_)))
670                {
671                    return Vec::new();
672                }
673                self.children(node)
674                    .into_iter()
675                    .filter(|&c| self.answers_to(c, name))
676                    .collect()
677            }
678        }
679    }
680
681    fn name(&self, node: NodeId) -> Option<String> {
682        match self.split(node) {
683            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().name(inner),
684            None => self.outer.name(node),
685        }
686    }
687
688    fn parent(&self, node: NodeId) -> Option<NodeId> {
689        match self.split(node) {
690            Some((g, inner)) => {
691                let p = self.grafts.borrow()[g].inner.adapter().parent(inner)?;
692                Some(self.wrap(g, p))
693            }
694            None => self.outer.parent(node),
695        }
696    }
697
698    fn child_host(&self, node: NodeId) -> NodeId {
699        match self.split(node) {
700            Some((g, inner)) => {
701                let h = self.grafts.borrow()[g].inner.adapter().child_host(inner);
702                self.wrap(g, h)
703            }
704            None => self.outer.child_host(node),
705        }
706    }
707
708    fn properties(&self, node: NodeId) -> Vec<(String, Value)> {
709        match self.split(node) {
710            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().properties(inner),
711            None => self.outer.properties(node),
712        }
713    }
714
715    /// The walk is open when the outer tree opens to it (a graft is
716    /// read whole once reached).
717    fn walks_for_references(&self) -> bool {
718        self.outer.walks_for_references()
719    }
720    /// A declaration answers on its own side of the boundary; across
721    /// it only a reference property of the source's own can reach.
722    fn can_refer_to(&self, source: NodeId, target: NodeId) -> Option<bool> {
723        match (self.split(source), self.split(target)) {
724            (None, None) => self.outer.can_refer_to(source, target),
725            (Some((g, s)), Some((h, t))) if g == h => {
726                self.grafts.borrow()[g].inner.adapter().can_refer_to(s, t)
727            }
728            _ => self.ref_property(source).map(|_| true),
729        }
730    }
731    fn reference_properties(&self, source: NodeId, target: NodeId) -> Vec<String> {
732        match (self.split(source), self.split(target)) {
733            (None, None) => self.outer.reference_properties(source, target),
734            (Some((g, s)), Some((h, t))) if g == h => self.grafts.borrow()[g]
735                .inner
736                .adapter()
737                .reference_properties(s, t),
738            _ => self.ref_property(source).into_iter().collect(),
739        }
740    }
741    /// The outer source's own index over a scope of its own nodes
742    /// (a database reading `/books/*` by key).
743    fn index_referrers(
744        &self,
745        scope: &str,
746        property: &str,
747        label: Option<&str>,
748        target: NodeId,
749    ) -> Option<Vec<NodeId>> {
750        match self.split(target) {
751            None => self.outer.index_referrers(scope, property, label, target),
752            Some(_) => None,
753        }
754    }
755    fn tree_identities(&self) -> Vec<(String, NodeId)> {
756        self.outer.tree_identities()
757    }
758    fn reverse_label(&self, target: NodeId) -> Option<String> {
759        match self.split(target) {
760            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().reverse_label(inner),
761            None => self.outer.reverse_label(target),
762        }
763    }
764    fn resolve_needs_label(&self, node: NodeId, property: &str) -> bool {
765        match self.split(node) {
766            Some((g, inner)) => self.grafts.borrow()[g]
767                .inner
768                .adapter()
769                .resolve_needs_label(inner, property),
770            None => self.outer.resolve_needs_label(node, property),
771        }
772    }
773    fn property_names(&self, node: NodeId) -> Vec<String> {
774        match self.split(node) {
775            Some((g, inner)) => self.grafts.borrow()[g]
776                .inner
777                .adapter()
778                .property_names(inner),
779            None => self.outer.property_names(node),
780        }
781    }
782    /// The outer source, or any document grafted so far.
783    fn resolves_references(&self) -> bool {
784        self.outer.resolves_references()
785            || self
786                .grafts
787                .borrow()
788                .iter()
789                .any(|g| g.inner.adapter().resolves_references())
790    }
791
792    /// The outer tree's non-tree nodes, then each graft's (the
793    /// grafts already read).
794    fn non_tree_nodes(&self) -> Vec<NodeId> {
795        let mut out = self.outer.non_tree_nodes();
796        let grafts = self.grafts.borrow();
797        for (g, graft) in grafts.iter().enumerate() {
798            out.extend(
799                graft
800                    .inner
801                    .adapter()
802                    .non_tree_nodes()
803                    .into_iter()
804                    .map(|n| self.wrap(g, n)),
805            );
806        }
807        out
808    }
809
810    /// A written name reaches a node by the owning adapter's rule
811    /// (its aliases included).
812    fn answers_to(&self, node: NodeId, name: &str) -> bool {
813        match self.split(node) {
814            Some((g, inner)) => self.grafts.borrow()[g]
815                .inner
816                .adapter()
817                .answers_to(inner, name),
818            None => self.outer.answers_to(node, name),
819        }
820    }
821
822    /// The graft root IS the outer leaf, so the leaf wears the
823    /// grafted document's own traits too — a page's declared tags
824    /// on the file that holds it.
825    fn traits(&self, node: NodeId) -> Vec<String> {
826        match self.split(node) {
827            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().traits(inner),
828            None => {
829                let mut out = self.outer.traits(node);
830                if let Some(g) = self.graft_at(node) {
831                    let grafts = self.grafts.borrow();
832                    let a = grafts[g].inner.adapter();
833                    for t in a.traits(a.root()) {
834                        if !out.contains(&t) {
835                            out.push(t);
836                        }
837                    }
838                }
839                out
840            }
841        }
842    }
843
844    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
845        match self.split(node) {
846            Some((g, inner)) => self.grafts.borrow()[g]
847                .inner
848                .adapter()
849                .property(inner, name),
850            // The graft root IS the outer leaf, so a property the
851            // outer can't answer falls through to the grafted
852            // document's root — `::key` dual exposure works *at*
853            // the graft, not only one level in.
854            None => self.outer.property(node, name).or_else(|| {
855                let g = self.graft_at(node)?;
856                let grafts = self.grafts.borrow();
857                let a = grafts[g].inner.adapter();
858                a.property(a.root(), name)
859            }),
860        }
861    }
862
863    fn default_value(&self, node: NodeId) -> Option<Value> {
864        match self.split(node) {
865            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().default_value(inner),
866            // A leaf that did not graft (plain text, a format this
867            // crate does not parse) is still a source: read through
868            // its path under the mount's rule, so `?encoding=` and
869            // a declaration reach it. The outer adapter's own text
870            // (strict UTF-8) stands where there is no path.
871            None => {
872                if self.decoding.explicit.is_some()
873                    && self.outer.children(node).is_empty()
874                    && let Some(path_of) = self.source_path
875                    && let Some(p) = path_of(&self.outer, node)
876                    && p.is_file()
877                    && let Ok(bytes) = std::fs::read(&p)
878                {
879                    let name = self.outer.name(node).unwrap_or_default();
880                    return self
881                        .decoding
882                        .decode(&name, &bytes)
883                        .ok()
884                        .map(|d| Value::Str(d.text));
885                }
886                self.outer.default_value(node)
887            }
888        }
889    }
890
891    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
892        match self.split(node) {
893            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().metadata(inner, key),
894            // Like properties: what the outer cannot answer, the
895            // grafted document's root may (`::::"og:type"` on the
896            // file that holds the page).
897            None => self.outer.metadata(node, key).or_else(|| {
898                let g = self.graft_at(node)?;
899                if matches!(key, "encoding" | "replaced") {
900                    // A leaf the host's reader read by path has no
901                    // record yet: decode its bytes under the mount's
902                    // rule once, for the record alone.
903                    if !self.decoded.borrow().contains_key(&g)
904                        && let Some(path_of) = self.source_path
905                        && let Some(p) = path_of(&self.outer, node)
906                        && let Ok(bytes) = std::fs::read(&p)
907                    {
908                        let name = self.outer.name(node).unwrap_or_default();
909                        if let Ok(d) = self.decoding.decode(&name, &bytes) {
910                            self.decoded
911                                .borrow_mut()
912                                .insert(g, (d.encoding, d.replaced));
913                        }
914                    }
915                    if let Some((enc, replaced)) = self.decoded.borrow().get(&g) {
916                        return Some(match key {
917                            "encoding" => Value::Str(enc.clone()),
918                            _ => Value::Int(*replaced as i64),
919                        });
920                    }
921                }
922                let grafts = self.grafts.borrow();
923                let a = grafts[g].inner.adapter();
924                a.metadata(a.root(), key)
925            }),
926        }
927    }
928
929    /// A graft's own aliases are not merged: a node either belongs
930    /// to the outer document or to a graft, and the alias list is
931    /// consulted only after the owning adapter's `property` misses.
932    fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
933        match self.split(node) {
934            Some((g, inner)) => self.grafts.borrow()[g]
935                .inner
936                .adapter()
937                .aliased_metadata(inner),
938            None => self.outer.aliased_metadata(node),
939        }
940    }
941
942    /// A graft is a provenance layer: the grafted document's own
943    /// components win, and the outer *leaf* it was parsed from fills
944    /// the rest (with an fs outer: the file's path and mtime). The
945    /// jar-style composite locator is deliberately not the source —
946    /// `?src` names a document, and the intra-document address is
947    /// dpid's job.
948    fn provenance(&self, node: NodeId) -> quarb::Provenance {
949        match self.split(node) {
950            Some((g, inner)) => {
951                // Clone what we need out of the borrow before calling
952                // into `self.outer` (which may re-enter the graft
953                // tables).
954                let (inner_prov, outer_leaf, inner_path) = {
955                    let grafts = self.grafts.borrow();
956                    let a = grafts[g].inner.adapter();
957                    (
958                        a.provenance(inner),
959                        grafts[g].outer,
960                        quarb::name_path(a, inner),
961                    )
962                };
963                inner_prov.or(self.graft_fill(outer_leaf, inner_path))
964            }
965            None => self.outer.provenance(node),
966        }
967    }
968    /// The same layering, entry by entry.
969    fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
970        match self.split(node) {
971            Some((g, inner)) => {
972                let (inner_list, outer_leaf, inner_path) = {
973                    let grafts = self.grafts.borrow();
974                    let a = grafts[g].inner.adapter();
975                    (
976                        a.provenance_list(inner),
977                        grafts[g].outer,
978                        quarb::name_path(a, inner),
979                    )
980                };
981                inner_list.or(self.graft_fill(outer_leaf, inner_path))
982            }
983            None => self.outer.provenance_list(node),
984        }
985    }
986
987    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
988        match self.split(node) {
989            Some((g, inner)) => {
990                let t = self.grafts.borrow()[g]
991                    .inner
992                    .adapter()
993                    .resolve(inner, property, hint)?;
994                Some(self.wrap(g, t))
995            }
996            None => self.outer.resolve(node, property, hint),
997        }
998    }
999
1000    /// A trait test reaches the graft — and, on the outer leaf,
1001    /// the graft's root, as `traits` does.
1002    fn has_trait(&self, node: NodeId, name: &str) -> bool {
1003        match self.split(node) {
1004            Some((g, inner)) => self.grafts.borrow()[g]
1005                .inner
1006                .adapter()
1007                .has_trait(inner, name),
1008            None => {
1009                self.outer.has_trait(node, name)
1010                    || self.graft_at(node).is_some_and(|g| {
1011                        let grafts = self.grafts.borrow();
1012                        let a = grafts[g].inner.adapter();
1013                        a.has_trait(a.root(), name)
1014                    })
1015            }
1016        }
1017    }
1018
1019    fn ref_property(&self, node: NodeId) -> Option<String> {
1020        match self.split(node) {
1021            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().ref_property(inner),
1022            None => self.outer.ref_property(node),
1023        }
1024    }
1025
1026    /// A graft's external references (a text-level ref leaving
1027    /// its document) come through; an outer leaf's own do too.
1028    fn external_ref(
1029        &self,
1030        node: NodeId,
1031        property: &str,
1032        hint: Option<&str>,
1033    ) -> Option<quarb::ExternalRef> {
1034        match self.split(node) {
1035            Some((g, inner)) => self.grafts.borrow()[g]
1036                .inner
1037                .adapter()
1038                .external_ref(inner, property, hint),
1039            None => self.outer.external_ref(node, property, hint),
1040        }
1041    }
1042
1043    fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
1044        match self.split(node) {
1045            Some((g, inner)) => self.grafts.borrow()[g]
1046                .inner
1047                .adapter()
1048                .ref_label(inner, property),
1049            None => self.outer.ref_label(node, property),
1050        }
1051    }
1052
1053    /// A fragment lands inside the document: on an outer leaf
1054    /// that holds a graft, inside that graft.
1055    fn resolve_fragment(&self, node: NodeId, fragment: &str) -> Option<NodeId> {
1056        match self.split(node) {
1057            Some((g, inner)) => {
1058                let t = self.grafts.borrow()[g]
1059                    .inner
1060                    .adapter()
1061                    .resolve_fragment(inner, fragment)?;
1062                Some(self.wrap(g, t))
1063            }
1064            None => match self.graft_at(node) {
1065                Some(g) => {
1066                    let t = {
1067                        let grafts = self.grafts.borrow();
1068                        let a = grafts[g].inner.adapter();
1069                        a.resolve_fragment(a.root(), fragment)?
1070                    };
1071                    Some(self.wrap(g, t))
1072                }
1073                None => self.outer.resolve_fragment(node, fragment),
1074            },
1075        }
1076    }
1077
1078    /// The leaf serving a URL, from the declared document URLs;
1079    /// else whatever the outer answers.
1080    fn document_by_ref(&self, id: &str) -> Option<NodeId> {
1081        if let Some(leaf) = self.leaf_by_url(id) {
1082            // The graft is parsed here so a fragment can land.
1083            let _ = self.graft_at(leaf);
1084            return Some(leaf);
1085        }
1086        self.outer.document_by_ref(id)
1087    }
1088
1089    /// A reverse index answers on one side of the boundary only:
1090    /// inside a graft, the graft's own; on the outer tree, the
1091    /// outer's declared references (a catalog key, a model `ref`),
1092    /// which no graft can hold a referrer for.
1093    fn reverse_resolve(
1094        &self,
1095        node: NodeId,
1096        property: Option<&str>,
1097        hint: Option<&str>,
1098    ) -> Option<Vec<NodeId>> {
1099        match self.split(node) {
1100            Some((g, inner)) => {
1101                let v = self.grafts.borrow()[g]
1102                    .inner
1103                    .adapter()
1104                    .reverse_resolve(inner, property, hint)?;
1105                Some(v.into_iter().map(|t| self.wrap(g, t)).collect())
1106            }
1107            None => self.outer.reverse_resolve(node, property, hint),
1108        }
1109    }
1110
1111    /// Inside a graft the graft's index answers; on the outer tree
1112    /// a name may also live inside grafts, so the walk stays.
1113    fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
1114        let (g, inner) = self.split(node)?;
1115        let v = self.grafts.borrow()[g]
1116            .inner
1117            .adapter()
1118            .descendants_named(inner, name)?;
1119        Some(v.into_iter().map(|(t, d)| (self.wrap(g, t), d)).collect())
1120    }
1121
1122    fn descendants_where(
1123        &self,
1124        node: NodeId,
1125        name: &str,
1126        property: &str,
1127        value: &Value,
1128    ) -> Option<Vec<(NodeId, usize)>> {
1129        let (g, inner) = self.split(node)?;
1130        let v = self.grafts.borrow()[g]
1131            .inner
1132            .adapter()
1133            .descendants_where(inner, name, property, value)?;
1134        Some(v.into_iter().map(|(t, d)| (self.wrap(g, t), d)).collect())
1135    }
1136
1137    fn select(&self, from: NodeId, plan: &quarb::StepPlan<'_>) -> Option<quarb::Selection> {
1138        match self.split(from) {
1139            // Inside a graft: its adapter's own answer, wrapped.
1140            Some((g, inner)) => {
1141                let sel = self.grafts.borrow()[g]
1142                    .inner
1143                    .adapter()
1144                    .select(inner, plan)?;
1145                Some(quarb::Selection {
1146                    nodes: sel
1147                        .nodes
1148                        .into_iter()
1149                        .map(|(t, d)| (self.wrap(g, t), d))
1150                        .collect(),
1151                    applied: sel.applied,
1152                })
1153            }
1154            // Outside: the outer adapter's descendants may cross
1155            // into grafts, which the walk handles; a child hop is
1156            // this adapter's own `children_named`.
1157            None => match plan.axis {
1158                quarb::PlanAxis::Child => quarb::adapter::select_by_hooks(self, from, plan),
1159                quarb::PlanAxis::Descendant => None,
1160            },
1161        }
1162    }
1163
1164    fn index_lookup(&self, scope: &str, property: &str, value: &Value) -> Option<Vec<NodeId>> {
1165        self.outer.index_lookup(scope, property, value)
1166    }
1167
1168    fn select_plan(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<Vec<bool>> {
1169        self.outer.select_plan(scope, plan)
1170    }
1171
1172    fn estimate(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<quarb::Estimate> {
1173        self.outer.estimate(scope, plan)
1174    }
1175
1176    fn has_node(&self, node: NodeId) -> bool {
1177        match self.split(node) {
1178            Some((g, inner)) => self.grafts.borrow()[g].inner.adapter().has_node(inner),
1179            None => self.outer.has_node(node),
1180        }
1181    }
1182
1183    fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
1184        self.outer.unit_scale(expr)
1185    }
1186
1187    fn quantifier_bound(&self) -> usize {
1188        self.outer.quantifier_bound()
1189    }
1190
1191    fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
1192        let outer: Vec<NodeId> = nodes
1193            .iter()
1194            .copied()
1195            .filter(|n| self.split(*n).is_none())
1196            .collect();
1197        if !outer.is_empty() {
1198            self.outer.prefetch_links(&outer, dir);
1199        }
1200    }
1201
1202    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
1203        match self.split(node) {
1204            Some((g, inner)) => {
1205                let ls: Vec<(String, NodeId)> = {
1206                    let grafts = self.grafts.borrow();
1207                    grafts[g].inner.adapter().links(inner)
1208                };
1209                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
1210            }
1211            None => self.outer.links(node),
1212        }
1213    }
1214
1215    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
1216        match self.split(node) {
1217            Some((g, inner)) => {
1218                let ls: Vec<(String, NodeId)> = {
1219                    let grafts = self.grafts.borrow();
1220                    grafts[g].inner.adapter().backlinks(inner)
1221                };
1222                ls.into_iter().map(|(l, n)| (l, self.wrap(g, n))).collect()
1223            }
1224            None => self.outer.backlinks(node),
1225        }
1226    }
1227
1228    fn link_property(
1229        &self,
1230        source: NodeId,
1231        label: &str,
1232        target: NodeId,
1233        name: &str,
1234    ) -> Option<Value> {
1235        // An edge lives on one side of the graft boundary: both
1236        // endpoints are inner (same graft) or both outer.
1237        match (self.split(source), self.split(target)) {
1238            (Some((g, src)), Some((gt, tgt))) if g == gt => self.grafts.borrow()[g]
1239                .inner
1240                .adapter()
1241                .link_property(src, label, tgt, name),
1242            (None, None) => self.outer.link_property(source, label, target, name),
1243            _ => None,
1244        }
1245    }
1246}