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(&self, node: NodeId, property: Option<&str>, hint: Option<&str>) -> Option<Vec<NodeId>> {
165 self.0.reverse_resolve(node, property, hint)
166 }
167 fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
168 self.0.descendants_named(node, name)
169 }
170 fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
171 self.0.prefetch_links(nodes, dir)
172 }
173 fn provenance(&self, node: NodeId) -> quarb::Provenance {
174 self.0.provenance(node)
175 }
176 fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
177 self.0.provenance_list(node)
178 }
179 fn allow_shell(&self) -> bool {
180 self.0.allow_shell()
181 }
182}
183
184impl Doc {
185 fn boxed_kaiv(a: quarb_kaiv::KaivAdapter) -> Doc {
189 let a = std::rc::Rc::new(a);
190 let r = a.clone();
191 Doc::Boxed(
192 Dyn(Box::new(quarb_mount::Shared(a))),
193 Box::new(move |n| r.locator(n)),
194 )
195 }
196
197 pub fn parse(input: &str, format: &str) -> Result<Doc> {
201 match format {
202 "kaiv" => {
207 let a = quarb_kaiv::KaivAdapter::parse_kaiv(input)
208 .map_err(|e| anyhow::anyhow!("parsing kaiv: {e}"))?;
209 return Ok(Self::boxed_kaiv(a));
210 }
211 "daiv" => {
212 let a = quarb_kaiv::KaivAdapter::parse_daiv(input)
213 .map_err(|e| anyhow::anyhow!("parsing daiv: {e}"))?;
214 return Ok(Self::boxed_kaiv(a));
215 }
216 "json" => quarb_json::JsonAdapter::parse(input)
217 .map(Doc::Json)
218 .context("parsing JSON"),
219 "jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
220 .map(Doc::Json)
221 .context("parsing JSONL"),
222 "yaml" | "yml" => quarb_yaml::parse(input).map(Doc::Json).context("parsing YAML"),
223 "toml" => quarb_toml::parse(input).map(Doc::Json).context("parsing TOML"),
224 "csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
225 .map(Doc::Csv)
226 .context("parsing CSV"),
227 "tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
228 .map(Doc::Csv)
229 .context("parsing TSV"),
230 "xml" => quarb_xml::XmlAdapter::parse(input)
231 .map(Doc::Xml)
232 .context("parsing XML"),
233 "html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
234 "markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
235 "tex" | "latex" => Ok(Doc::Text(quarb_text_latex::parse(input))),
238 "text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
242 "text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
243 "text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
244 #[cfg(feature = "koine")]
247 "text-bibtex" | "bibtex" => quarb_text_koine::parse_bibtex(input)
248 .map(Doc::Text)
249 .map_err(|e| anyhow::anyhow!("{e}")),
250 #[cfg(feature = "native")]
253 "code-rust" => quarb_code::CodeModel::parse(input, "rs")
254 .map(Doc::Code)
255 .context("parsing Rust at the code level"),
256 #[cfg(feature = "native")]
257 "code-python" => quarb_code::CodeModel::parse(input, "py")
258 .map(Doc::Code)
259 .context("parsing Python at the code level"),
260 #[cfg(feature = "native")]
261 "code-javascript" => quarb_code::CodeModel::parse(input, "js")
262 .map(Doc::Code)
263 .context("parsing JavaScript at the code level"),
264 #[cfg(feature = "native")]
265 "code-c" => quarb_code::CodeModel::parse(input, "c")
266 .map(Doc::Code)
267 .context("parsing C at the code level"),
268 other => bail!("unknown format: {other}"),
269 }
270 }
271
272 pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
281 match self {
282 Doc::Json(a) => a,
283 Doc::Csv(a) => a,
284 Doc::Xml(a) => a,
285 Doc::Html(a) => a,
286 Doc::Text(a) => a,
287 #[cfg(feature = "sqlite")]
288 Doc::Sqlite(a) => a,
289 #[cfg(feature = "native")]
290 Doc::Fs(a) => a,
291 #[cfg(feature = "native")]
292 Doc::FsDeep(a) => a,
293 #[cfg(feature = "native")]
294 Doc::Git(a) => a,
295 #[cfg(feature = "native")]
296 Doc::Archive(a) => a,
297 #[cfg(feature = "native")]
298 Doc::ArchiveRaw(a) => a,
299 #[cfg(feature = "native")]
300 Doc::Xlsx(a) => a,
301 #[cfg(feature = "native")]
302 Doc::Syntax(a) => a,
303 #[cfg(feature = "native")]
304 Doc::Code(a) => a,
305 Doc::Mount(a) => a,
306 Doc::Boxed(a, _) => &*a.0,
307 }
308 }
309
310 pub fn export(
316 &self,
317 query: &str,
318 now: (i64, u32),
319 allow_shell: bool,
320 kind: &str,
321 ) -> Result<String> {
322 let render = quarb_text::Render::from_name(kind)
323 .ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
324 if query.trim().is_empty() {
326 let base = self.base_dyn();
327 return Ok(quarb_text::render_nodes(base, &[base.root()], render));
328 }
329 match self
330 .run(query, now, allow_shell)
331 .map_err(|e| anyhow::anyhow!("{e}"))?
332 {
333 QueryResult::Nodes(nodes) => {
334 Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
335 }
336 QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
337 }
338 }
339
340 pub fn run_modeled(
345 &self,
346 query: &str,
347 now: (i64, u32),
348 allow_shell: bool,
349 model: &quarb_model::Model,
350 ) -> quarb::Result<QueryResult> {
351 let (secs, nanos) = now;
352 let base = quarb_model::Borrowed(self.base_dyn());
353 let nowed = WithNow {
354 inner: &base,
355 secs,
356 nanos,
357 };
358 let enriched = quarb_model::ModelAdapter::new(nowed, model.clone());
359 if allow_shell {
360 quarb::run(query, &AllowShell { inner: &enriched })
361 } else {
362 quarb::run(query, &enriched)
363 }
364 }
365
366 pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
369 let enriched =
370 quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
371 enriched.locator(node, |bn| self.render(bn))
372 }
373
374 pub fn run(&self, query: &str, now: (i64, u32), allow_shell: bool) -> quarb::Result<QueryResult> {
375 let (secs, nanos) = now;
376 macro_rules! go {
377 ($a:expr) => {{
378 let nowed = WithNow {
379 inner: $a,
380 secs,
381 nanos,
382 };
383 if allow_shell {
384 quarb::run(query, &AllowShell { inner: &nowed })
385 } else {
386 quarb::run(query, &nowed)
387 }
388 }};
389 }
390 match self {
391 Doc::Json(a) => go!(a),
392 Doc::Csv(a) => go!(a),
393 Doc::Xml(a) => go!(a),
394 Doc::Html(a) => go!(a),
395 Doc::Text(a) => go!(a),
396 #[cfg(feature = "sqlite")]
397 Doc::Sqlite(a) => go!(a),
398 #[cfg(feature = "native")]
399 Doc::Fs(a) => go!(a),
400 #[cfg(feature = "native")]
401 Doc::FsDeep(a) => go!(a),
402 #[cfg(feature = "native")]
403 Doc::Git(a) => go!(a),
404 #[cfg(feature = "native")]
405 Doc::Archive(a) => go!(a),
406 #[cfg(feature = "native")]
407 Doc::ArchiveRaw(a) => go!(a),
408 #[cfg(feature = "native")]
409 Doc::Xlsx(a) => go!(a),
410 #[cfg(feature = "native")]
411 Doc::Syntax(a) => go!(a),
412 #[cfg(feature = "native")]
413 Doc::Code(a) => go!(a),
414 Doc::Mount(a) => go!(a),
415 Doc::Boxed(a, _) => go!(a),
416 }
417 }
418
419 pub fn render(&self, node: NodeId) -> String {
421 match self {
422 Doc::Json(a) => a.pointer(node),
423 Doc::Csv(a) => a.locator(node),
424 Doc::Xml(a) => a.locator(node),
425 Doc::Html(a) => a.locator(node),
426 Doc::Text(a) => a.locator(node),
427 #[cfg(feature = "sqlite")]
428 Doc::Sqlite(a) => a.locator(node),
429 #[cfg(feature = "native")]
430 Doc::Fs(a) => a.path(node).display().to_string(),
431 #[cfg(feature = "native")]
432 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
433 #[cfg(feature = "native")]
434 Doc::Git(a) => a.locator(node),
435 #[cfg(feature = "native")]
436 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
437 #[cfg(feature = "native")]
438 Doc::ArchiveRaw(a) => a.locator(node),
439 #[cfg(feature = "native")]
440 Doc::Xlsx(a) => a.locator(node),
441 #[cfg(feature = "native")]
442 Doc::Syntax(a) => a.locator(node),
443 #[cfg(feature = "native")]
444 Doc::Code(a) => a.locator(node),
445 Doc::Mount(a) => generic_locator(a, node),
446 Doc::Boxed(_, render) => render(node),
447 }
448 }
449
450 #[cfg(feature = "sqlite")]
453 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
454 Ok(Doc::Sqlite(
455 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
456 .map_err(|e| anyhow::anyhow!("{e}"))
457 .context("opening SQLite bytes")?,
458 ))
459 }
460
461 #[cfg(not(feature = "sqlite"))]
465 pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
466 anyhow::bail!(
467 "this quarb-session was built without the `sqlite` feature"
468 )
469 }
470
471 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
475 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
476 for (name, doc) in parts {
477 if mounts.iter().any(|m| m.name == name) {
478 bail!("two sources mount as '{name}'; give each a distinct name");
479 }
480 mounts.push(quarb_mount::Mount {
481 name,
482 target: None,
485 adapter: doc.into_boxed()?,
486 });
487 }
488 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
489 }
490
491 pub fn attach_url(&mut self, url: &str) {
496 match self {
497 Doc::Html(a) => a.set_document_url(url),
498 Doc::Text(a) => a.set_document_url(url),
499 _ => {}
500 }
501 }
502
503 pub fn url_roots(
509 &self,
510 urls: &[(String, String)],
511 ) -> std::collections::HashMap<String, quarb::NodeId> {
512 use quarb::AstAdapter;
513 let a = self.base_dyn();
514 let root = a.root();
515 match self {
516 Doc::Mount(_) => urls
517 .iter()
518 .filter_map(|(name, url)| {
519 a.children_named(root, name)
520 .first()
521 .map(|&n| (url.clone(), n))
522 })
523 .collect(),
524 _ => urls
525 .iter()
526 .take(1)
527 .map(|(_, url)| (url.clone(), root))
528 .collect(),
529 }
530 }
531
532 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
537 let mut docs: Vec<(String, Doc)> = Vec::new();
538 for (name, format, text) in parts {
539 let doc = Doc::parse(text, format)
540 .with_context(|| format!("parsing '{name}' as {format}"))?;
541 docs.push((name.clone(), doc));
542 }
543 Doc::mount_docs(docs)
544 }
545
546 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
548 use quarb_mount::Shared;
549 Ok(match self {
550 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
551 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
552 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
553 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
554 Doc::Text(a) => Box::new(Shared(Rc::new(a))),
555 #[cfg(feature = "sqlite")]
556 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
557 #[cfg(feature = "native")]
558 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
559 #[cfg(feature = "native")]
560 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
561 #[cfg(feature = "native")]
562 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
563 #[cfg(feature = "native")]
564 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
565 #[cfg(feature = "native")]
566 Doc::ArchiveRaw(a) => Box::new(Shared(Rc::new(a))),
567 #[cfg(feature = "native")]
568 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
569 #[cfg(feature = "native")]
570 Doc::Syntax(a) => Box::new(Shared(Rc::new(a))),
571 #[cfg(feature = "native")]
572 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
573 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
574 Doc::Boxed(a, _) => a.0,
575 })
576 }
577}
578
579#[cfg(feature = "native")]
584impl Doc {
585 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
591 if path.is_dir() {
592 let fsopts = quarb_fs::FsOptions {
593 hidden: opts.hidden,
594 respect_ignore: opts.respect_ignore,
595 };
596 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
597 .with_context(|| format!("opening directory {}", path.display()))?;
598 return Ok(if opts.graft {
599 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
600 fs,
601 |fs, n| Some(fs.path(n)),
602 ))
603 } else {
604 Doc::Fs(fs)
605 });
606 }
607
608 let s = path.to_string_lossy();
609 if let Some(repo) = s.strip_prefix("git:") {
610 let a =
611 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
612 return Ok(Doc::Git(a));
613 }
614 if let Some(rest) = s.strip_prefix("koine:")
619 && !rest.is_empty()
620 {
621 let (path_part, format) = match rest.split_once('?') {
623 Some((p, q)) => {
624 let mut fmt = None;
625 for pair in q.split('&') {
626 match pair.split_once('=') {
627 Some(("format", v)) => fmt = Some(v.to_string()),
628 _ => {
629 return Err(anyhow::anyhow!(
630 "unknown koine option {pair:?} — supported: format="
631 ));
632 }
633 }
634 }
635 (p, fmt)
636 }
637 None => (rest, None),
638 };
639 let target = Path::new(path_part);
640 let ext = target
641 .extension()
642 .and_then(|e| e.to_str())
643 .map(|e| e.to_ascii_lowercase());
644 let read = || -> Result<String> {
645 let text = std::fs::read_to_string(target)
646 .with_context(|| format!("reading {}", target.display()))?;
647 Ok(text
648 .strip_prefix('\u{feff}')
649 .map(str::to_owned)
650 .unwrap_or(text))
651 };
652 if let Some(fmt) = format {
653 let imported = match fmt.as_str() {
654 "atd" | "atk" => {
655 let model =
656 quarb_text_koine::parse_file(target).with_context(|| {
657 format!("reading {} as an atrep document", target.display())
658 })?;
659 return Ok(Doc::Text(model));
660 }
661 "md" | "markdown" => quarb_text_koine::parse_markdown(&read()?),
662 "html" => quarb_text_koine::parse_html(&read()?),
663 "rst" => quarb_text_koine::parse_rst(&read()?),
664 "org" => quarb_text_koine::parse_org(&read()?),
665 "dj" | "djot" => quarb_text_koine::parse_djot(&read()?),
666 "tei" | "docbook" | "jats" | "usx" | "osis" => {
667 quarb_text_koine::parse_xml_as(&read()?, &fmt)
668 }
669 other => {
670 return Err(anyhow::anyhow!(
671 "unknown koine format {other:?} — known: md, html, rst, org, djot, tei, docbook, jats, usx, osis, atd"
672 ));
673 }
674 };
675 return imported.map(Doc::Text).with_context(|| {
676 format!("importing {} through atrep as {fmt}", target.display())
677 });
678 }
679 let imported = match ext.as_deref() {
680 Some("atd" | "atk") => {
681 let model = quarb_text_koine::parse_file(target).with_context(|| {
682 format!("reading {} as an atrep document", target.display())
683 })?;
684 return Ok(Doc::Text(model));
685 }
686 Some("bib") => quarb_text_koine::parse_bibtex(&read()?),
687 Some("md" | "markdown") => quarb_text_koine::parse_markdown(&read()?),
688 Some("html" | "htm") => quarb_text_koine::parse_html(&read()?),
689 Some("rst") => quarb_text_koine::parse_rst(&read()?),
690 Some("org") => quarb_text_koine::parse_org(&read()?),
691 Some("dj" | "djot") => quarb_text_koine::parse_djot(&read()?),
692 Some("xml") => {
693 let text = read()?;
694 match quarb_text_koine::detect_xml_kind(&text) {
695 Some(kind) => quarb_text_koine::parse_xml_as(&text, kind),
696 None => {
697 return Err(anyhow::anyhow!(
698 "{} declares no XML identity this route knows — force one with koine:{}?format=tei|docbook|jats|usx|osis",
699 target.display(),
700 target.display()
701 ));
702 }
703 }
704 }
705 Some("pdf") => {
706 return Err(anyhow::anyhow!(
707 "atrep cannot import a PDF — the print reading is text:{}",
708 target.display()
709 ));
710 }
711 _ => {
712 return Err(anyhow::anyhow!(
713 "no atrep import for this format yet — the native reading is text:{}",
714 target.display()
715 ));
716 }
717 };
718 return imported
719 .map(Doc::Text)
720 .with_context(|| format!("importing {} through atrep", target.display()));
721 }
722 if let Some(rest) = s.strip_prefix("text:")
726 && !rest.is_empty()
727 {
728 let target = Path::new(rest);
729 let ext = target
732 .extension()
733 .and_then(|e| e.to_str())
734 .map(|e| e.to_ascii_lowercase());
735 if let Some("atd" | "atk") = ext.as_deref() {
737 let model = quarb_text_koine::parse_file(target).with_context(|| {
738 format!("reading {} as an atrep document", target.display())
739 })?;
740 return Ok(Doc::Text(model));
741 }
742 if ext.as_deref() == Some("pdf") {
743 let bytes = std::fs::read(target)
744 .with_context(|| format!("reading {}", target.display()))?;
745 let model = quarb_text_pdf::parse(&bytes)
746 .with_context(|| format!("reading {} as PDF", target.display()))?;
747 let a = std::rc::Rc::new(model);
748 let r = a.clone();
749 return Ok(Doc::Boxed(
750 Dyn(Box::new(quarb_mount::Shared(a))),
751 Box::new(move |n| r.locator(n)),
752 ));
753 }
754 if let Some(ext @ ("docx" | "epub")) = ext.as_deref() {
755 let bytes = std::fs::read(target)
756 .with_context(|| format!("reading {}", target.display()))?;
757 let model = if ext == "docx" {
758 quarb_text_docx::parse(&bytes)
759 .with_context(|| format!("reading {} as Word", target.display()))?
760 } else {
761 quarb_text_epub::parse(&bytes)
762 .with_context(|| format!("reading {} as EPUB", target.display()))?
763 };
764 return Ok(Doc::Text(model));
765 }
766 let text = std::fs::read_to_string(target)
767 .with_context(|| format!("reading {}", target.display()))?;
768 let text = text
769 .strip_prefix('\u{feff}')
770 .map(str::to_owned)
771 .unwrap_or(text);
772 let format = match target
773 .extension()
774 .and_then(|e| e.to_str())
775 .map(|e| e.to_ascii_lowercase())
776 .as_deref()
777 {
778 Some("html" | "htm") => "text-html",
779 Some("md" | "markdown") => "text-markdown",
780 Some("tex" | "latex") => "tex",
781 Some("txt") => "text",
782 #[cfg(feature = "koine")]
783 Some("bib") => "text-bibtex",
784 _ if text.trim_start().starts_with('<') => "text-html",
785 _ => "text",
786 };
787 return Doc::parse(&text, format);
788 }
789 if let Some(rest) = s.strip_prefix("code:")
794 && !rest.is_empty()
795 {
796 if opts.no_graft {
797 bail!(
798 "no_graft refuses the code: prefix: the prefix's whole \
799 meaning is the grafted code-level view"
800 );
801 }
802 let target = Path::new(rest);
803 if target.is_dir() {
804 let fsopts = quarb_fs::FsOptions {
805 hidden: opts.hidden,
806 respect_ignore: opts.respect_ignore,
807 };
808 let fs = quarb_fs::FsAdapter::with_options(target, fsopts)
809 .with_context(|| format!("opening directory {}", target.display()))?;
810 return Ok(Doc::FsDeep(
811 quarb_compose::ComposeAdapter::with_source_paths(fs, |fs, n| {
812 Some(fs.path(n))
813 })
814 .with_source_graft(quarb_compose::SourceGraft::Code),
815 ));
816 }
817 let a = quarb_code::CodeModel::open(target)
818 .with_context(|| format!("parsing {} at the code level", target.display()))?;
819 return Ok(Doc::Code(a));
820 }
821
822 let ext = path
823 .extension()
824 .and_then(|e| e.to_str())
825 .map(|e| e.to_ascii_lowercase());
826
827 if let Some(e) = &ext
828 && quarb_tree_sitter::supported(e)
829 {
830 let a = quarb_tree_sitter::TreeSitterAdapter::open(path).context("parsing source file")?;
831 return Ok(Doc::Syntax(a));
832 }
833 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
834 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
835 return Ok(Doc::Xlsx(a));
836 }
837 if is_sqlite(path) {
838 #[cfg(not(feature = "sqlite"))]
842 anyhow::bail!(
843 "{}: this quarb-session was built without the `sqlite` feature",
844 path.display()
845 );
846 #[cfg(feature = "sqlite")]
847 {
848 let a = quarb_sqlite::SqliteAdapter::open_with_refs(path, &opts.refs)
849 .context("opening SQLite database")?;
850 return Ok(Doc::Sqlite(a));
851 }
852 }
853 if is_archive(path) {
854 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
855 return Ok(if opts.no_graft {
856 Doc::ArchiveRaw(a)
857 } else {
858 Doc::Archive(quarb_compose::ComposeAdapter::new(a))
859 });
860 }
861
862 let text = std::fs::read_to_string(path)
864 .with_context(|| format!("reading {}", path.display()))?;
865 let text = text
866 .strip_prefix('\u{feff}')
867 .map(str::to_owned)
868 .unwrap_or(text);
869 match ext.as_deref() {
870 Some("csv") => Doc::parse(&text, "csv"),
871 Some("tsv") => Doc::parse(&text, "tsv"),
872 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
873 Some("toml") => Doc::parse(&text, "toml"),
874 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
875 Some("txt") => Doc::parse(&text, "text"),
876 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
877 _ => {
878 if is_xml(path, &text) {
879 Doc::parse(&text, "xml")
880 } else if is_html(path, &text) {
881 Doc::parse(&text, "html")
882 } else {
883 Doc::parse(&text, "json")
884 }
885 }
886 }
887 }
888
889 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
893 let specs: Vec<crate::MountSpec> = paths
894 .iter()
895 .map(|p| crate::MountSpec {
896 name: None,
897 path: p.clone(),
898 })
899 .collect();
900 Doc::mount_specs(&specs, opts)
901 }
902
903 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
906 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
907 for (i, spec) in specs.iter().enumerate() {
908 let name = spec.name.clone().unwrap_or_else(|| {
909 spec.path
910 .file_stem()
911 .map(|s| s.to_string_lossy().into_owned())
912 .unwrap_or_else(|| format!("doc{i}"))
913 });
914 if mounts.iter().any(|m| m.name == name) {
915 bail!(
916 "input '{}' mounts as '{name}', colliding with an earlier input of the \
917 same name; give each a distinct basename (or a NAME=TARGET alias)",
918 spec.path.display()
919 );
920 }
921 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
922 mounts.push(quarb_mount::Mount {
923 name,
924 target: Some(spec.path.display().to_string()),
925 adapter,
926 });
927 }
928 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
929 }
930
931}
932
933fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
937 let mut parts = Vec::new();
938 let mut cur = Some(node);
939 while let Some(n) = cur {
940 if let Some(nm) = a.name(n) {
941 parts.push(nm);
942 }
943 cur = a.parent(n);
944 }
945 parts.reverse();
946 format!("/{}", parts.join("/"))
947}
948
949#[cfg(feature = "native")]
952fn is_sqlite(path: &Path) -> bool {
953 if path
954 .extension()
955 .and_then(|e| e.to_str())
956 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
957 {
958 return true;
959 }
960 use std::io::Read as _;
961 let mut buf = [0u8; 16];
962 std::fs::File::open(path)
963 .and_then(|mut f| f.read_exact(&mut buf))
964 .is_ok()
965 && &buf == b"SQLite format 3\0"
966}
967
968#[cfg(feature = "native")]
970fn is_archive(path: &Path) -> bool {
971 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
972 matches!(
973 e.to_ascii_lowercase().as_str(),
974 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
975 )
976 }) {
977 return true;
978 }
979 use std::io::Read as _;
980 let mut buf = [0u8; 2];
981 std::fs::File::open(path)
982 .and_then(|mut f| f.read_exact(&mut buf))
983 .is_ok()
984 && (&buf == b"PK" || buf == [0x1f, 0x8b])
985}
986
987#[cfg(feature = "native")]
990fn is_xml(path: &Path, text: &str) -> bool {
991 path.extension()
992 .and_then(|e| e.to_str())
993 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
994 || text.trim_start().starts_with("<?xml")
995}
996
997#[cfg(feature = "native")]
1000fn is_html(path: &Path, text: &str) -> bool {
1001 path.extension()
1002 .and_then(|e| e.to_str())
1003 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
1004 || text.trim_start().starts_with('<')
1005}