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 link_property(
119 &self,
120 source: NodeId,
121 label: &str,
122 target: NodeId,
123 name: &str,
124 ) -> Option<quarb::Value> {
125 self.0.link_property(source, label, target, name)
126 }
127 fn quantifier_bound(&self) -> usize {
128 self.0.quantifier_bound()
129 }
130 fn invocation_instant(&self) -> Option<(i64, u32)> {
131 self.0.invocation_instant()
132 }
133 fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
134 self.0.unit_scale(expr)
135 }
136}
137
138impl Doc {
139 fn boxed_kaiv(a: quarb_kaiv::KaivAdapter) -> Doc {
143 let a = std::rc::Rc::new(a);
144 let r = a.clone();
145 Doc::Boxed(
146 Dyn(Box::new(quarb_mount::Shared(a))),
147 Box::new(move |n| r.locator(n)),
148 )
149 }
150
151 pub fn parse(input: &str, format: &str) -> Result<Doc> {
155 match format {
156 "kaiv" => {
161 let a = quarb_kaiv::KaivAdapter::parse_kaiv(input)
162 .map_err(|e| anyhow::anyhow!("parsing kaiv: {e}"))?;
163 return Ok(Self::boxed_kaiv(a));
164 }
165 "daiv" => {
166 let a = quarb_kaiv::KaivAdapter::parse_daiv(input)
167 .map_err(|e| anyhow::anyhow!("parsing daiv: {e}"))?;
168 return Ok(Self::boxed_kaiv(a));
169 }
170 "json" => quarb_json::JsonAdapter::parse(input)
171 .map(Doc::Json)
172 .context("parsing JSON"),
173 "jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
174 .map(Doc::Json)
175 .context("parsing JSONL"),
176 "yaml" | "yml" => quarb_yaml::parse(input).map(Doc::Json).context("parsing YAML"),
177 "toml" => quarb_toml::parse(input).map(Doc::Json).context("parsing TOML"),
178 "csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
179 .map(Doc::Csv)
180 .context("parsing CSV"),
181 "tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
182 .map(Doc::Csv)
183 .context("parsing TSV"),
184 "xml" => quarb_xml::XmlAdapter::parse(input)
185 .map(Doc::Xml)
186 .context("parsing XML"),
187 "html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
188 "markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
189 "tex" | "latex" => Ok(Doc::Text(quarb_text_latex::parse(input))),
192 "text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
196 "text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
197 "text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
198 #[cfg(feature = "native")]
201 "code-rust" => quarb_code::CodeModel::parse(input, "rs")
202 .map(Doc::Code)
203 .context("parsing Rust at the code level"),
204 #[cfg(feature = "native")]
205 "code-python" => quarb_code::CodeModel::parse(input, "py")
206 .map(Doc::Code)
207 .context("parsing Python at the code level"),
208 #[cfg(feature = "native")]
209 "code-javascript" => quarb_code::CodeModel::parse(input, "js")
210 .map(Doc::Code)
211 .context("parsing JavaScript at the code level"),
212 #[cfg(feature = "native")]
213 "code-c" => quarb_code::CodeModel::parse(input, "c")
214 .map(Doc::Code)
215 .context("parsing C at the code level"),
216 other => bail!("unknown format: {other}"),
217 }
218 }
219
220 pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
229 match self {
230 Doc::Json(a) => a,
231 Doc::Csv(a) => a,
232 Doc::Xml(a) => a,
233 Doc::Html(a) => a,
234 Doc::Text(a) => a,
235 #[cfg(feature = "sqlite")]
236 Doc::Sqlite(a) => a,
237 #[cfg(feature = "native")]
238 Doc::Fs(a) => a,
239 #[cfg(feature = "native")]
240 Doc::FsDeep(a) => a,
241 #[cfg(feature = "native")]
242 Doc::Git(a) => a,
243 #[cfg(feature = "native")]
244 Doc::Archive(a) => a,
245 #[cfg(feature = "native")]
246 Doc::ArchiveRaw(a) => a,
247 #[cfg(feature = "native")]
248 Doc::Xlsx(a) => a,
249 #[cfg(feature = "native")]
250 Doc::Syntax(a) => a,
251 #[cfg(feature = "native")]
252 Doc::Code(a) => a,
253 Doc::Mount(a) => a,
254 Doc::Boxed(a, _) => &*a.0,
255 }
256 }
257
258 pub fn export(
264 &self,
265 query: &str,
266 now: (i64, u32),
267 allow_shell: bool,
268 kind: &str,
269 ) -> Result<String> {
270 let render = quarb_text::Render::from_name(kind)
271 .ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
272 if query.trim().is_empty() {
274 let base = self.base_dyn();
275 return Ok(quarb_text::render_nodes(base, &[base.root()], render));
276 }
277 match self
278 .run(query, now, allow_shell)
279 .map_err(|e| anyhow::anyhow!("{e}"))?
280 {
281 QueryResult::Nodes(nodes) => {
282 Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
283 }
284 QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
285 }
286 }
287
288 pub fn run_modeled(
293 &self,
294 query: &str,
295 now: (i64, u32),
296 allow_shell: bool,
297 model: &quarb_model::Model,
298 ) -> quarb::Result<QueryResult> {
299 let (secs, nanos) = now;
300 let base = quarb_model::Borrowed(self.base_dyn());
301 let nowed = WithNow {
302 inner: &base,
303 secs,
304 nanos,
305 };
306 let enriched = quarb_model::ModelAdapter::new(nowed, model.clone());
307 if allow_shell {
308 quarb::run(query, &AllowShell { inner: &enriched })
309 } else {
310 quarb::run(query, &enriched)
311 }
312 }
313
314 pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
317 let enriched =
318 quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
319 enriched.locator(node, |bn| self.render(bn))
320 }
321
322 pub fn run(&self, query: &str, now: (i64, u32), allow_shell: bool) -> quarb::Result<QueryResult> {
323 let (secs, nanos) = now;
324 macro_rules! go {
325 ($a:expr) => {{
326 let nowed = WithNow {
327 inner: $a,
328 secs,
329 nanos,
330 };
331 if allow_shell {
332 quarb::run(query, &AllowShell { inner: &nowed })
333 } else {
334 quarb::run(query, &nowed)
335 }
336 }};
337 }
338 match self {
339 Doc::Json(a) => go!(a),
340 Doc::Csv(a) => go!(a),
341 Doc::Xml(a) => go!(a),
342 Doc::Html(a) => go!(a),
343 Doc::Text(a) => go!(a),
344 #[cfg(feature = "sqlite")]
345 Doc::Sqlite(a) => go!(a),
346 #[cfg(feature = "native")]
347 Doc::Fs(a) => go!(a),
348 #[cfg(feature = "native")]
349 Doc::FsDeep(a) => go!(a),
350 #[cfg(feature = "native")]
351 Doc::Git(a) => go!(a),
352 #[cfg(feature = "native")]
353 Doc::Archive(a) => go!(a),
354 #[cfg(feature = "native")]
355 Doc::ArchiveRaw(a) => go!(a),
356 #[cfg(feature = "native")]
357 Doc::Xlsx(a) => go!(a),
358 #[cfg(feature = "native")]
359 Doc::Syntax(a) => go!(a),
360 #[cfg(feature = "native")]
361 Doc::Code(a) => go!(a),
362 Doc::Mount(a) => go!(a),
363 Doc::Boxed(a, _) => go!(a),
364 }
365 }
366
367 pub fn render(&self, node: NodeId) -> String {
369 match self {
370 Doc::Json(a) => a.pointer(node),
371 Doc::Csv(a) => a.locator(node),
372 Doc::Xml(a) => a.locator(node),
373 Doc::Html(a) => a.locator(node),
374 Doc::Text(a) => a.locator(node),
375 #[cfg(feature = "sqlite")]
376 Doc::Sqlite(a) => a.locator(node),
377 #[cfg(feature = "native")]
378 Doc::Fs(a) => a.path(node).display().to_string(),
379 #[cfg(feature = "native")]
380 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
381 #[cfg(feature = "native")]
382 Doc::Git(a) => a.locator(node),
383 #[cfg(feature = "native")]
384 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
385 #[cfg(feature = "native")]
386 Doc::ArchiveRaw(a) => a.locator(node),
387 #[cfg(feature = "native")]
388 Doc::Xlsx(a) => a.locator(node),
389 #[cfg(feature = "native")]
390 Doc::Syntax(a) => a.locator(node),
391 #[cfg(feature = "native")]
392 Doc::Code(a) => a.locator(node),
393 Doc::Mount(a) => generic_locator(a, node),
394 Doc::Boxed(_, render) => render(node),
395 }
396 }
397
398 #[cfg(feature = "sqlite")]
401 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
402 Ok(Doc::Sqlite(
403 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
404 .map_err(|e| anyhow::anyhow!("{e}"))
405 .context("opening SQLite bytes")?,
406 ))
407 }
408
409 #[cfg(not(feature = "sqlite"))]
413 pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
414 anyhow::bail!(
415 "this quarb-session was built without the `sqlite` feature"
416 )
417 }
418
419 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
423 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
424 for (name, doc) in parts {
425 if mounts.iter().any(|m| m.name == name) {
426 bail!("two sources mount as '{name}'; give each a distinct name");
427 }
428 mounts.push(quarb_mount::Mount {
429 name,
430 target: None,
433 adapter: doc.into_boxed()?,
434 });
435 }
436 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
437 }
438
439 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
444 let mut docs: Vec<(String, Doc)> = Vec::new();
445 for (name, format, text) in parts {
446 let doc = Doc::parse(text, format)
447 .with_context(|| format!("parsing '{name}' as {format}"))?;
448 docs.push((name.clone(), doc));
449 }
450 Doc::mount_docs(docs)
451 }
452
453 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
455 use quarb_mount::Shared;
456 Ok(match self {
457 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
458 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
459 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
460 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
461 Doc::Text(a) => Box::new(Shared(Rc::new(a))),
462 #[cfg(feature = "sqlite")]
463 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
464 #[cfg(feature = "native")]
465 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
466 #[cfg(feature = "native")]
467 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
468 #[cfg(feature = "native")]
469 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
470 #[cfg(feature = "native")]
471 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
472 #[cfg(feature = "native")]
473 Doc::ArchiveRaw(a) => Box::new(Shared(Rc::new(a))),
474 #[cfg(feature = "native")]
475 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
476 #[cfg(feature = "native")]
477 Doc::Syntax(a) => Box::new(Shared(Rc::new(a))),
478 #[cfg(feature = "native")]
479 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
480 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
481 Doc::Boxed(a, _) => a.0,
482 })
483 }
484}
485
486#[cfg(feature = "native")]
491impl Doc {
492 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
498 if path.is_dir() {
499 let fsopts = quarb_fs::FsOptions {
500 hidden: opts.hidden,
501 respect_ignore: opts.respect_ignore,
502 };
503 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
504 .with_context(|| format!("opening directory {}", path.display()))?;
505 return Ok(if opts.graft {
506 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
507 fs,
508 |fs, n| Some(fs.path(n)),
509 ))
510 } else {
511 Doc::Fs(fs)
512 });
513 }
514
515 let s = path.to_string_lossy();
516 if let Some(repo) = s.strip_prefix("git:") {
517 let a =
518 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
519 return Ok(Doc::Git(a));
520 }
521 if let Some(rest) = s.strip_prefix("koine:")
526 && !rest.is_empty()
527 {
528 let (path_part, format) = match rest.split_once('?') {
530 Some((p, q)) => {
531 let mut fmt = None;
532 for pair in q.split('&') {
533 match pair.split_once('=') {
534 Some(("format", v)) => fmt = Some(v.to_string()),
535 _ => {
536 return Err(anyhow::anyhow!(
537 "unknown koine option {pair:?} — supported: format="
538 ));
539 }
540 }
541 }
542 (p, fmt)
543 }
544 None => (rest, None),
545 };
546 let target = Path::new(path_part);
547 let ext = target
548 .extension()
549 .and_then(|e| e.to_str())
550 .map(|e| e.to_ascii_lowercase());
551 let read = || -> Result<String> {
552 let text = std::fs::read_to_string(target)
553 .with_context(|| format!("reading {}", target.display()))?;
554 Ok(text
555 .strip_prefix('\u{feff}')
556 .map(str::to_owned)
557 .unwrap_or(text))
558 };
559 if let Some(fmt) = format {
560 let imported = match fmt.as_str() {
561 "atd" | "atk" => {
562 let model =
563 quarb_text_koine::parse_file(target).with_context(|| {
564 format!("reading {} as an atrep document", target.display())
565 })?;
566 return Ok(Doc::Text(model));
567 }
568 "md" | "markdown" => quarb_text_koine::parse_markdown(&read()?),
569 "html" => quarb_text_koine::parse_html(&read()?),
570 "rst" => quarb_text_koine::parse_rst(&read()?),
571 "org" => quarb_text_koine::parse_org(&read()?),
572 "dj" | "djot" => quarb_text_koine::parse_djot(&read()?),
573 "tei" | "docbook" | "jats" | "usx" | "osis" => {
574 quarb_text_koine::parse_xml_as(&read()?, &fmt)
575 }
576 other => {
577 return Err(anyhow::anyhow!(
578 "unknown koine format {other:?} — known: md, html, rst, org, djot, tei, docbook, jats, usx, osis, atd"
579 ));
580 }
581 };
582 return imported.map(Doc::Text).with_context(|| {
583 format!("importing {} through atrep as {fmt}", target.display())
584 });
585 }
586 let imported = match ext.as_deref() {
587 Some("atd" | "atk") => {
588 let model = quarb_text_koine::parse_file(target).with_context(|| {
589 format!("reading {} as an atrep document", target.display())
590 })?;
591 return Ok(Doc::Text(model));
592 }
593 Some("md" | "markdown") => quarb_text_koine::parse_markdown(&read()?),
594 Some("html" | "htm") => quarb_text_koine::parse_html(&read()?),
595 Some("rst") => quarb_text_koine::parse_rst(&read()?),
596 Some("org") => quarb_text_koine::parse_org(&read()?),
597 Some("dj" | "djot") => quarb_text_koine::parse_djot(&read()?),
598 Some("xml") => {
599 let text = read()?;
600 match quarb_text_koine::detect_xml_kind(&text) {
601 Some(kind) => quarb_text_koine::parse_xml_as(&text, kind),
602 None => {
603 return Err(anyhow::anyhow!(
604 "{} declares no XML identity this route knows — force one with koine:{}?format=tei|docbook|jats|usx|osis",
605 target.display(),
606 target.display()
607 ));
608 }
609 }
610 }
611 Some("pdf") => {
612 return Err(anyhow::anyhow!(
613 "atrep cannot import a PDF — the print reading is text:{}",
614 target.display()
615 ));
616 }
617 _ => {
618 return Err(anyhow::anyhow!(
619 "no atrep import for this format yet — the native reading is text:{}",
620 target.display()
621 ));
622 }
623 };
624 return imported
625 .map(Doc::Text)
626 .with_context(|| format!("importing {} through atrep", target.display()));
627 }
628 if let Some(rest) = s.strip_prefix("text:")
632 && !rest.is_empty()
633 {
634 let target = Path::new(rest);
635 let ext = target
638 .extension()
639 .and_then(|e| e.to_str())
640 .map(|e| e.to_ascii_lowercase());
641 if let Some("atd" | "atk") = ext.as_deref() {
643 let model = quarb_text_koine::parse_file(target).with_context(|| {
644 format!("reading {} as an atrep document", target.display())
645 })?;
646 return Ok(Doc::Text(model));
647 }
648 if ext.as_deref() == Some("pdf") {
649 let bytes = std::fs::read(target)
650 .with_context(|| format!("reading {}", target.display()))?;
651 let model = quarb_text_pdf::parse(&bytes)
652 .with_context(|| format!("reading {} as PDF", target.display()))?;
653 let a = std::rc::Rc::new(model);
654 let r = a.clone();
655 return Ok(Doc::Boxed(
656 Dyn(Box::new(quarb_mount::Shared(a))),
657 Box::new(move |n| r.locator(n)),
658 ));
659 }
660 if let Some(ext @ ("docx" | "epub")) = ext.as_deref() {
661 let bytes = std::fs::read(target)
662 .with_context(|| format!("reading {}", target.display()))?;
663 let model = if ext == "docx" {
664 quarb_text_docx::parse(&bytes)
665 .with_context(|| format!("reading {} as Word", target.display()))?
666 } else {
667 quarb_text_epub::parse(&bytes)
668 .with_context(|| format!("reading {} as EPUB", target.display()))?
669 };
670 return Ok(Doc::Text(model));
671 }
672 let text = std::fs::read_to_string(target)
673 .with_context(|| format!("reading {}", target.display()))?;
674 let text = text
675 .strip_prefix('\u{feff}')
676 .map(str::to_owned)
677 .unwrap_or(text);
678 let format = match target
679 .extension()
680 .and_then(|e| e.to_str())
681 .map(|e| e.to_ascii_lowercase())
682 .as_deref()
683 {
684 Some("html" | "htm") => "text-html",
685 Some("md" | "markdown") => "text-markdown",
686 Some("tex" | "latex") => "tex",
687 Some("txt") => "text",
688 _ if text.trim_start().starts_with('<') => "text-html",
689 _ => "text",
690 };
691 return Doc::parse(&text, format);
692 }
693 if let Some(rest) = s.strip_prefix("code:")
698 && !rest.is_empty()
699 {
700 if opts.no_graft {
701 bail!(
702 "no_graft refuses the code: prefix: the prefix's whole \
703 meaning is the grafted code-level view"
704 );
705 }
706 let target = Path::new(rest);
707 if target.is_dir() {
708 let fsopts = quarb_fs::FsOptions {
709 hidden: opts.hidden,
710 respect_ignore: opts.respect_ignore,
711 };
712 let fs = quarb_fs::FsAdapter::with_options(target, fsopts)
713 .with_context(|| format!("opening directory {}", target.display()))?;
714 return Ok(Doc::FsDeep(
715 quarb_compose::ComposeAdapter::with_source_paths(fs, |fs, n| {
716 Some(fs.path(n))
717 })
718 .with_source_graft(quarb_compose::SourceGraft::Code),
719 ));
720 }
721 let a = quarb_code::CodeModel::open(target)
722 .with_context(|| format!("parsing {} at the code level", target.display()))?;
723 return Ok(Doc::Code(a));
724 }
725
726 let ext = path
727 .extension()
728 .and_then(|e| e.to_str())
729 .map(|e| e.to_ascii_lowercase());
730
731 if let Some(e) = &ext
732 && quarb_tree_sitter::supported(e)
733 {
734 let a = quarb_tree_sitter::TreeSitterAdapter::open(path).context("parsing source file")?;
735 return Ok(Doc::Syntax(a));
736 }
737 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
738 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
739 return Ok(Doc::Xlsx(a));
740 }
741 if is_sqlite(path) {
742 #[cfg(not(feature = "sqlite"))]
746 anyhow::bail!(
747 "{}: this quarb-session was built without the `sqlite` feature",
748 path.display()
749 );
750 #[cfg(feature = "sqlite")]
751 {
752 let a = quarb_sqlite::SqliteAdapter::open_with_refs(path, &opts.refs)
753 .context("opening SQLite database")?;
754 return Ok(Doc::Sqlite(a));
755 }
756 }
757 if is_archive(path) {
758 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
759 return Ok(if opts.no_graft {
760 Doc::ArchiveRaw(a)
761 } else {
762 Doc::Archive(quarb_compose::ComposeAdapter::new(a))
763 });
764 }
765
766 let text = std::fs::read_to_string(path)
768 .with_context(|| format!("reading {}", path.display()))?;
769 let text = text
770 .strip_prefix('\u{feff}')
771 .map(str::to_owned)
772 .unwrap_or(text);
773 match ext.as_deref() {
774 Some("csv") => Doc::parse(&text, "csv"),
775 Some("tsv") => Doc::parse(&text, "tsv"),
776 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
777 Some("toml") => Doc::parse(&text, "toml"),
778 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
779 Some("txt") => Doc::parse(&text, "text"),
780 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
781 _ => {
782 if is_xml(path, &text) {
783 Doc::parse(&text, "xml")
784 } else if is_html(path, &text) {
785 Doc::parse(&text, "html")
786 } else {
787 Doc::parse(&text, "json")
788 }
789 }
790 }
791 }
792
793 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
797 let specs: Vec<crate::MountSpec> = paths
798 .iter()
799 .map(|p| crate::MountSpec {
800 name: None,
801 path: p.clone(),
802 })
803 .collect();
804 Doc::mount_specs(&specs, opts)
805 }
806
807 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
810 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
811 for (i, spec) in specs.iter().enumerate() {
812 let name = spec.name.clone().unwrap_or_else(|| {
813 spec.path
814 .file_stem()
815 .map(|s| s.to_string_lossy().into_owned())
816 .unwrap_or_else(|| format!("doc{i}"))
817 });
818 if mounts.iter().any(|m| m.name == name) {
819 bail!(
820 "input '{}' mounts as '{name}', colliding with an earlier input of the \
821 same name; give each a distinct basename (or a NAME=TARGET alias)",
822 spec.path.display()
823 );
824 }
825 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
826 mounts.push(quarb_mount::Mount {
827 name,
828 target: Some(spec.path.display().to_string()),
829 adapter,
830 });
831 }
832 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
833 }
834
835}
836
837fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
841 let mut parts = Vec::new();
842 let mut cur = Some(node);
843 while let Some(n) = cur {
844 if let Some(nm) = a.name(n) {
845 parts.push(nm);
846 }
847 cur = a.parent(n);
848 }
849 parts.reverse();
850 format!("/{}", parts.join("/"))
851}
852
853#[cfg(feature = "native")]
856fn is_sqlite(path: &Path) -> bool {
857 if path
858 .extension()
859 .and_then(|e| e.to_str())
860 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
861 {
862 return true;
863 }
864 use std::io::Read as _;
865 let mut buf = [0u8; 16];
866 std::fs::File::open(path)
867 .and_then(|mut f| f.read_exact(&mut buf))
868 .is_ok()
869 && &buf == b"SQLite format 3\0"
870}
871
872#[cfg(feature = "native")]
874fn is_archive(path: &Path) -> bool {
875 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
876 matches!(
877 e.to_ascii_lowercase().as_str(),
878 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
879 )
880 }) {
881 return true;
882 }
883 use std::io::Read as _;
884 let mut buf = [0u8; 2];
885 std::fs::File::open(path)
886 .and_then(|mut f| f.read_exact(&mut buf))
887 .is_ok()
888 && (&buf == b"PK" || buf == [0x1f, 0x8b])
889}
890
891#[cfg(feature = "native")]
894fn is_xml(path: &Path, text: &str) -> bool {
895 path.extension()
896 .and_then(|e| e.to_str())
897 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
898 || text.trim_start().starts_with("<?xml")
899}
900
901#[cfg(feature = "native")]
904fn is_html(path: &Path, text: &str) -> bool {
905 path.extension()
906 .and_then(|e| e.to_str())
907 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
908 || text.trim_start().starts_with('<')
909}