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 pub allow_shell: bool,
39 pub bonds: Option<Rc<syndesmos::Syndesmos>>,
42}
43
44pub enum Doc {
48 Json(quarb_json::JsonAdapter),
49 Csv(quarb_csv::CsvAdapter),
50 Xml(quarb_xml::XmlAdapter),
51 Html(quarb_html::HtmlAdapter),
52 Text(quarb_text::TextModel),
53 #[cfg(feature = "sqlite")]
54 Sqlite(quarb_sqlite::SqliteAdapter),
55 #[cfg(feature = "native")]
56 Fs(quarb_fs::FsAdapter),
57 #[cfg(feature = "native")]
58 FsDeep(quarb_compose::ComposeAdapter<quarb_fs::FsAdapter>),
59 #[cfg(feature = "native")]
60 Git(quarb_git::GitAdapter),
61 #[cfg(feature = "native")]
62 Archive(quarb_compose::ComposeAdapter<quarb_archive::ArchiveAdapter>),
63 #[cfg(feature = "native")]
66 ArchiveRaw(quarb_archive::ArchiveAdapter),
67 #[cfg(feature = "native")]
68 Xlsx(quarb_xlsx::XlsxAdapter),
69 #[cfg(feature = "native")]
70 Syntax(quarb_tree_sitter::TreeSitterAdapter),
71 #[cfg(feature = "native")]
72 Code(quarb_code::CodeModel),
73 Mount(quarb_mount::MountAdapter),
74 Boxed(Dyn, Box<dyn Fn(NodeId) -> String>),
78}
79
80pub struct Dyn(pub Box<dyn quarb::AstAdapter>);
83
84impl quarb::AstAdapter for Dyn {
85 fn root(&self) -> NodeId {
86 self.0.root()
87 }
88 fn children(&self, node: NodeId) -> Vec<NodeId> {
89 self.0.children(node)
90 }
91 fn name(&self, node: NodeId) -> Option<String> {
92 self.0.name(node)
93 }
94 fn parent(&self, node: NodeId) -> Option<NodeId> {
95 self.0.parent(node)
96 }
97 fn traits(&self, node: NodeId) -> Vec<String> {
98 self.0.traits(node)
99 }
100 fn property(&self, node: NodeId, name: &str) -> Option<quarb::Value> {
101 self.0.property(node, name)
102 }
103 fn children_named(&self, node: NodeId, name: &str) -> Vec<NodeId> {
104 self.0.children_named(node, name)
105 }
106 fn child_host(&self, node: NodeId) -> NodeId {
107 self.0.child_host(node)
108 }
109 fn non_tree_nodes(&self) -> Vec<NodeId> {
110 self.0.non_tree_nodes()
111 }
112 fn properties(&self, node: NodeId) -> Vec<(String, quarb::Value)> {
113 self.0.properties(node)
114 }
115 fn default_value(&self, node: NodeId) -> Option<quarb::Value> {
116 self.0.default_value(node)
117 }
118 fn metadata(&self, node: NodeId, key: &str) -> Option<quarb::Value> {
119 self.0.metadata(node, key)
120 }
121 fn aliased_metadata(&self, node: NodeId) -> &'static [&'static str] {
122 self.0.aliased_metadata(node)
123 }
124 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
125 self.0.links(node)
126 }
127 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
128 self.0.backlinks(node)
129 }
130 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
131 self.0.resolve(node, property, hint)
132 }
133 fn external_ref(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<String> {
134 self.0.external_ref(node, property, hint)
135 }
136 fn resolve_fragment(&self, node: NodeId, fragment: &str) -> Option<NodeId> {
137 self.0.resolve_fragment(node, fragment)
138 }
139 fn ref_property(&self, node: NodeId) -> Option<String> {
140 self.0.ref_property(node)
141 }
142 fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
143 self.0.ref_label(node, property)
144 }
145 fn link_property(
146 &self,
147 source: NodeId,
148 label: &str,
149 target: NodeId,
150 name: &str,
151 ) -> Option<quarb::Value> {
152 self.0.link_property(source, label, target, name)
153 }
154 fn quantifier_bound(&self) -> usize {
155 self.0.quantifier_bound()
156 }
157 fn invocation_instant(&self) -> Option<(i64, u32)> {
158 self.0.invocation_instant()
159 }
160 fn unit_scale(&self, expr: &str) -> Option<(f64, String)> {
161 self.0.unit_scale(expr)
162 }
163 fn answers_to(&self, node: NodeId, name: &str) -> bool {
168 self.0.answers_to(node, name)
169 }
170 fn has_trait(&self, node: NodeId, name: &str) -> bool {
171 self.0.has_trait(node, name)
172 }
173 fn has_node(&self, node: NodeId) -> bool {
174 self.0.has_node(node)
175 }
176 fn document_by_ref(&self, id: &str) -> Option<NodeId> {
177 self.0.document_by_ref(id)
178 }
179 fn reverse_resolve(
180 &self,
181 node: NodeId,
182 property: Option<&str>,
183 hint: Option<&str>,
184 ) -> Option<Vec<NodeId>> {
185 self.0.reverse_resolve(node, property, hint)
186 }
187 fn descendants_named(&self, node: NodeId, name: &str) -> Option<Vec<(NodeId, usize)>> {
188 self.0.descendants_named(node, name)
189 }
190 fn prefetch_links(&self, nodes: &[NodeId], dir: quarb::LinkDir) {
191 self.0.prefetch_links(nodes, dir)
192 }
193 fn provenance(&self, node: NodeId) -> quarb::Provenance {
194 self.0.provenance(node)
195 }
196 fn provenance_list(&self, node: NodeId) -> quarb::ProvenanceList {
197 self.0.provenance_list(node)
198 }
199 fn allow_shell(&self) -> bool {
200 self.0.allow_shell()
201 }
202}
203
204impl Doc {
205 fn boxed_kaiv(a: quarb_kaiv::KaivAdapter) -> Doc {
209 let a = std::rc::Rc::new(a);
210 let r = a.clone();
211 Doc::Boxed(
212 Dyn(Box::new(quarb_mount::Shared(a))),
213 Box::new(move |n| r.locator(n)),
214 )
215 }
216
217 pub fn parse(input: &str, format: &str) -> Result<Doc> {
221 match format {
222 "kaiv" => {
227 let a = quarb_kaiv::KaivAdapter::parse_kaiv(input)
228 .map_err(|e| anyhow::anyhow!("parsing kaiv: {e}"))?;
229 return Ok(Self::boxed_kaiv(a));
230 }
231 "daiv" => {
232 let a = quarb_kaiv::KaivAdapter::parse_daiv(input)
233 .map_err(|e| anyhow::anyhow!("parsing daiv: {e}"))?;
234 return Ok(Self::boxed_kaiv(a));
235 }
236 "json" => quarb_json::JsonAdapter::parse(input)
237 .map(Doc::Json)
238 .context("parsing JSON"),
239 "json2" => quarb_json::JsonAdapter::parse_as(input, quarb_json::Reading::Property)
243 .map(Doc::Json)
244 .context("parsing JSON"),
245 "jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
246 .map(Doc::Json)
247 .context("parsing JSONL"),
248 "yaml" | "yml" => quarb_yaml::parse(input)
249 .map(Doc::Json)
250 .context("parsing YAML"),
251 "toml" => quarb_toml::parse(input)
252 .map(Doc::Json)
253 .context("parsing TOML"),
254 "csv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b',')
255 .map(Doc::Csv)
256 .context("parsing CSV"),
257 "tsv" => quarb_csv::CsvAdapter::parse_with_delimiter(input, b'\t')
258 .map(Doc::Csv)
259 .context("parsing TSV"),
260 "xml" => quarb_xml::XmlAdapter::parse(input)
261 .map(Doc::Xml)
262 .context("parsing XML"),
263 "html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
264 "markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
265 "tex" | "latex" => Ok(Doc::Text(quarb_text_latex::parse(input))),
268 "text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
272 "text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
273 "text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
274 "conllu" => quarb_text::TextModel::parse_conllu_text(input)
277 .map(Doc::Text)
278 .map_err(|e| anyhow::anyhow!("reading CoNLL-U: {e}")),
279 "corpus-conllu" => quarb_text::TextModel::parse_conllu_corpus(input)
280 .map(Doc::Text)
281 .map_err(|e| anyhow::anyhow!("reading CoNLL-U: {e}")),
282 "corpus-html" | "corpus-markdown" | "corpus" => {
284 let mut model = match format {
285 "corpus-html" => quarb_text_html::parse(input),
286 "corpus-markdown" => quarb_text_markdown::parse(input),
287 _ => quarb_text::TextModel::parse_plain(input),
288 };
289 model.tokenize();
290 Ok(Doc::Text(model))
291 }
292 #[cfg(feature = "koine")]
295 "text-bibtex" | "bibtex" => quarb_text_koine::parse_bibtex(input)
296 .map(Doc::Text)
297 .map_err(|e| anyhow::anyhow!("{e}")),
298 #[cfg(feature = "native")]
301 "code-rust" => quarb_code::CodeModel::parse(input, "rs")
302 .map(Doc::Code)
303 .context("parsing Rust at the code level"),
304 #[cfg(feature = "native")]
305 "code-python" => quarb_code::CodeModel::parse(input, "py")
306 .map(Doc::Code)
307 .context("parsing Python at the code level"),
308 #[cfg(feature = "native")]
309 "code-javascript" => quarb_code::CodeModel::parse(input, "js")
310 .map(Doc::Code)
311 .context("parsing JavaScript at the code level"),
312 #[cfg(feature = "native")]
313 "code-c" => quarb_code::CodeModel::parse(input, "c")
314 .map(Doc::Code)
315 .context("parsing C at the code level"),
316 other => bail!("unknown format: {other}"),
317 }
318 }
319
320 pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
329 match self {
330 Doc::Json(a) => a,
331 Doc::Csv(a) => a,
332 Doc::Xml(a) => a,
333 Doc::Html(a) => a,
334 Doc::Text(a) => a,
335 #[cfg(feature = "sqlite")]
336 Doc::Sqlite(a) => a,
337 #[cfg(feature = "native")]
338 Doc::Fs(a) => a,
339 #[cfg(feature = "native")]
340 Doc::FsDeep(a) => a,
341 #[cfg(feature = "native")]
342 Doc::Git(a) => a,
343 #[cfg(feature = "native")]
344 Doc::Archive(a) => a,
345 #[cfg(feature = "native")]
346 Doc::ArchiveRaw(a) => a,
347 #[cfg(feature = "native")]
348 Doc::Xlsx(a) => a,
349 #[cfg(feature = "native")]
350 Doc::Syntax(a) => a,
351 #[cfg(feature = "native")]
352 Doc::Code(a) => a,
353 Doc::Mount(a) => a,
354 Doc::Boxed(a, _) => &*a.0,
355 }
356 }
357
358 pub fn export(
364 &self,
365 query: &str,
366 now: (i64, u32),
367 allow_shell: bool,
368 kind: &str,
369 ) -> Result<String> {
370 let render = quarb_text::Render::from_name(kind)
371 .ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
372 if query.trim().is_empty() {
374 let base = self.base_dyn();
375 return Ok(quarb_text::render_nodes(base, &[base.root()], render));
376 }
377 match self
378 .run(query, now, allow_shell)
379 .map_err(|e| anyhow::anyhow!("{e}"))?
380 {
381 QueryResult::Nodes(nodes) => {
382 Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
383 }
384 QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
385 }
386 }
387
388 pub fn run_modeled(
393 &self,
394 query: &str,
395 now: (i64, u32),
396 allow_shell: bool,
397 model: &quarb_model::Model,
398 ) -> quarb::Result<QueryResult> {
399 let (secs, nanos) = now;
400 let base = quarb_model::Borrowed(self.base_dyn());
401 let nowed = WithNow {
402 inner: &base,
403 secs,
404 nanos,
405 };
406 let enriched = quarb_model::ModelAdapter::new(nowed, model.clone());
407 enriched
408 .check_declared_traits()
409 .map_err(|e| quarb::QuarbError::Parse(format!("reading the model: {e}")))?;
410 if allow_shell {
411 quarb::run(query, &AllowShell { inner: &enriched })
412 } else {
413 quarb::run(query, &enriched)
414 }
415 }
416
417 pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
420 let enriched =
421 quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
422 enriched.locator(node, |bn| self.render(bn))
423 }
424
425 pub fn run(
426 &self,
427 query: &str,
428 now: (i64, u32),
429 allow_shell: bool,
430 ) -> quarb::Result<QueryResult> {
431 let (secs, nanos) = now;
432 macro_rules! go {
433 ($a:expr) => {{
434 let nowed = WithNow {
435 inner: $a,
436 secs,
437 nanos,
438 };
439 if allow_shell {
440 quarb::run(query, &AllowShell { inner: &nowed })
441 } else {
442 quarb::run(query, &nowed)
443 }
444 }};
445 }
446 match self {
447 Doc::Json(a) => go!(a),
448 Doc::Csv(a) => go!(a),
449 Doc::Xml(a) => go!(a),
450 Doc::Html(a) => go!(a),
451 Doc::Text(a) => go!(a),
452 #[cfg(feature = "sqlite")]
453 Doc::Sqlite(a) => go!(a),
454 #[cfg(feature = "native")]
455 Doc::Fs(a) => go!(a),
456 #[cfg(feature = "native")]
457 Doc::FsDeep(a) => go!(a),
458 #[cfg(feature = "native")]
459 Doc::Git(a) => go!(a),
460 #[cfg(feature = "native")]
461 Doc::Archive(a) => go!(a),
462 #[cfg(feature = "native")]
463 Doc::ArchiveRaw(a) => go!(a),
464 #[cfg(feature = "native")]
465 Doc::Xlsx(a) => go!(a),
466 #[cfg(feature = "native")]
467 Doc::Syntax(a) => go!(a),
468 #[cfg(feature = "native")]
469 Doc::Code(a) => go!(a),
470 Doc::Mount(a) => go!(a),
471 Doc::Boxed(a, _) => go!(a),
472 }
473 }
474
475 pub fn family(&self) -> &'static str {
479 match self {
480 Doc::Json(_) => "json",
481 Doc::Csv(_) => "csv",
482 Doc::Xml(_) => "xml",
483 Doc::Html(_) => "html",
484 Doc::Text(_) => "text",
485 #[cfg(feature = "sqlite")]
486 Doc::Sqlite(_) => "sqlite",
487 #[cfg(feature = "native")]
488 Doc::Fs(_) | Doc::FsDeep(_) => "fs",
489 #[cfg(feature = "native")]
490 Doc::Git(_) => "git",
491 #[cfg(feature = "native")]
492 Doc::Archive(_) | Doc::ArchiveRaw(_) => "archive",
493 #[cfg(feature = "native")]
494 Doc::Xlsx(_) => "xlsx",
495 #[cfg(feature = "native")]
496 Doc::Syntax(_) => "tree-sitter",
497 #[cfg(feature = "native")]
498 Doc::Code(_) => "code",
499 Doc::Mount(_) => "mount",
500 Doc::Boxed(..) => "boxed",
501 }
502 }
503
504 pub fn trace(
509 &self,
510 committed: &str,
511 staged: Option<&str>,
512 opts: &quarb::trace::Options,
513 ) -> quarb::Result<quarb_trace::Payload> {
514 let base = quarb_model::Borrowed(self.base_dyn());
515 quarb::trace::trace(committed, staged, &base, &|n| self.render(n), opts)
516 }
517
518 pub fn render(&self, node: NodeId) -> String {
519 match self {
520 Doc::Json(a) => a.pointer(node),
521 Doc::Csv(a) => a.locator(node),
522 Doc::Xml(a) => a.locator(node),
523 Doc::Html(a) => a.locator(node),
524 Doc::Text(a) => a.locator(node),
525 #[cfg(feature = "sqlite")]
526 Doc::Sqlite(a) => a.locator(node),
527 #[cfg(feature = "native")]
528 Doc::Fs(a) => a.path(node).display().to_string(),
529 #[cfg(feature = "native")]
530 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
531 #[cfg(feature = "native")]
532 Doc::Git(a) => a.locator(node),
533 #[cfg(feature = "native")]
534 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
535 #[cfg(feature = "native")]
536 Doc::ArchiveRaw(a) => a.locator(node),
537 #[cfg(feature = "native")]
538 Doc::Xlsx(a) => a.locator(node),
539 #[cfg(feature = "native")]
540 Doc::Syntax(a) => a.locator(node),
541 #[cfg(feature = "native")]
542 Doc::Code(a) => a.locator(node),
543 Doc::Mount(a) => generic_locator(a, node),
544 Doc::Boxed(_, render) => render(node),
545 }
546 }
547
548 #[cfg(feature = "sqlite")]
551 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
552 Ok(Doc::Sqlite(
553 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
554 .map_err(|e| anyhow::anyhow!("{e}"))
555 .context("opening SQLite bytes")?,
556 ))
557 }
558
559 #[cfg(not(feature = "sqlite"))]
563 pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
564 anyhow::bail!("this quarb-session was built without the `sqlite` feature")
565 }
566
567 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
571 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
572 for (name, doc) in parts {
573 if mounts.iter().any(|m| m.name == name) {
574 bail!("two sources mount as '{name}'; give each a distinct name");
575 }
576 mounts.push(quarb_mount::Mount {
577 name,
578 target: None,
581 adapter: doc.into_boxed()?,
582 });
583 }
584 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
585 }
586
587 pub fn attach_url(&mut self, url: &str) {
592 match self {
593 Doc::Html(a) => a.set_document_url(url),
594 Doc::Text(a) => a.set_document_url(url),
595 _ => {}
596 }
597 }
598
599 pub fn url_roots(
605 &self,
606 urls: &[(String, String)],
607 ) -> std::collections::HashMap<String, quarb::NodeId> {
608 use quarb::AstAdapter;
609 let a = self.base_dyn();
610 let root = a.root();
611 match self {
612 Doc::Mount(_) => urls
613 .iter()
614 .filter_map(|(name, url)| {
615 a.children_named(root, name)
616 .first()
617 .map(|&n| (url.clone(), n))
618 })
619 .collect(),
620 _ => urls
621 .iter()
622 .take(1)
623 .map(|(_, url)| (url.clone(), root))
624 .collect(),
625 }
626 }
627
628 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
633 let mut docs: Vec<(String, Doc)> = Vec::new();
634 for (name, format, text) in parts {
635 let doc = Doc::parse(text, format)
636 .with_context(|| format!("parsing '{name}' as {format}"))?;
637 docs.push((name.clone(), doc));
638 }
639 Doc::mount_docs(docs)
640 }
641
642 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
644 use quarb_mount::Shared;
645 Ok(match self {
646 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
647 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
648 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
649 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
650 Doc::Text(a) => Box::new(Shared(Rc::new(a))),
651 #[cfg(feature = "sqlite")]
652 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
653 #[cfg(feature = "native")]
654 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
655 #[cfg(feature = "native")]
656 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
657 #[cfg(feature = "native")]
658 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
659 #[cfg(feature = "native")]
660 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
661 #[cfg(feature = "native")]
662 Doc::ArchiveRaw(a) => Box::new(Shared(Rc::new(a))),
663 #[cfg(feature = "native")]
664 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
665 #[cfg(feature = "native")]
666 Doc::Syntax(a) => Box::new(Shared(Rc::new(a))),
667 #[cfg(feature = "native")]
668 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
669 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
670 Doc::Boxed(a, _) => a.0,
671 })
672 }
673}
674
675#[cfg(feature = "native")]
680impl Doc {
681 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
687 if path.is_dir() {
688 let fsopts = quarb_fs::FsOptions {
689 hidden: opts.hidden,
690 respect_ignore: opts.respect_ignore,
691 };
692 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
693 .with_context(|| format!("opening directory {}", path.display()))?;
694 return Ok(if opts.graft {
695 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
696 fs,
697 |fs, n| Some(fs.path(n)),
698 ))
699 } else {
700 Doc::Fs(fs)
701 });
702 }
703
704 let s = path.to_string_lossy();
705 if let Some(repo) = s.strip_prefix("git:") {
706 let a =
707 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
708 return Ok(Doc::Git(a));
709 }
710 if let Some(rest) = s.strip_prefix("koine:")
715 && !rest.is_empty()
716 {
717 let (path_part, format) = match rest.split_once('?') {
719 Some((p, q)) => {
720 let mut fmt = None;
721 for pair in q.split('&') {
722 match pair.split_once('=') {
723 Some(("format", v)) => fmt = Some(v.to_string()),
724 _ => {
725 return Err(anyhow::anyhow!(
726 "unknown koine option {pair:?} — supported: format="
727 ));
728 }
729 }
730 }
731 (p, fmt)
732 }
733 None => (rest, None),
734 };
735 let target = Path::new(path_part);
736 let ext = target
737 .extension()
738 .and_then(|e| e.to_str())
739 .map(|e| e.to_ascii_lowercase());
740 let read = || -> Result<String> {
741 let text = std::fs::read_to_string(target)
742 .with_context(|| format!("reading {}", target.display()))?;
743 Ok(text
744 .strip_prefix('\u{feff}')
745 .map(str::to_owned)
746 .unwrap_or(text))
747 };
748 if let Some(fmt) = format {
749 let imported = match fmt.as_str() {
750 "atd" | "atk" => {
751 let model = quarb_text_koine::parse_file(target).with_context(|| {
752 format!("reading {} as an atrep document", target.display())
753 })?;
754 return Ok(Doc::Text(model));
755 }
756 "md" | "markdown" => quarb_text_koine::parse_markdown(&read()?),
757 "html" => quarb_text_koine::parse_html(&read()?),
758 "rst" => quarb_text_koine::parse_rst(&read()?),
759 "org" => quarb_text_koine::parse_org(&read()?),
760 "dj" | "djot" => quarb_text_koine::parse_djot(&read()?),
761 "tei" | "docbook" | "jats" | "usx" | "osis" | "rnc" | "opencorpora"
762 | "proiel" => quarb_text_koine::parse_xml_as(&read()?, &fmt),
763 other => {
764 return Err(anyhow::anyhow!(
765 "unknown koine format {other:?} — known: md, html, rst, org, djot, tei, docbook, jats, usx, osis, atd"
766 ));
767 }
768 };
769 return imported.map(Doc::Text).with_context(|| {
770 format!("importing {} through atrep as {fmt}", target.display())
771 });
772 }
773 let imported = match ext.as_deref() {
774 Some("atd" | "atk") => {
775 let model = quarb_text_koine::parse_file(target).with_context(|| {
776 format!("reading {} as an atrep document", target.display())
777 })?;
778 return Ok(Doc::Text(model));
779 }
780 Some("usfm" | "sfm") => quarb_text_koine::parse_xml_as(&read()?, "usfm"),
781 Some("bib") => quarb_text_koine::parse_bibtex(&read()?),
782 Some("md" | "markdown") => quarb_text_koine::parse_markdown(&read()?),
783 Some("html" | "htm") => quarb_text_koine::parse_html(&read()?),
784 Some("rst") => quarb_text_koine::parse_rst(&read()?),
785 Some("org") => quarb_text_koine::parse_org(&read()?),
786 Some("dj" | "djot") => quarb_text_koine::parse_djot(&read()?),
787 Some("xml") => {
788 let text = read()?;
789 match quarb_text_koine::detect_xml_kind(&text) {
790 Some(kind) => quarb_text_koine::parse_xml_as(&text, kind),
791 None => {
792 return Err(anyhow::anyhow!(
793 "{} declares no XML identity this route knows — force one with koine:{}?format=tei|docbook|jats|usx|osis|rnc|opencorpora|proiel",
794 target.display(),
795 target.display()
796 ));
797 }
798 }
799 }
800 Some("pdf") => {
801 return Err(anyhow::anyhow!(
802 "atrep cannot import a PDF — the print reading is text:{}",
803 target.display()
804 ));
805 }
806 _ => {
807 return Err(anyhow::anyhow!(
808 "no atrep import for this format yet — the native reading is text:{}",
809 target.display()
810 ));
811 }
812 };
813 return imported
814 .map(Doc::Text)
815 .with_context(|| format!("importing {} through atrep", target.display()));
816 }
817 if let Some(rest) = s.strip_prefix("lit:")
821 && !rest.is_empty()
822 {
823 let (rest, conllu, annotate, desm, format, cast, places, modernize) = match rest
824 .split_once('?')
825 {
826 Some((p, q)) => {
827 let (
828 mut conllu,
829 mut annotate,
830 mut desm,
831 mut format,
832 mut cast,
833 mut places,
834 mut modernize,
835 ) = (None, None, Vec::new(), None, None, None, None);
836 for pair in q.split('&') {
837 match pair.split_once('=') {
838 Some(("conllu", v)) => conllu = Some(v.to_string()),
839 Some(("annotate", v)) => annotate = Some(v.to_string()),
840 Some(("desm", v)) => desm.push(v.to_string()),
841 Some(("cast", v)) => cast = Some(v.to_string()),
842 Some(("places", v)) => places = Some(v.to_string()),
843 Some(("modernize", v)) => modernize = Some(v.to_string()),
844 Some(("format", v)) => format = Some(v.to_string()),
845 _ => bail!(
846 "unknown lit option {pair:?} — supported: conllu=, annotate=, desm=, cast=, places=, modernize=, format="
847 ),
848 }
849 }
850 (p, conllu, annotate, desm, format, cast, places, modernize)
851 }
852 None => (rest, None, None, Vec::new(), None, None, None, None),
853 };
854 let target = Path::new(rest);
855 let ext = target
856 .extension()
857 .and_then(|e| e.to_str())
858 .map(|e| e.to_ascii_lowercase());
859 let read = || -> Result<String> {
860 let text = std::fs::read_to_string(target)
861 .with_context(|| format!("reading {}", target.display()))?;
862 Ok(text
863 .strip_prefix('\u{feff}')
864 .map(str::to_owned)
865 .unwrap_or(text))
866 };
867 let kind = match format {
868 Some(k) => k,
869 None => match ext.as_deref() {
870 Some("atd" | "atk") => "atd".to_string(),
871 Some("usx") => "usx".to_string(),
872 _ => match quarb_text_koine::detect_xml_kind(&read()?) {
873 Some(k) => k.to_string(),
874 None => bail!(
875 "lit: reads TEI, USX, OSIS, RNC, OpenCorpora, PROIEL and litogramma — {rest} declares none of them (koine:{rest} reads the other XML vocabularies, corpus:{rest} plain prose)"
876 ),
877 },
878 },
879 };
880 let mut model = match kind.as_str() {
881 "atd" | "atk" => quarb_text_koine::parse_lit_file(target).with_context(|| {
882 format!("reading {} as an atrep document", target.display())
883 })?,
884 "tei" | "usx" | "osis" | "rnc" | "opencorpora" | "proiel" => {
885 quarb_text_koine::parse_lit_xml_as(&read()?, &kind).with_context(|| {
886 format!("reading {} as {}", target.display(), kind.to_uppercase())
887 })?
888 }
889 other => bail!(
890 "lit: reads TEI, USX, OSIS, RNC, OpenCorpora, PROIEL and litogramma — not {other} (koine:{rest} reads the other XML vocabularies)"
891 ),
892 };
893 model.set_document_path(rest);
894 return finish_corpus(
895 model, conllu, annotate, &desm, cast, places, modernize, opts, "lit",
896 );
897 }
898 if let Some(rest) = s.strip_prefix("corpus:")
905 && !rest.is_empty()
906 {
907 let (rest, conllu, annotate, desm, format, cast, places, modernize) = match rest
909 .split_once('?')
910 {
911 Some((p, q)) => {
912 let (
913 mut conllu,
914 mut annotate,
915 mut desm,
916 mut format,
917 mut cast,
918 mut places,
919 mut modernize,
920 ) = (None, None, Vec::new(), None, None, None, None);
921 for pair in q.split('&') {
922 match pair.split_once('=') {
923 Some(("conllu", v)) => conllu = Some(v.to_string()),
924 Some(("annotate", v)) => annotate = Some(v.to_string()),
925 Some(("desm", v)) => desm.push(v.to_string()),
926 Some(("cast", v)) => cast = Some(v.to_string()),
927 Some(("places", v)) => places = Some(v.to_string()),
928 Some(("modernize", v)) => modernize = Some(v.to_string()),
929 Some(("format", "conllu")) => format = Some("conllu"),
930 Some(("format", v)) => bail!(
931 "corpus: format={v} is not a treebank format — format= takes conllu"
932 ),
933 _ => bail!(
934 "unknown corpus option {pair:?} — supported: conllu=, annotate=, desm=, cast=, places=, modernize=, format="
935 ),
936 }
937 }
938 (p, conllu, annotate, desm, format, cast, places, modernize)
939 }
940 None => (rest, None, None, Vec::new(), None, None, None, None),
941 };
942 let target = Path::new(rest);
943 let ext = target
944 .extension()
945 .and_then(|e| e.to_str())
946 .map(|e| e.to_ascii_lowercase());
947 if ext.as_deref() == Some("pdf") {
948 bail!(
949 "corpus: needs a prose reading, and a PDF's is line geometry — text:{rest} reads its lines"
950 );
951 }
952 let own_annotation = || {
957 if conllu.is_some() || annotate.is_some() {
958 bail!(
959 "corpus: {rest} is its own annotation — conllu= and annotate= apply to prose"
960 );
961 }
962 Ok(())
963 };
964 if target.is_dir() {
965 own_annotation()?;
966 let files = quarb_text::read_conllu_dir(target).map_err(|e| {
967 anyhow::anyhow!("corpus: over a directory reads its treebank — {e}")
968 })?;
969 let refs: Vec<(&str, &str)> = files
970 .iter()
971 .map(|(n, t)| (n.as_str(), t.as_str()))
972 .collect();
973 let mut model = quarb_text::TextModel::parse_conllu_corpus_set(&refs)
974 .map_err(|e| anyhow::anyhow!("reading {rest} as CoNLL-U: {e}"))?;
975 model.set_document_path(rest);
976 return treebank_options(model, &cast, &places, &modernize);
977 }
978 let treebank = matches!(ext.as_deref(), Some("conllu" | "conllup"))
979 || format == Some("conllu")
980 || (ext.is_none()
981 && std::fs::read_to_string(target)
982 .is_ok_and(|t| quarb_text::looks_like_conllu(&t)));
983 if treebank {
984 own_annotation()?;
985 let text = std::fs::read_to_string(target)
986 .with_context(|| format!("reading {}", target.display()))?;
987 let text = text
988 .strip_prefix('\u{feff}')
989 .map(str::to_owned)
990 .unwrap_or(text);
991 let Doc::Text(mut model) = Doc::parse(&text, "corpus-conllu")? else {
992 bail!("corpus: needs a prose reading of {rest}")
993 };
994 model.set_document_path(rest);
995 return treebank_options(model, &cast, &places, &modernize);
996 }
997 let route = if matches!(ext.as_deref(), Some("xml" | "tei")) {
998 format!("koine:{rest}")
999 } else {
1000 format!("text:{rest}")
1001 };
1002 let Doc::Text(model) = Doc::open(Path::new(&route), opts)? else {
1003 bail!("corpus: needs a prose reading of {rest}")
1004 };
1005 return finish_corpus(
1006 model, conllu, annotate, &desm, cast, places, modernize, opts, "corpus",
1007 );
1008 }
1009 if let Some(rest) = s.strip_prefix("json2:")
1012 && !rest.is_empty()
1013 {
1014 use quarb_json::Reading;
1015 let target = Path::new(rest);
1016 let text = std::fs::read_to_string(target)
1017 .with_context(|| format!("reading {}", target.display()))?;
1018 let ext = target
1019 .extension()
1020 .and_then(|e| e.to_str())
1021 .map(|e| e.to_ascii_lowercase())
1022 .unwrap_or_default();
1023 let a = match ext.as_str() {
1024 "yaml" | "yml" => {
1025 quarb_yaml::parse_as(&text, Reading::Property).context("parsing YAML")?
1026 }
1027 "toml" => quarb_toml::parse_as(&text, Reading::Property).context("parsing TOML")?,
1028 "jsonl" | "ndjson" => {
1029 quarb_json::JsonAdapter::parse_lines_as(&text, Reading::Property)
1030 .context("parsing JSONL")?
1031 }
1032 _ => quarb_json::JsonAdapter::parse_as(&text, Reading::Property)
1033 .context("parsing JSON")?,
1034 };
1035 return Ok(Doc::Json(a));
1036 }
1037 if let Some(rest) = s.strip_prefix("text:")
1041 && !rest.is_empty()
1042 {
1043 let target = Path::new(rest);
1044 if target.is_dir() {
1047 let files = quarb_text::read_conllu_dir(target).map_err(|e| {
1048 anyhow::anyhow!("text: over a directory reads its treebank — {e}")
1049 })?;
1050 let refs: Vec<(&str, &str)> = files
1051 .iter()
1052 .map(|(n, t)| (n.as_str(), t.as_str()))
1053 .collect();
1054 let mut model = quarb_text::TextModel::parse_conllu_text_set(&refs)
1055 .map_err(|e| anyhow::anyhow!("reading {rest} as CoNLL-U: {e}"))?;
1056 model.set_document_path(rest);
1057 return Ok(Doc::Text(model));
1058 }
1059 let ext = target
1062 .extension()
1063 .and_then(|e| e.to_str())
1064 .map(|e| e.to_ascii_lowercase());
1065 if let Some("usfm" | "sfm") = ext.as_deref() {
1067 let text = std::fs::read_to_string(target)
1068 .with_context(|| format!("reading {}", target.display()))?;
1069 let text = text
1070 .strip_prefix('\u{feff}')
1071 .map(str::to_owned)
1072 .unwrap_or(text);
1073 let model = quarb_text_koine::parse_xml_as(&text, "usfm")
1074 .with_context(|| format!("reading {} as USFM", target.display()))?;
1075 return Ok(Doc::Text(model));
1076 }
1077 if let Some("atd" | "atk") = ext.as_deref() {
1078 let model = quarb_text_koine::parse_file(target).with_context(|| {
1079 format!("reading {} as an atrep document", target.display())
1080 })?;
1081 return Ok(Doc::Text(model));
1082 }
1083 if ext.as_deref() == Some("pdf") {
1084 let bytes = std::fs::read(target)
1085 .with_context(|| format!("reading {}", target.display()))?;
1086 let model = quarb_text_pdf::parse(&bytes)
1087 .with_context(|| format!("reading {} as PDF", target.display()))?;
1088 let a = std::rc::Rc::new(model);
1089 let r = a.clone();
1090 return Ok(Doc::Boxed(
1091 Dyn(Box::new(quarb_mount::Shared(a))),
1092 Box::new(move |n| r.locator(n)),
1093 ));
1094 }
1095 if let Some(ext @ ("docx" | "epub")) = ext.as_deref() {
1096 let bytes = std::fs::read(target)
1097 .with_context(|| format!("reading {}", target.display()))?;
1098 let model = if ext == "docx" {
1099 let mut m = quarb_text_docx::parse(&bytes)
1100 .with_context(|| format!("reading {} as Word", target.display()))?;
1101 quarb_text_koine::attach_bib_aliases(&mut m);
1102 m
1103 } else {
1104 quarb_text_epub::parse(&bytes)
1105 .with_context(|| format!("reading {} as EPUB", target.display()))?
1106 };
1107 return Ok(Doc::Text(model));
1108 }
1109 let text = std::fs::read_to_string(target)
1110 .with_context(|| format!("reading {}", target.display()))?;
1111 let text = text
1112 .strip_prefix('\u{feff}')
1113 .map(str::to_owned)
1114 .unwrap_or(text);
1115 let format = match target
1116 .extension()
1117 .and_then(|e| e.to_str())
1118 .map(|e| e.to_ascii_lowercase())
1119 .as_deref()
1120 {
1121 Some("html" | "htm") => "text-html",
1122 Some("md" | "markdown") => "text-markdown",
1123 Some("tex" | "latex") => "tex",
1124 Some("txt") => "text",
1125 Some("conllu" | "conllup") => "conllu",
1126 #[cfg(feature = "koine")]
1127 Some("bib") => "text-bibtex",
1128 _ if text.trim_start().starts_with('<') => "text-html",
1129 None if quarb_text::looks_like_conllu(&text) => "conllu",
1130 _ => "text",
1131 };
1132 return Doc::parse(&text, format);
1133 }
1134 if let Some(rest) = s.strip_prefix("code:")
1139 && !rest.is_empty()
1140 {
1141 if opts.no_graft {
1142 bail!(
1143 "no_graft refuses the code: prefix: the prefix's whole \
1144 meaning is the grafted code-level view"
1145 );
1146 }
1147 let target = Path::new(rest);
1148 if target.is_dir() {
1149 let fsopts = quarb_fs::FsOptions {
1150 hidden: opts.hidden,
1151 respect_ignore: opts.respect_ignore,
1152 };
1153 let fs = quarb_fs::FsAdapter::with_options(target, fsopts)
1154 .with_context(|| format!("opening directory {}", target.display()))?;
1155 return Ok(Doc::FsDeep(
1156 quarb_compose::ComposeAdapter::with_source_paths(fs, |fs, n| Some(fs.path(n)))
1157 .with_source_graft(quarb_compose::SourceGraft::Code),
1158 ));
1159 }
1160 let a = quarb_code::CodeModel::open(target)
1161 .with_context(|| format!("parsing {} at the code level", target.display()))?;
1162 return Ok(Doc::Code(a));
1163 }
1164
1165 let ext = path
1166 .extension()
1167 .and_then(|e| e.to_str())
1168 .map(|e| e.to_ascii_lowercase());
1169
1170 if let Some(e) = &ext
1171 && quarb_tree_sitter::supported(e)
1172 {
1173 let a =
1174 quarb_tree_sitter::TreeSitterAdapter::open(path).context("parsing source file")?;
1175 return Ok(Doc::Syntax(a));
1176 }
1177 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
1178 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
1179 return Ok(Doc::Xlsx(a));
1180 }
1181 if is_sqlite(path) {
1182 #[cfg(not(feature = "sqlite"))]
1186 anyhow::bail!(
1187 "{}: this quarb-session was built without the `sqlite` feature",
1188 path.display()
1189 );
1190 #[cfg(feature = "sqlite")]
1191 {
1192 let a = quarb_sqlite::SqliteAdapter::open_with_refs(path, &opts.refs)
1193 .context("opening SQLite database")?;
1194 return Ok(Doc::Sqlite(a));
1195 }
1196 }
1197 if is_archive(path) {
1198 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
1199 return Ok(if opts.no_graft {
1200 Doc::ArchiveRaw(a)
1201 } else {
1202 Doc::Archive(quarb_compose::ComposeAdapter::new(a))
1203 });
1204 }
1205
1206 let text =
1208 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
1209 let text = text
1210 .strip_prefix('\u{feff}')
1211 .map(str::to_owned)
1212 .unwrap_or(text);
1213 match ext.as_deref() {
1214 Some("csv") => Doc::parse(&text, "csv"),
1215 Some("tsv") => Doc::parse(&text, "tsv"),
1216 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
1217 Some("toml") => Doc::parse(&text, "toml"),
1218 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
1219 Some("txt") => Doc::parse(&text, "text"),
1220 Some("conllu" | "conllup") => Doc::parse(&text, "conllu"),
1221 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
1222 _ => {
1223 if is_xml(path, &text) {
1224 Doc::parse(&text, "xml")
1225 } else if is_html(path, &text) {
1226 Doc::parse(&text, "html")
1227 } else {
1228 Doc::parse(&text, "json")
1229 }
1230 }
1231 }
1232 }
1233
1234 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
1238 let specs: Vec<crate::MountSpec> = paths
1239 .iter()
1240 .map(|p| crate::MountSpec {
1241 name: None,
1242 path: p.clone(),
1243 })
1244 .collect();
1245 Doc::mount_specs(&specs, opts)
1246 }
1247
1248 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
1251 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
1252 for (i, spec) in specs.iter().enumerate() {
1253 let name = spec.name.clone().unwrap_or_else(|| {
1254 spec.path
1255 .file_stem()
1256 .map(|s| s.to_string_lossy().into_owned())
1257 .unwrap_or_else(|| format!("doc{i}"))
1258 });
1259 if mounts.iter().any(|m| m.name == name) {
1260 bail!(
1261 "input '{}' mounts as '{name}', colliding with an earlier input of the \
1262 same name; give each a distinct basename (or a NAME=TARGET alias)",
1263 spec.path.display()
1264 );
1265 }
1266 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
1267 mounts.push(quarb_mount::Mount {
1268 name,
1269 target: Some(spec.path.display().to_string()),
1270 adapter,
1271 });
1272 }
1273 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
1274 }
1275}
1276
1277fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
1281 let mut parts = Vec::new();
1282 let mut cur = Some(node);
1283 while let Some(n) = cur {
1284 if let Some(nm) = a.name(n) {
1285 parts.push(nm);
1286 }
1287 cur = a.parent(n);
1288 }
1289 parts.reverse();
1290 format!("/{}", parts.join("/"))
1291}
1292
1293#[cfg(feature = "native")]
1296fn is_sqlite(path: &Path) -> bool {
1297 if path
1298 .extension()
1299 .and_then(|e| e.to_str())
1300 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
1301 {
1302 return true;
1303 }
1304 use std::io::Read as _;
1305 let mut buf = [0u8; 16];
1306 std::fs::File::open(path)
1307 .and_then(|mut f| f.read_exact(&mut buf))
1308 .is_ok()
1309 && &buf == b"SQLite format 3\0"
1310}
1311
1312#[cfg(feature = "native")]
1314fn is_archive(path: &Path) -> bool {
1315 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
1316 matches!(
1317 e.to_ascii_lowercase().as_str(),
1318 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
1319 )
1320 }) {
1321 return true;
1322 }
1323 use std::io::Read as _;
1324 let mut buf = [0u8; 2];
1325 std::fs::File::open(path)
1326 .and_then(|mut f| f.read_exact(&mut buf))
1327 .is_ok()
1328 && (&buf == b"PK" || buf == [0x1f, 0x8b])
1329}
1330
1331#[cfg(feature = "native")]
1334fn is_xml(path: &Path, text: &str) -> bool {
1335 path.extension()
1336 .and_then(|e| e.to_str())
1337 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
1338 || text.trim_start().starts_with("<?xml")
1339}
1340
1341#[cfg(feature = "native")]
1344fn is_html(path: &Path, text: &str) -> bool {
1345 path.extension()
1346 .and_then(|e| e.to_str())
1347 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
1348 || text.trim_start().starts_with('<')
1349}
1350
1351fn treebank_options(
1357 mut model: quarb_text::TextModel,
1358 cast: &Option<String>,
1359 places: &Option<String>,
1360 modernize: &Option<String>,
1361) -> Result<Doc> {
1362 if let Some(t) = modernize {
1363 model
1364 .set_orthography(t)
1365 .map_err(|e| anyhow::anyhow!("corpus: {e}"))?;
1366 }
1367 if let Some(f) = cast {
1368 let rows = quarb_text::CastRow::read_csv(f)
1369 .map_err(|e| anyhow::anyhow!("corpus: reading the cast table {f}: {e}"))?;
1370 model.apply_cast(&rows);
1371 }
1372 if let Some(f) = places {
1373 let rows = quarb_text::CastRow::read_csv(f)
1374 .map_err(|e| anyhow::anyhow!("corpus: reading the places table {f}: {e}"))?;
1375 model.apply_places(&rows);
1376 }
1377 Ok(Doc::Text(model))
1378}
1379
1380fn finish_corpus(
1381 mut model: quarb_text::TextModel,
1382 conllu: Option<String>,
1383 annotate: Option<String>,
1384 desm: &[String],
1385 cast: Option<String>,
1386 places: Option<String>,
1387 modernize: Option<String>,
1388 opts: &Options,
1389 level: &str,
1390) -> Result<Doc> {
1391 if let Some(table) = &modernize {
1392 model
1393 .set_orthography(table)
1394 .map_err(|e| anyhow::anyhow!("{level}: {e}"))?;
1395 }
1396 let conllu_text = match (conllu, annotate) {
1397 (Some(_), Some(_)) => bail!("{level}: takes conllu= or annotate=, not both"),
1398 (Some(file), None) => Some(
1399 std::fs::read_to_string(&file).with_context(|| format!("reading {file} as CoNLL-U"))?,
1400 ),
1401 (None, Some(cmd)) => {
1402 if !opts.allow_shell {
1403 bail!("{level}: annotate= runs a command; the shell gate is closed");
1404 }
1405 use std::io::Write;
1406 let mut child = std::process::Command::new("sh")
1407 .arg("-c")
1408 .arg(&cmd)
1409 .stdin(std::process::Stdio::piped())
1410 .stdout(std::process::Stdio::piped())
1411 .spawn()
1412 .with_context(|| format!("running annotator {cmd:?}"))?;
1413 if let Some(mut stdin) = child.stdin.take() {
1414 stdin
1415 .write_all(model.corpus_text().as_bytes())
1416 .context("feeding the annotator")?;
1417 }
1418 let out = child
1419 .wait_with_output()
1420 .context("waiting for the annotator")?;
1421 if !out.status.success() {
1422 bail!("annotator {cmd:?} failed: {}", out.status);
1423 }
1424 Some(String::from_utf8(out.stdout).context("annotator output is not UTF-8")?)
1425 }
1426 (None, None) => None,
1427 };
1428 match conllu_text {
1429 Some(text) => model
1430 .annotate_conllu(&text)
1431 .map_err(|e| anyhow::anyhow!("{level}: {e}"))?,
1432 None => {
1433 let mut bonds = opts.bonds.as_ref().map(|b| (**b).clone());
1434 for f in desm {
1435 let more = syndesmos::Syndesmos::load(f)
1436 .map_err(|e| anyhow::anyhow!("reading {f}: {e}"))?;
1437 bonds
1438 .get_or_insert_with(syndesmos::Syndesmos::empty)
1439 .extend(more);
1440 }
1441 model.tokenize_with(bonds.as_ref())
1442 }
1443 }
1444 if let Some(file) = cast {
1445 let rows = quarb_text::CastRow::read_csv(&file)
1446 .map_err(|e| anyhow::anyhow!("{level}: reading the cast table {file}: {e}"))?;
1447 model.apply_cast(&rows);
1448 }
1449 if let Some(file) = places {
1450 let rows = quarb_text::CastRow::read_csv(&file)
1451 .map_err(|e| anyhow::anyhow!("{level}: reading the places table {file}: {e}"))?;
1452 model.apply_places(&rows);
1453 }
1454 Ok(Doc::Text(model))
1455}