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 "text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
193 "text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
194 "text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
195 #[cfg(feature = "native")]
198 "code-rust" => quarb_code::CodeModel::parse(input, "rs")
199 .map(Doc::Code)
200 .context("parsing Rust at the code level"),
201 #[cfg(feature = "native")]
202 "code-python" => quarb_code::CodeModel::parse(input, "py")
203 .map(Doc::Code)
204 .context("parsing Python at the code level"),
205 #[cfg(feature = "native")]
206 "code-javascript" => quarb_code::CodeModel::parse(input, "js")
207 .map(Doc::Code)
208 .context("parsing JavaScript at the code level"),
209 #[cfg(feature = "native")]
210 "code-c" => quarb_code::CodeModel::parse(input, "c")
211 .map(Doc::Code)
212 .context("parsing C at the code level"),
213 other => bail!("unknown format: {other}"),
214 }
215 }
216
217 pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
226 match self {
227 Doc::Json(a) => a,
228 Doc::Csv(a) => a,
229 Doc::Xml(a) => a,
230 Doc::Html(a) => a,
231 Doc::Text(a) => a,
232 #[cfg(feature = "sqlite")]
233 Doc::Sqlite(a) => a,
234 #[cfg(feature = "native")]
235 Doc::Fs(a) => a,
236 #[cfg(feature = "native")]
237 Doc::FsDeep(a) => a,
238 #[cfg(feature = "native")]
239 Doc::Git(a) => a,
240 #[cfg(feature = "native")]
241 Doc::Archive(a) => a,
242 #[cfg(feature = "native")]
243 Doc::ArchiveRaw(a) => a,
244 #[cfg(feature = "native")]
245 Doc::Xlsx(a) => a,
246 #[cfg(feature = "native")]
247 Doc::Syntax(a) => a,
248 #[cfg(feature = "native")]
249 Doc::Code(a) => a,
250 Doc::Mount(a) => a,
251 Doc::Boxed(a, _) => &*a.0,
252 }
253 }
254
255 pub fn export(
261 &self,
262 query: &str,
263 now: (i64, u32),
264 allow_shell: bool,
265 kind: &str,
266 ) -> Result<String> {
267 let render = quarb_text::Render::from_name(kind)
268 .ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
269 if query.trim().is_empty() {
271 let base = self.base_dyn();
272 return Ok(quarb_text::render_nodes(base, &[base.root()], render));
273 }
274 match self
275 .run(query, now, allow_shell)
276 .map_err(|e| anyhow::anyhow!("{e}"))?
277 {
278 QueryResult::Nodes(nodes) => {
279 Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
280 }
281 QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
282 }
283 }
284
285 pub fn run_modeled(
290 &self,
291 query: &str,
292 now: (i64, u32),
293 allow_shell: bool,
294 model: &quarb_model::Model,
295 ) -> quarb::Result<QueryResult> {
296 let (secs, nanos) = now;
297 let base = quarb_model::Borrowed(self.base_dyn());
298 let nowed = WithNow {
299 inner: &base,
300 secs,
301 nanos,
302 };
303 let enriched = quarb_model::ModelAdapter::new(nowed, model.clone());
304 if allow_shell {
305 quarb::run(query, &AllowShell { inner: &enriched })
306 } else {
307 quarb::run(query, &enriched)
308 }
309 }
310
311 pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
314 let enriched =
315 quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
316 enriched.locator(node, |bn| self.render(bn))
317 }
318
319 pub fn run(&self, query: &str, now: (i64, u32), allow_shell: bool) -> quarb::Result<QueryResult> {
320 let (secs, nanos) = now;
321 macro_rules! go {
322 ($a:expr) => {{
323 let nowed = WithNow {
324 inner: $a,
325 secs,
326 nanos,
327 };
328 if allow_shell {
329 quarb::run(query, &AllowShell { inner: &nowed })
330 } else {
331 quarb::run(query, &nowed)
332 }
333 }};
334 }
335 match self {
336 Doc::Json(a) => go!(a),
337 Doc::Csv(a) => go!(a),
338 Doc::Xml(a) => go!(a),
339 Doc::Html(a) => go!(a),
340 Doc::Text(a) => go!(a),
341 #[cfg(feature = "sqlite")]
342 Doc::Sqlite(a) => go!(a),
343 #[cfg(feature = "native")]
344 Doc::Fs(a) => go!(a),
345 #[cfg(feature = "native")]
346 Doc::FsDeep(a) => go!(a),
347 #[cfg(feature = "native")]
348 Doc::Git(a) => go!(a),
349 #[cfg(feature = "native")]
350 Doc::Archive(a) => go!(a),
351 #[cfg(feature = "native")]
352 Doc::ArchiveRaw(a) => go!(a),
353 #[cfg(feature = "native")]
354 Doc::Xlsx(a) => go!(a),
355 #[cfg(feature = "native")]
356 Doc::Syntax(a) => go!(a),
357 #[cfg(feature = "native")]
358 Doc::Code(a) => go!(a),
359 Doc::Mount(a) => go!(a),
360 Doc::Boxed(a, _) => go!(a),
361 }
362 }
363
364 pub fn render(&self, node: NodeId) -> String {
366 match self {
367 Doc::Json(a) => a.pointer(node),
368 Doc::Csv(a) => a.locator(node),
369 Doc::Xml(a) => a.locator(node),
370 Doc::Html(a) => a.locator(node),
371 Doc::Text(a) => a.locator(node),
372 #[cfg(feature = "sqlite")]
373 Doc::Sqlite(a) => a.locator(node),
374 #[cfg(feature = "native")]
375 Doc::Fs(a) => a.path(node).display().to_string(),
376 #[cfg(feature = "native")]
377 Doc::FsDeep(a) => a.locator(node, |o| a.outer().path(o).display().to_string()),
378 #[cfg(feature = "native")]
379 Doc::Git(a) => a.locator(node),
380 #[cfg(feature = "native")]
381 Doc::Archive(a) => a.locator(node, |o| a.outer().locator(o)),
382 #[cfg(feature = "native")]
383 Doc::ArchiveRaw(a) => a.locator(node),
384 #[cfg(feature = "native")]
385 Doc::Xlsx(a) => a.locator(node),
386 #[cfg(feature = "native")]
387 Doc::Syntax(a) => a.locator(node),
388 #[cfg(feature = "native")]
389 Doc::Code(a) => a.locator(node),
390 Doc::Mount(a) => generic_locator(a, node),
391 Doc::Boxed(_, render) => render(node),
392 }
393 }
394
395 #[cfg(feature = "sqlite")]
398 pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
399 Ok(Doc::Sqlite(
400 quarb_sqlite::SqliteAdapter::from_bytes(bytes)
401 .map_err(|e| anyhow::anyhow!("{e}"))
402 .context("opening SQLite bytes")?,
403 ))
404 }
405
406 #[cfg(not(feature = "sqlite"))]
410 pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
411 anyhow::bail!(
412 "this quarb-session was built without the `sqlite` feature"
413 )
414 }
415
416 pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
420 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
421 for (name, doc) in parts {
422 if mounts.iter().any(|m| m.name == name) {
423 bail!("two sources mount as '{name}'; give each a distinct name");
424 }
425 mounts.push(quarb_mount::Mount {
426 name,
427 target: None,
430 adapter: doc.into_boxed()?,
431 });
432 }
433 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
434 }
435
436 pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
441 let mut docs: Vec<(String, Doc)> = Vec::new();
442 for (name, format, text) in parts {
443 let doc = Doc::parse(text, format)
444 .with_context(|| format!("parsing '{name}' as {format}"))?;
445 docs.push((name.clone(), doc));
446 }
447 Doc::mount_docs(docs)
448 }
449
450 fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
452 use quarb_mount::Shared;
453 Ok(match self {
454 Doc::Json(a) => Box::new(Shared(Rc::new(a))),
455 Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
456 Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
457 Doc::Html(a) => Box::new(Shared(Rc::new(a))),
458 Doc::Text(a) => Box::new(Shared(Rc::new(a))),
459 #[cfg(feature = "sqlite")]
460 Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
461 #[cfg(feature = "native")]
462 Doc::Fs(a) => Box::new(Shared(Rc::new(a))),
463 #[cfg(feature = "native")]
464 Doc::FsDeep(a) => Box::new(Shared(Rc::new(a))),
465 #[cfg(feature = "native")]
466 Doc::Git(a) => Box::new(Shared(Rc::new(a))),
467 #[cfg(feature = "native")]
468 Doc::Archive(a) => Box::new(Shared(Rc::new(a))),
469 #[cfg(feature = "native")]
470 Doc::ArchiveRaw(a) => Box::new(Shared(Rc::new(a))),
471 #[cfg(feature = "native")]
472 Doc::Xlsx(a) => Box::new(Shared(Rc::new(a))),
473 #[cfg(feature = "native")]
474 Doc::Syntax(a) => Box::new(Shared(Rc::new(a))),
475 #[cfg(feature = "native")]
476 Doc::Code(a) => Box::new(Shared(Rc::new(a))),
477 Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
478 Doc::Boxed(a, _) => a.0,
479 })
480 }
481}
482
483#[cfg(feature = "native")]
488impl Doc {
489 pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
495 if path.is_dir() {
496 let fsopts = quarb_fs::FsOptions {
497 hidden: opts.hidden,
498 respect_ignore: opts.respect_ignore,
499 };
500 let fs = quarb_fs::FsAdapter::with_options(path, fsopts)
501 .with_context(|| format!("opening directory {}", path.display()))?;
502 return Ok(if opts.graft {
503 Doc::FsDeep(quarb_compose::ComposeAdapter::with_source_paths(
504 fs,
505 |fs, n| Some(fs.path(n)),
506 ))
507 } else {
508 Doc::Fs(fs)
509 });
510 }
511
512 let s = path.to_string_lossy();
513 if let Some(repo) = s.strip_prefix("git:") {
514 let a =
515 quarb_git::GitAdapter::open(Path::new(repo)).context("opening git repository")?;
516 return Ok(Doc::Git(a));
517 }
518 if let Some(rest) = s.strip_prefix("text:")
522 && !rest.is_empty()
523 {
524 let target = Path::new(rest);
525 let text = std::fs::read_to_string(target)
526 .with_context(|| format!("reading {}", target.display()))?;
527 let text = text
528 .strip_prefix('\u{feff}')
529 .map(str::to_owned)
530 .unwrap_or(text);
531 let format = match target
532 .extension()
533 .and_then(|e| e.to_str())
534 .map(|e| e.to_ascii_lowercase())
535 .as_deref()
536 {
537 Some("html" | "htm") => "text-html",
538 Some("md" | "markdown") => "text-markdown",
539 Some("txt") => "text",
540 _ if text.trim_start().starts_with('<') => "text-html",
541 _ => "text",
542 };
543 return Doc::parse(&text, format);
544 }
545 if let Some(rest) = s.strip_prefix("code:")
550 && !rest.is_empty()
551 {
552 if opts.no_graft {
553 bail!(
554 "no_graft refuses the code: prefix: the prefix's whole \
555 meaning is the grafted code-level view"
556 );
557 }
558 let target = Path::new(rest);
559 if target.is_dir() {
560 let fsopts = quarb_fs::FsOptions {
561 hidden: opts.hidden,
562 respect_ignore: opts.respect_ignore,
563 };
564 let fs = quarb_fs::FsAdapter::with_options(target, fsopts)
565 .with_context(|| format!("opening directory {}", target.display()))?;
566 return Ok(Doc::FsDeep(
567 quarb_compose::ComposeAdapter::with_source_paths(fs, |fs, n| {
568 Some(fs.path(n))
569 })
570 .with_source_graft(quarb_compose::SourceGraft::Code),
571 ));
572 }
573 let a = quarb_code::CodeModel::open(target)
574 .with_context(|| format!("parsing {} at the code level", target.display()))?;
575 return Ok(Doc::Code(a));
576 }
577
578 let ext = path
579 .extension()
580 .and_then(|e| e.to_str())
581 .map(|e| e.to_ascii_lowercase());
582
583 if let Some(e) = &ext
584 && quarb_tree_sitter::supported(e)
585 {
586 let a = quarb_tree_sitter::TreeSitterAdapter::open(path).context("parsing source file")?;
587 return Ok(Doc::Syntax(a));
588 }
589 if matches!(ext.as_deref(), Some("xlsx" | "xls" | "ods")) {
590 let a = quarb_xlsx::XlsxAdapter::open(path).context("opening workbook")?;
591 return Ok(Doc::Xlsx(a));
592 }
593 if is_sqlite(path) {
594 #[cfg(not(feature = "sqlite"))]
598 anyhow::bail!(
599 "{}: this quarb-session was built without the `sqlite` feature",
600 path.display()
601 );
602 #[cfg(feature = "sqlite")]
603 {
604 let a = quarb_sqlite::SqliteAdapter::open_with_refs(path, &opts.refs)
605 .context("opening SQLite database")?;
606 return Ok(Doc::Sqlite(a));
607 }
608 }
609 if is_archive(path) {
610 let a = quarb_archive::ArchiveAdapter::open(path).context("opening archive")?;
611 return Ok(if opts.no_graft {
612 Doc::ArchiveRaw(a)
613 } else {
614 Doc::Archive(quarb_compose::ComposeAdapter::new(a))
615 });
616 }
617
618 let text = std::fs::read_to_string(path)
620 .with_context(|| format!("reading {}", path.display()))?;
621 let text = text
622 .strip_prefix('\u{feff}')
623 .map(str::to_owned)
624 .unwrap_or(text);
625 match ext.as_deref() {
626 Some("csv") => Doc::parse(&text, "csv"),
627 Some("tsv") => Doc::parse(&text, "tsv"),
628 Some("yaml" | "yml") => Doc::parse(&text, "yaml"),
629 Some("toml") => Doc::parse(&text, "toml"),
630 Some("md" | "markdown") => Doc::parse(&text, "markdown"),
631 Some("txt") => Doc::parse(&text, "text"),
632 Some("jsonl" | "ndjson") => Doc::parse(&text, "jsonl"),
633 _ => {
634 if is_xml(path, &text) {
635 Doc::parse(&text, "xml")
636 } else if is_html(path, &text) {
637 Doc::parse(&text, "html")
638 } else {
639 Doc::parse(&text, "json")
640 }
641 }
642 }
643 }
644
645 pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
649 let specs: Vec<crate::MountSpec> = paths
650 .iter()
651 .map(|p| crate::MountSpec {
652 name: None,
653 path: p.clone(),
654 })
655 .collect();
656 Doc::mount_specs(&specs, opts)
657 }
658
659 pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
662 let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
663 for (i, spec) in specs.iter().enumerate() {
664 let name = spec.name.clone().unwrap_or_else(|| {
665 spec.path
666 .file_stem()
667 .map(|s| s.to_string_lossy().into_owned())
668 .unwrap_or_else(|| format!("doc{i}"))
669 });
670 if mounts.iter().any(|m| m.name == name) {
671 bail!(
672 "input '{}' mounts as '{name}', colliding with an earlier input of the \
673 same name; give each a distinct basename (or a NAME=TARGET alias)",
674 spec.path.display()
675 );
676 }
677 let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
678 mounts.push(quarb_mount::Mount {
679 name,
680 target: Some(spec.path.display().to_string()),
681 adapter,
682 });
683 }
684 Ok(Doc::Mount(quarb_mount::MountAdapter::new(mounts)))
685 }
686
687}
688
689fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
693 let mut parts = Vec::new();
694 let mut cur = Some(node);
695 while let Some(n) = cur {
696 if let Some(nm) = a.name(n) {
697 parts.push(nm);
698 }
699 cur = a.parent(n);
700 }
701 parts.reverse();
702 format!("/{}", parts.join("/"))
703}
704
705#[cfg(feature = "native")]
708fn is_sqlite(path: &Path) -> bool {
709 if path
710 .extension()
711 .and_then(|e| e.to_str())
712 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "db" | "sqlite" | "sqlite3"))
713 {
714 return true;
715 }
716 use std::io::Read as _;
717 let mut buf = [0u8; 16];
718 std::fs::File::open(path)
719 .and_then(|mut f| f.read_exact(&mut buf))
720 .is_ok()
721 && &buf == b"SQLite format 3\0"
722}
723
724#[cfg(feature = "native")]
726fn is_archive(path: &Path) -> bool {
727 if path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
728 matches!(
729 e.to_ascii_lowercase().as_str(),
730 "zip" | "tar" | "gz" | "tgz" | "jar" | "war" | "docx" | "pptx" | "odt" | "odp"
731 )
732 }) {
733 return true;
734 }
735 use std::io::Read as _;
736 let mut buf = [0u8; 2];
737 std::fs::File::open(path)
738 .and_then(|mut f| f.read_exact(&mut buf))
739 .is_ok()
740 && (&buf == b"PK" || buf == [0x1f, 0x8b])
741}
742
743#[cfg(feature = "native")]
746fn is_xml(path: &Path, text: &str) -> bool {
747 path.extension()
748 .and_then(|e| e.to_str())
749 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "xml" | "svg" | "xhtml"))
750 || text.trim_start().starts_with("<?xml")
751}
752
753#[cfg(feature = "native")]
756fn is_html(path: &Path, text: &str) -> bool {
757 path.extension()
758 .and_then(|e| e.to_str())
759 .is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "html" | "htm"))
760 || text.trim_start().starts_with('<')
761}