1use anyhow::{Context, Result, bail};
17use quarb::{AllowShell, NodeId, QueryResult, WithNow};
18
19#[cfg(feature = "native")]
20use std::path::Path;
21use std::rc::Rc;
22
23#[derive(Clone, Default)]
26pub struct Options {
27 pub hidden: bool,
28 pub respect_ignore: bool,
29 pub graft: bool,
31 pub no_graft: bool,
33 pub refs: Rc<Vec<(String, String)>>,
36}
37
38pub enum Doc {
42 Json(quarb_json::JsonAdapter),
43 Csv(quarb_csv::CsvAdapter),
44 Xml(quarb_xml::XmlAdapter),
45 Html(quarb_html::HtmlAdapter),
46 Text(quarb_text::TextModel),
47 #[cfg(feature = "sqlite")]
48 Sqlite(quarb_sqlite::SqliteAdapter),
49 #[cfg(feature = "native")]
50 Fs(quarb_fs::FsAdapter),
51 #[cfg(feature = "native")]
52 FsDeep(quarb_compose::ComposeAdapter<quarb_fs::FsAdapter>),
53 #[cfg(feature = "native")]
54 Git(quarb_git::GitAdapter),
55 #[cfg(feature = "native")]
56 Archive(quarb_compose::ComposeAdapter<quarb_archive::ArchiveAdapter>),
57 #[cfg(feature = "native")]
60 ArchiveRaw(quarb_archive::ArchiveAdapter),
61 #[cfg(feature = "native")]
62 Xlsx(quarb_xlsx::XlsxAdapter),
63 #[cfg(feature = "native")]
64 Syntax(quarb_tree_sitter::TreeSitterAdapter),
65 #[cfg(feature = "native")]
66 Code(quarb_code::CodeModel),
67 Mount(quarb_mount::MountAdapter),
68 Boxed(Dyn, Box<dyn Fn(NodeId) -> String>),
72}
73
74pub struct Dyn(pub Box<dyn quarb::AstAdapter>);
77
78impl quarb::AstAdapter for Dyn {
79 fn root(&self) -> NodeId {
80 self.0.root()
81 }
82 fn children(&self, node: NodeId) -> Vec<NodeId> {
83 self.0.children(node)
84 }
85 fn name(&self, node: NodeId) -> Option<String> {
86 self.0.name(node)
87 }
88 fn parent(&self, node: NodeId) -> Option<NodeId> {
89 self.0.parent(node)
90 }
91 fn traits(&self, node: NodeId) -> Vec<String> {
92 self.0.traits(node)
93 }
94 fn property(&self, node: NodeId, name: &str) -> Option<quarb::Value> {
95 self.0.property(node, name)
96 }
97 fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
98 self.0.children_named(node, name)
99 }
100 fn default_value(&self, node: NodeId) -> Option<quarb::Value> {
101 self.0.default_value(node)
102 }
103 fn metadata(&self, node: NodeId, key: &str) -> Option<quarb::Value> {
104 self.0.metadata(node, key)
105 }
106 fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
107 self.0.aliased_metadata(node)
108 }
109 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
110 self.0.links(node)
111 }
112 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
113 self.0.backlinks(node)
114 }
115 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
116 self.0.resolve(node, property, hint)
117 }
118 fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
119 self.0.external_ref(node, property, hint)
120 }
121 fn resolve_fragment(&self, node: NodeId, fragment: &str) -> Option<NodeId> {
122 self.0.resolve_fragment(node, fragment)
123 }
124 fn ref_property(&self, node: NodeId) -> Option<String> {
125 self.0.ref_property(node)
126 }
127 fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
128 self.0.ref_label(node, property)
129 }
130 fn link_property(
131 &self,
132 source: NodeId,
133 label: &str,
134 target: NodeId,
135 name: &str,
136 ) -> Option<quarb::Value> {
137 self.0.link_property(source, label, target, name)
138 }
139 fn quantifier_bound(&self) -> usize {
140 self.0.quantifier_bound()
141 }
142 fn invocation_instant(&self) -> Option<(i64, u32)> {
143 self.0.invocation_instant()
144 }
145 fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
146 self.0.unit_scale(expr)
147 }
148 fn answers_to(&self, node: NodeId, name: &str) -> bool {
153 self.0.answers_to(node, name)
154 }
155 fn has_trait(&self, node: NodeId, name: &str) -> bool {
156 self.0.has_trait(node, name)
157 }
158 fn has_node(&self, node: NodeId) -> bool {
159 self.0.has_node(node)
160 }
161 fn document_by_ref(&self, id: &str) -> Option<NodeId> {
162 self.0.document_by_ref(id)
163 }
164 fn reverse_resolve(
165 &self,
166 node: NodeId,
167 property: Option<&str>,
168 hint: Option<&str>,
169 ) -> Option<Vec<NodeId>> {
170 self.0.reverse_resolve(node, property, hint)
171 }
172 fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
173 self.0.descendants_named(node, name)
174 }
175 fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
176 self.0.prefetch_links(nodes, dir)
177 }
178 fn provenance(&self, node: NodeId) -> quarb::Provenance {
179 self.0.provenance(node)
180 }
181 fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
182 self.0.provenance_list(node)
183 }
184 fn allow_shell(&self) -> bool {
185 self.0.allow_shell()
186 }
187}
188
189impl Doc {
190 fn boxed_kaiv(a: quarb_kaiv::KaivAdapter) -> Doc {
194 let a = std::rc::Rc::new(a);
195 let r = a.clone();
196 Doc::Boxed(
197 Dyn(Box::new(quarb_mount::Shared(a))),
198 Box::new(move |n| r.locator(n)),
199 )
200 }
201
202 pub fn parse(input: &str, format: &str) -> Result<Doc> {
206 match format {
207 "kaiv" => {
212 let a = quarb_kaiv::KaivAdapter::parse_kaiv(input)
213 .map_err(|e| anyhow::anyhow!("parsing kaiv: {e}"))?;
214 return Ok(Self::boxed_kaiv(a));
215 }
216 "daiv" => {
217 let a = quarb_kaiv::KaivAdapter::parse_daiv(input)
218 .map_err(|e| anyhow::anyhow!("parsing daiv: {e}"))?;
219 return Ok(Self::boxed_kaiv(a));
220 }
221 "json" => quarb_json::JsonAdapter::parse(input)
222 .map(Doc::Json)
223 .context("parsing JSON"),
224 "jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
225 .map(Doc::Json)
226 .context("parsing JSONL"),
227 "yaml" | "yml" => quarb_yaml::parse(input)
228 .map(Doc::Json)
229 .context("parsing YAML"),
230 "toml" => quarb_toml::parse(input)
231 .map(Doc::Json)
232 .context("parsing TOML"),
233 "csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
234 .map(Doc::Csv)
235 .context("parsing CSV"),
236 "tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
237 .map(Doc::Csv)
238 .context("parsing TSV"),
239 "xml" => quarb_xml::XmlAdapter::parse(input)
240 .map(Doc::Xml)
241 .context("parsing XML"),
242 "html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
243 "markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
244 "tex" | "latex" => Ok(Doc::Text(quarb_text_latex::parse(input))),
247 "text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
251 "text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
252 "text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
253 #[cfg(feature = "koine")]
256 "text-bibtex" | "bibtex" => quarb_text_koine::parse_bibtex(input)
257 .map(Doc::Text)
258 .map_err(|e| anyhow::anyhow!("{e}")),
259 #[cfg(feature = "native")]
262 "code-rust" => quarb_code::CodeModel::parse(input, "rs")
263 .map(Doc::Code)
264 .context("parsing Rust at the code level"),
265 #[cfg(feature = "native")]
266 "code-python" => quarb_code::CodeModel::parse(input, "py")
267 .map(Doc::Code)
268 .context("parsing Python at the code level"),
269 #[cfg(feature = "native")]
270 "code-javascript" => quarb_code::CodeModel::parse(input, "js")
271 .map(Doc::Code)
272 .context("parsing JavaScript at the code level"),
273 #[cfg(feature = "native")]
274 "code-c" => quarb_code::CodeModel::parse(input, "c")
275 .map(Doc::Code)
276 .context("parsing C at the code level"),
277 other => bail!("unknown format: {other}"),
278 }
279 }
280
281 pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
290 match self {
291 Doc::Json(a) => a,
292 Doc::Csv(a) => a,
293 Doc::Xml(a) => a,
294 Doc::Html(a) => a,
295 Doc::Text(a) => a,
296 #[cfg(feature = "sqlite")]
297 Doc::Sqlite(a) => a,
298 #[cfg(feature = "native")]
299 Doc::Fs(a) => a,
300 #[cfg(feature = "native")]
301 Doc::FsDeep(a) => a,
302 #[cfg(feature = "native")]
303 Doc::Git(a) => a,
304 #[cfg(feature = "native")]
305 Doc::Archive(a) => a,
306 #[cfg(feature = "native")]
307 Doc::ArchiveRaw(a) => a,
308 #[cfg(feature = "native")]
309 Doc::Xlsx(a) => a,
310 #[cfg(feature = "native")]
311 Doc::Syntax(a) => a,
312 #[cfg(feature = "native")]
313 Doc::Code(a) => a,
314 Doc::Mount(a) => a,
315 Doc::Boxed(a, _) => &*a.0,
316 }
317 }
318
319 pub fn export(
325 &self,
326 query: &str,
327 now: (i64, u32),
328 allow_shell: bool,
329 kind: &str,
330 ) -> Result<String> {
331 let render = quarb_text::Render::from_name(kind)
332 .ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
333 if query.trim().is_empty() {
335 let base = self.base_dyn();
336 return Ok(quarb_text::render_nodes(base, &[base.root()], render));
337 }
338 match self
339 .run(query, now, allow_shell)
340 .map_err(|e| anyhow::anyhow!("{e}"))?
341 {
342 QueryResult::Nodes(nodes) => {
343 Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
344 }
345 QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
346 }
347 }
348
349 pub fn run_modeled(
354 &self,
355 query: &str,
356 now: (i64, u32),
357 allow_shell: bool,
358 model: &quarb_model::Model,
359 ) -> quarb::Result<QueryResult> {
360 let (secs, nanos) = now;
361 let base = quarb_model::Borrowed(self.base_dyn());
362 let nowed = WithNow {
363 inner: &base,
364 secs,
365 nanos,
366 };
367 let enriched = quarb_model::ModelAdapter::new(nowed, model.clone());
368 if allow_shell {
369 quarb::run(query, &AllowShell { inner: &enriched })
370 } else {
371 quarb::run(query, &enriched)
372 }
373 }
374
375 pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
378 let enriched =
379 quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
380 enriched.locator(node, |bn| self.render(bn))
381 }
382
383 pub fn run(
384 &self,
385 query: &str,
386 now: (i64, u32),
387 allow_shell: bool,
388 ) -> quarb::Result<QueryResult> {
389 let (secs, nanos) = now;
390 macro_rules! go {
391 ($a:expr) => {{
392 let nowed = WithNow {
393 inner: $a,
394 secs,
395 nanos,
396 };
397 if allow_shell {
398 quarb::run(query, &AllowShell { inner: &nowed })
399 } else {
400 quarb::run(query, &nowed)
401 }
402 }};
403 }
404 match self {
405 Doc::Json(a) => go!(a),
406 Doc::Csv(a) => go!(a),
407 Doc::Xml(a) => go!(a),
408 Doc::Html(a) => go!(a),
409 Doc::Text(a) => go!(a),
410 #[cfg(feature = "sqlite")]
411 Doc::Sqlite(a) => go!(a),
412 #[cfg(feature = "native")]
413 Doc::Fs(a) => go!(a),
414 #[cfg(feature = "native")]
415 Doc::FsDeep(a) => go!(a),
416 #[cfg(feature = "native")]
417 Doc::Git(a) => go!(a),
418 #[cfg(feature = "native")]
419 Doc::Archive(a) => go!(a),
420 #[cfg(feature = "native")]
421 Doc::ArchiveRaw(a) => go!(a),
422 #[cfg(feature = "native")]
423 Doc::Xlsx(a) => go!(a),
424 #[cfg(feature = "native")]
425 Doc::Syntax(a) => go!(a),
426 #[cfg(feature = "native")]
427 Doc::Code(a) => go!(a),
428 Doc::Mount(a) => go!(a),
429 Doc::Boxed(a, _) => go!(a),
430 }
431 }
432
433 pub fn family(&self) -> &'static str {
437 match self {
438 Doc::Json(_) => "json",
439 Doc::Csv(_) => "csv",
440 Doc::Xml(_) => "xml",
441 Doc::Html(_) => "html",
442 Doc::Text(_) => "text",
443 #[cfg(feature = "sqlite")]
444 Doc::Sqlite(_) => "sqlite",
445 #[cfg(feature = "native")]
446 Doc::Fs(_) | Doc::FsDeep(_) => "fs",
447 #[cfg(feature = "native")]
448 Doc::Git(_) => "git",
449 #[cfg(feature = "native")]
450 Doc::Archive(_) | Doc::ArchiveRaw(_) => "archive",
451 #[cfg(feature = "native")]
452 Doc::Xlsx(_) => "xlsx",
453 #[cfg(feature = "native")]
454 Doc::Syntax(_) => "tree-sitter",
455 #[cfg(feature = "native")]
456 Doc::Code(_) => "code",
457 Doc::Mount(_) => "mount",
458 Doc::Boxed(..) => "boxed",
459 }
460 }
461
462 pub fn trace(
467 &self,
468 committed: &str,
469 staged: Option<&str>,
470 opts: &quarb::trace::Options,
471 ) -> quarb::Result<quarb_trace::Payload> {
472 let base = quarb_model::Borrowed(self.base_dyn());
473 quarb::trace::trace(committed, staged, &base, &|n| self.render(n), opts)
474 }
475
476 pub fn render(&self, node: NodeId) -> String {
477 match self {
478 Doc::Json(a) => a.pointer(node),
479 Doc::Csv(a) => a.locator(node),
480 Doc::Xml(a) => a.locator(node),
481 Doc::Html(a) => a.locator(node),
482 Doc::Text(a) => a.locator(node),
483 #[cfg(feature = "sqlite")]
484 Doc::Sqlite(a) => a.locator(node),
485 #[cfg(feature = "native")]
486 Doc::Fs(a) => a.path(node).display().to_string(),
487 #[cfg(feature = "native")]
488 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
489 #[cfg(feature = "native")]
490 Doc::Git(a) => a.locator(node),
491 #[cfg(feature = "native")]
492 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
493 #[cfg(feature = "native")]
494 Doc::ArchiveRaw(a) => a.locator(node),
495 #[cfg(feature = "native")]
496 Doc::Xlsx(a) => a.locator(node),
497 #[cfg(feature = "native")]
498 Doc::Syntax(a) => a.locator(node),
499 #[cfg(feature = "native")]
500 Doc::Code(a) => a.locator(node),
501 Doc::Mount(a) => generic_locator(a, node),
502 Doc::Boxed(_, render) => render(node),
503 }
504 }
505
506 #[cfg(feature = "sqlite")]
509 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
510 Ok(Doc::Sqlite(
511 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
512 .map_err(|e| anyhow::anyhow!("{e}"))
513 .context("opening SQLite bytes")?,
514 ))
515 }
516
517 #[cfg(not(feature = "sqlite"))]
521 pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
522 anyhow::bail!("this quarb-session was built without the `sqlite` feature")
523 }
524
525 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
529 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
530 for (name, doc) in parts {
531 if mounts.iter().any(|m| m.name == name) {
532 bail!("two sources mount as '{name}'; give each a distinct name");
533 }
534 mounts.push(quarb_mount::Mount {
535 name,
536 target: None,
539 adapter: doc.into_boxed()?,
540 });
541 }
542 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
543 }
544
545 pub fn attach_url(&mut self, url: &str) {
550 match self {
551 Doc::Html(a) => a.set_document_url(url),
552 Doc::Text(a) => a.set_document_url(url),
553 _ => {}
554 }
555 }
556
557 pub fn url_roots(
563 &self,
564 urls: &[(String, String)],
565 ) -> std::collections::HashMap<String, quarb::NodeId> {
566 use quarb::AstAdapter;
567 let a = self.base_dyn();
568 let root = a.root();
569 match self {
570 Doc::Mount(_) => urls
571 .iter()
572 .filter_map(|(name, url)| {
573 a.children_named(root, name)
574 .first()
575 .map(|&n| (url.clone(), n))
576 })
577 .collect(),
578 _ => urls
579 .iter()
580 .take(1)
581 .map(|(_, url)| (url.clone(), root))
582 .collect(),
583 }
584 }
585
586 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
591 let mut docs: Vec<(String, Doc)> = Vec::new();
592 for (name, format, text) in parts {
593 let doc = Doc::parse(text, format)
594 .with_context(|| format!("parsing '{name}' as {format}"))?;
595 docs.push((name.clone(), doc));
596 }
597 Doc::mount_docs(docs)
598 }
599
600 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
602 use quarb_mount::Shared;
603 Ok(match self {
604 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
605 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
606 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
607 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
608 Doc::Text(a) => Box::new(Shared(Rc::new(a))),
609 #[cfg(feature = "sqlite")]
610 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
611 #[cfg(feature = "native")]
612 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
613 #[cfg(feature = "native")]
614 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
615 #[cfg(feature = "native")]
616 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
617 #[cfg(feature = "native")]
618 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
619 #[cfg(feature = "native")]
620 Doc::ArchiveRaw(a) => Box::new(Shared(Rc::new(a))),
621 #[cfg(feature = "native")]
622 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
623 #[cfg(feature = "native")]
624 Doc::Syntax(a) => Box::new(Shared(Rc::new(a))),
625 #[cfg(feature = "native")]
626 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
627 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
628 Doc::Boxed(a, _) => a.0,
629 })
630 }
631}
632
633#[cfg(feature = "native")]
638impl Doc {
639 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
645 if path.is_dir() {
646 let fsopts = quarb_fs::FsOptions {
647 hidden: opts.hidden,
648 respect_ignore: opts.respect_ignore,
649 };
650 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
651 .with_context(|| format!("opening directory {}", path.display()))?;
652 return Ok(if opts.graft {
653 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
654 fs,
655 |fs, n| Some(fs.path(n)),
656 ))
657 } else {
658 Doc::Fs(fs)
659 });
660 }
661
662 let s = path.to_string_lossy();
663 if let Some(repo) = s.strip_prefix("git:") {
664 let a =
665 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
666 return Ok(Doc::Git(a));
667 }
668 if let Some(rest) = s.strip_prefix("koine:")
673 && !rest.is_empty()
674 {
675 let (path_part, format) = match rest.split_once('?') {
677 Some((p, q)) => {
678 let mut fmt = None;
679 for pair in q.split('&') {
680 match pair.split_once('=') {
681 Some(("format", v)) => fmt = Some(v.to_string()),
682 _ => {
683 return Err(anyhow::anyhow!(
684 "unknown koine option {pair:?} — supported: format="
685 ));
686 }
687 }
688 }
689 (p, fmt)
690 }
691 None => (rest, None),
692 };
693 let target = Path::new(path_part);
694 let ext = target
695 .extension()
696 .and_then(|e| e.to_str())
697 .map(|e| e.to_ascii_lowercase());
698 let read = || -> Result<String> {
699 let text = std::fs::read_to_string(target)
700 .with_context(|| format!("reading {}", target.display()))?;
701 Ok(text
702 .strip_prefix('\u{feff}')
703 .map(str::to_owned)
704 .unwrap_or(text))
705 };
706 if let Some(fmt) = format {
707 let imported = match fmt.as_str() {
708 "atd" | "atk" => {
709 let model = quarb_text_koine::parse_file(target).with_context(|| {
710 format!("reading {} as an atrep document", target.display())
711 })?;
712 return Ok(Doc::Text(model));
713 }
714 "md" | "markdown" => quarb_text_koine::parse_markdown(&read()?),
715 "html" => quarb_text_koine::parse_html(&read()?),
716 "rst" => quarb_text_koine::parse_rst(&read()?),
717 "org" => quarb_text_koine::parse_org(&read()?),
718 "dj" | "djot" => quarb_text_koine::parse_djot(&read()?),
719 "tei" | "docbook" | "jats" | "usx" | "osis" => {
720 quarb_text_koine::parse_xml_as(&read()?, &fmt)
721 }
722 other => {
723 return Err(anyhow::anyhow!(
724 "unknown koine format {other:?} — known: md, html, rst, org, djot, tei, docbook, jats, usx, osis, atd"
725 ));
726 }
727 };
728 return imported.map(Doc::Text).with_context(|| {
729 format!("importing {} through atrep as {fmt}", target.display())
730 });
731 }
732 let imported = match ext.as_deref() {
733 Some("atd" | "atk") => {
734 let model = quarb_text_koine::parse_file(target).with_context(|| {
735 format!("reading {} as an atrep document", target.display())
736 })?;
737 return Ok(Doc::Text(model));
738 }
739 Some("bib") => quarb_text_koine::parse_bibtex(&read()?),
740 Some("md" | "markdown") => quarb_text_koine::parse_markdown(&read()?),
741 Some("html" | "htm") => quarb_text_koine::parse_html(&read()?),
742 Some("rst") => quarb_text_koine::parse_rst(&read()?),
743 Some("org") => quarb_text_koine::parse_org(&read()?),
744 Some("dj" | "djot") => quarb_text_koine::parse_djot(&read()?),
745 Some("xml") => {
746 let text = read()?;
747 match quarb_text_koine::detect_xml_kind(&text) {
748 Some(kind) => quarb_text_koine::parse_xml_as(&text, kind),
749 None => {
750 return Err(anyhow::anyhow!(
751 "{} declares no XML identity this route knows — force one with koine:{}?format=tei|docbook|jats|usx|osis",
752 target.display(),
753 target.display()
754 ));
755 }
756 }
757 }
758 Some("pdf") => {
759 return Err(anyhow::anyhow!(
760 "atrep cannot import a PDF — the print reading is text:{}",
761 target.display()
762 ));
763 }
764 _ => {
765 return Err(anyhow::anyhow!(
766 "no atrep import for this format yet — the native reading is text:{}",
767 target.display()
768 ));
769 }
770 };
771 return imported
772 .map(Doc::Text)
773 .with_context(|| format!("importing {} through atrep", target.display()));
774 }
775 if let Some(rest) = s.strip_prefix("text:")
779 && !rest.is_empty()
780 {
781 let target = Path::new(rest);
782 let ext = target
785 .extension()
786 .and_then(|e| e.to_str())
787 .map(|e| e.to_ascii_lowercase());
788 if let Some("atd" | "atk") = ext.as_deref() {
790 let model = quarb_text_koine::parse_file(target).with_context(|| {
791 format!("reading {} as an atrep document", target.display())
792 })?;
793 return Ok(Doc::Text(model));
794 }
795 if ext.as_deref() == Some("pdf") {
796 let bytes = std::fs::read(target)
797 .with_context(|| format!("reading {}", target.display()))?;
798 let model = quarb_text_pdf::parse(&bytes)
799 .with_context(|| format!("reading {} as PDF", target.display()))?;
800 let a = std::rc::Rc::new(model);
801 let r = a.clone();
802 return Ok(Doc::Boxed(
803 Dyn(Box::new(quarb_mount::Shared(a))),
804 Box::new(move |n| r.locator(n)),
805 ));
806 }
807 if let Some(ext @ ("docx" | "epub")) = ext.as_deref() {
808 let bytes = std::fs::read(target)
809 .with_context(|| format!("reading {}", target.display()))?;
810 let model = if ext == "docx" {
811 quarb_text_docx::parse(&bytes)
812 .with_context(|| format!("reading {} as Word", target.display()))?
813 } else {
814 quarb_text_epub::parse(&bytes)
815 .with_context(|| format!("reading {} as EPUB", target.display()))?
816 };
817 return Ok(Doc::Text(model));
818 }
819 let text = std::fs::read_to_string(target)
820 .with_context(|| format!("reading {}", target.display()))?;
821 let text = text
822 .strip_prefix('\u{feff}')
823 .map(str::to_owned)
824 .unwrap_or(text);
825 let format = match target
826 .extension()
827 .and_then(|e| e.to_str())
828 .map(|e| e.to_ascii_lowercase())
829 .as_deref()
830 {
831 Some("html" | "htm") => "text-html",
832 Some("md" | "markdown") => "text-markdown",
833 Some("tex" | "latex") => "tex",
834 Some("txt") => "text",
835 #[cfg(feature = "koine")]
836 Some("bib") => "text-bibtex",
837 _ if text.trim_start().starts_with('<') => "text-html",
838 _ => "text",
839 };
840 return Doc::parse(&text, format);
841 }
842 if let Some(rest) = s.strip_prefix("code:")
847 && !rest.is_empty()
848 {
849 if opts.no_graft {
850 bail!(
851 "no_graft refuses the code: prefix: the prefix's whole \
852 meaning is the grafted code-level view"
853 );
854 }
855 let target = Path::new(rest);
856 if target.is_dir() {
857 let fsopts = quarb_fs::FsOptions {
858 hidden: opts.hidden,
859 respect_ignore: opts.respect_ignore,
860 };
861 let fs = quarb_fs::FsAdapter::with_options(target, fsopts)
862 .with_context(|| format!("opening directory {}", target.display()))?;
863 return Ok(Doc::FsDeep(
864 quarb_compose::ComposeAdapter::with_source_paths(fs, |fs, n| Some(fs.path(n)))
865 .with_source_graft(quarb_compose::SourceGraft::Code),
866 ));
867 }
868 let a = quarb_code::CodeModel::open(target)
869 .with_context(|| format!("parsing {} at the code level", target.display()))?;
870 return Ok(Doc::Code(a));
871 }
872
873 let ext = path
874 .extension()
875 .and_then(|e| e.to_str())
876 .map(|e| e.to_ascii_lowercase());
877
878 if let Some(e) = &ext
879 && quarb_tree_sitter::supported(e)
880 {
881 let a =
882 quarb_tree_sitter::TreeSitterAdapter::open(path).context("parsing source file")?;
883 return Ok(Doc::Syntax(a));
884 }
885 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
886 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
887 return Ok(Doc::Xlsx(a));
888 }
889 if is_sqlite(path) {
890 #[cfg(not(feature = "sqlite"))]
894 anyhow::bail!(
895 "{}: this quarb-session was built without the `sqlite` feature",
896 path.display()
897 );
898 #[cfg(feature = "sqlite")]
899 {
900 let a = quarb_sqlite::SqliteAdapter::open_with_refs(path, &opts.refs)
901 .context("opening SQLite database")?;
902 return Ok(Doc::Sqlite(a));
903 }
904 }
905 if is_archive(path) {
906 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
907 return Ok(if opts.no_graft {
908 Doc::ArchiveRaw(a)
909 } else {
910 Doc::Archive(quarb_compose::ComposeAdapter::new(a))
911 });
912 }
913
914 let text =
916 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
917 let text = text
918 .strip_prefix('\u{feff}')
919 .map(str::to_owned)
920 .unwrap_or(text);
921 match ext.as_deref() {
922 Some("csv") => Doc::parse(&text, "csv"),
923 Some("tsv") => Doc::parse(&text, "tsv"),
924 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
925 Some("toml") => Doc::parse(&text, "toml"),
926 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
927 Some("txt") => Doc::parse(&text, "text"),
928 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
929 _ => {
930 if is_xml(path, &text) {
931 Doc::parse(&text, "xml")
932 } else if is_html(path, &text) {
933 Doc::parse(&text, "html")
934 } else {
935 Doc::parse(&text, "json")
936 }
937 }
938 }
939 }
940
941 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
945 let specs: Vec<crate::MountSpec> = paths
946 .iter()
947 .map(|p| crate::MountSpec {
948 name: None,
949 path: p.clone(),
950 })
951 .collect();
952 Doc::mount_specs(&specs, opts)
953 }
954
955 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
958 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
959 for (i, spec) in specs.iter().enumerate() {
960 let name = spec.name.clone().unwrap_or_else(|| {
961 spec.path
962 .file_stem()
963 .map(|s| s.to_string_lossy().into_owned())
964 .unwrap_or_else(|| format!("doc{i}"))
965 });
966 if mounts.iter().any(|m| m.name == name) {
967 bail!(
968 "input '{}' mounts as '{name}', colliding with an earlier input of the \
969 same name; give each a distinct basename (or a NAME=TARGET alias)",
970 spec.path.display()
971 );
972 }
973 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
974 mounts.push(quarb_mount::Mount {
975 name,
976 target: Some(spec.path.display().to_string()),
977 adapter,
978 });
979 }
980 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
981 }
982}
983
984fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
988 let mut parts = Vec::new();
989 let mut cur = Some(node);
990 while let Some(n) = cur {
991 if let Some(nm) = a.name(n) {
992 parts.push(nm);
993 }
994 cur = a.parent(n);
995 }
996 parts.reverse();
997 format!("/{}", parts.join("/"))
998}
999
1000#[cfg(feature = "native")]
1003fn is_sqlite(path: &Path) -> bool {
1004 if path
1005 .extension()
1006 .and_then(|e| e.to_str())
1007 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
1008 {
1009 return true;
1010 }
1011 use std::io::Read as _;
1012 let mut buf = [0u8; 16];
1013 std::fs::File::open(path)
1014 .and_then(|mut f| f.read_exact(&mut buf))
1015 .is_ok()
1016 && &buf == b"SQLite format 3\0"
1017}
1018
1019#[cfg(feature = "native")]
1021fn is_archive(path: &Path) -> bool {
1022 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
1023 matches!(
1024 e.to_ascii_lowercase().as_str(),
1025 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
1026 )
1027 }) {
1028 return true;
1029 }
1030 use std::io::Read as _;
1031 let mut buf = [0u8; 2];
1032 std::fs::File::open(path)
1033 .and_then(|mut f| f.read_exact(&mut buf))
1034 .is_ok()
1035 && (&buf == b"PK" || buf == [0x1f, 0x8b])
1036}
1037
1038#[cfg(feature = "native")]
1041fn is_xml(path: &Path, text: &str) -> bool {
1042 path.extension()
1043 .and_then(|e| e.to_str())
1044 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
1045 || text.trim_start().starts_with("<?xml")
1046}
1047
1048#[cfg(feature = "native")]
1051fn is_html(path: &Path, text: &str) -> bool {
1052 path.extension()
1053 .and_then(|e| e.to_str())
1054 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
1055 || text.trim_start().starts_with('<')
1056}