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