use anyhow::{Context, Result, bail};
use quarb::{NodeId, Outcome, QueryResult, Run};
#[cfg(feature = "native")]
use std::path::Path;
use std::rc::Rc;
#[derive(Clone, Default)]
pub struct Options {
pub hidden: bool,
pub respect_ignore: bool,
pub graft: bool,
pub no_graft: bool,
pub refs: Option<std::path::PathBuf>,
pub allow_shell: bool,
pub bonds: Option<Rc<syndesmos::Syndesmos>>,
}
#[cfg(feature = "native")]
impl Options {
pub fn open_opts(&self) -> quarb_open::OpenOpts {
quarb_open::OpenOpts {
hidden: self.hidden,
no_ignore: !self.respect_ignore,
graft: self.graft,
no_graft: self.no_graft,
refs: self.refs.clone(),
allow_shell: self.allow_shell,
}
}
}
pub enum Doc {
Json(quarb_json::JsonAdapter),
Csv(quarb_csv::CsvAdapter),
Xml(quarb_xml::XmlAdapter),
Html(quarb_html::HtmlAdapter),
Text(quarb_text::TextModel),
#[cfg(feature = "sqlite")]
Sqlite(quarb_sqlite::SqliteAdapter),
Mount(quarb_mount::MountAdapter),
Boxed(Dyn, Box<dyn Fn(NodeId) -> String>, &'static str),
}
pub struct Dyn(pub Box<dyn quarb::AstAdapter>);
impl quarb::AstAdapter for Dyn {
quarb::forward_ast_adapter!(0);
}
impl Doc {
fn boxed_kaiv(a: quarb_kaiv::KaivAdapter) -> Doc {
let a = std::rc::Rc::new(a);
let r = a.clone();
Doc::Boxed(
Dyn(Box::new(quarb_mount::Shared(a))),
Box::new(move |n| r.locator(n)),
"kaiv",
)
}
#[cfg(feature = "native")]
fn boxed_code(m: quarb_code::CodeModel) -> Doc {
let a = std::rc::Rc::new(m);
let r = a.clone();
Doc::Boxed(
Dyn(Box::new(quarb_mount::Shared(a))),
Box::new(move |n| r.locator(n)),
"code",
)
}
pub fn parse(input: &str, format: &str) -> Result<Doc> {
match format {
"kaiv" => {
let a = quarb_kaiv::KaivAdapter::parse_kaiv(input)
.map_err(|e| anyhow::anyhow!("parsing kaiv: {e}"))?;
return Ok(Self::boxed_kaiv(a));
}
"daiv" => {
let a = quarb_kaiv::KaivAdapter::parse_daiv(input)
.map_err(|e| anyhow::anyhow!("parsing daiv: {e}"))?;
return Ok(Self::boxed_kaiv(a));
}
"json" => quarb_json::JsonAdapter::parse(input)
.map(Doc::Json)
.context("parsing JSON"),
"json2" => quarb_json::JsonAdapter::parse_as(input, quarb_json::Reading::Property)
.map(Doc::Json)
.context("parsing JSON"),
"jsonl" | "ndjson" => quarb_json::JsonAdapter::parse_lines(input)
.map(Doc::Json)
.context("parsing JSONL"),
"yaml" | "yml" => quarb_yaml::parse(input)
.map(Doc::Json)
.context("parsing YAML"),
"toml" => quarb_toml::parse(input)
.map(Doc::Json)
.context("parsing TOML"),
"csv" => quarb_csv::CsvAdapter::parse_declared(input, b',', Default::default())
.map(Doc::Csv)
.context("parsing CSV"),
"tsv" => quarb_csv::CsvAdapter::parse_declared(input, b'\t', Default::default())
.map(Doc::Csv)
.context("parsing TSV"),
"xml" => quarb_xml::XmlAdapter::parse(input)
.map(Doc::Xml)
.context("parsing XML"),
"html" => Ok(Doc::Html(quarb_html::HtmlAdapter::parse(input))),
"markdown" | "md" => Ok(Doc::Html(quarb_markdown::parse(input))),
"tex" | "latex" => Ok(Doc::Text(quarb_text_latex::parse(input))),
"text-html" => Ok(Doc::Text(quarb_text_html::parse(input))),
"text-markdown" | "text-md" => Ok(Doc::Text(quarb_text_markdown::parse(input))),
"text" => Ok(Doc::Text(quarb_text::TextModel::parse_plain(input))),
"text-rtf" => quarb_text_rtf::parse(input.as_bytes())
.map(Doc::Text)
.map_err(|e| anyhow::anyhow!("reading RTF: {e}")),
"conllu" => quarb_text::TextModel::parse_conllu_text(input)
.map(Doc::Text)
.map_err(|e| anyhow::anyhow!("reading CoNLL-U: {e}")),
"corpus-conllu" => quarb_text::TextModel::parse_conllu_corpus(input)
.map(Doc::Text)
.map_err(|e| anyhow::anyhow!("reading CoNLL-U: {e}")),
"corpus-html" | "corpus-markdown" | "corpus" => {
let mut model = match format {
"corpus-html" => quarb_text_html::parse(input),
"corpus-markdown" => quarb_text_markdown::parse(input),
_ => quarb_text::TextModel::parse_plain(input),
};
model.tokenize();
Ok(Doc::Text(model))
}
#[cfg(feature = "native")]
"text-bibtex" | "bibtex" => quarb_text_koine::parse_bibtex(input)
.map(Doc::Text)
.map_err(|e| anyhow::anyhow!("{e}")),
#[cfg(feature = "native")]
"text-fb2" | "fb2" => quarb_text_koine::parse_xml_as(input, "fb2")
.map(Doc::Text)
.map_err(|e| anyhow::anyhow!("{e}")),
#[cfg(feature = "native")]
"code-rust" => quarb_code::CodeModel::parse(input, "rs")
.map(Doc::boxed_code)
.context("parsing Rust at the code level"),
#[cfg(feature = "native")]
"code-python" => quarb_code::CodeModel::parse(input, "py")
.map(Doc::boxed_code)
.context("parsing Python at the code level"),
#[cfg(feature = "native")]
"code-javascript" => quarb_code::CodeModel::parse(input, "js")
.map(Doc::boxed_code)
.context("parsing JavaScript at the code level"),
#[cfg(feature = "native")]
"code-c" => quarb_code::CodeModel::parse(input, "c")
.map(Doc::boxed_code)
.context("parsing C at the code level"),
other => bail!("unknown format: {other}"),
}
}
pub(crate) fn base_dyn(&self) -> &dyn quarb::AstAdapter {
match self {
Doc::Json(a) => a,
Doc::Csv(a) => a,
Doc::Xml(a) => a,
Doc::Html(a) => a,
Doc::Text(a) => a,
#[cfg(feature = "sqlite")]
Doc::Sqlite(a) => a,
Doc::Mount(a) => a,
Doc::Boxed(a, _, _) => &*a.0,
}
}
pub fn export(&self, query: &str, run: &Run, kind: &str) -> Result<String> {
let render = quarb_text::Render::from_name(kind)
.ok_or_else(|| anyhow::anyhow!("unknown export format: {kind} (md, html, txt)"))?;
if query.trim().is_empty() {
let base = self.base_dyn();
return Ok(quarb_text::render_nodes(base, &[base.root()], render));
}
match self
.run(query, run)
.map_err(|e| anyhow::anyhow!("{e}"))?
.result
{
QueryResult::Nodes(nodes) => {
Ok(quarb_text::render_nodes(self.base_dyn(), &nodes, render))
}
QueryResult::Values(values) => Ok(quarb_text::render::render_values(&values, render)),
}
}
pub fn run_modeled(
&self,
query: &str,
run: &Run,
model: &quarb_model::Model,
) -> quarb::Result<Outcome> {
let base = quarb_model::Borrowed(self.base_dyn());
let enriched = quarb_model::ModelAdapter::new(base, model.clone());
enriched
.check()
.map_err(|e| quarb::QuarbError::parse(format!("reading the model: {e}")))?;
quarb::Query::parse_with(query, &quarb::Defs::default(), &enriched, run)?
.run(&enriched, run)
}
pub fn render_modeled(&self, node: NodeId, model: &quarb_model::Model) -> String {
let enriched =
quarb_model::ModelAdapter::new(quarb_model::Borrowed(self.base_dyn()), model.clone());
enriched.locator(node, |bn| self.render(bn))
}
pub fn run(&self, query: &str, run: &Run) -> quarb::Result<Outcome> {
macro_rules! go {
($a:expr) => {{ quarb::Query::parse_with(query, &quarb::Defs::default(), $a, run)?.run($a, run) }};
}
match self {
Doc::Json(a) => go!(a),
Doc::Csv(a) => go!(a),
Doc::Xml(a) => go!(a),
Doc::Html(a) => go!(a),
Doc::Text(a) => go!(a),
#[cfg(feature = "sqlite")]
Doc::Sqlite(a) => go!(a),
Doc::Mount(a) => go!(a),
Doc::Boxed(a, _, _) => go!(a),
}
}
pub fn family(&self) -> &'static str {
match self {
Doc::Json(_) => "json",
Doc::Csv(_) => "csv",
Doc::Xml(_) => "xml",
Doc::Html(_) => "html",
Doc::Text(_) => "text",
#[cfg(feature = "sqlite")]
Doc::Sqlite(_) => "sqlite",
Doc::Mount(_) => "mount",
Doc::Boxed(_, _, family) => family,
}
}
pub fn trace(
&self,
committed: &str,
staged: Option<&str>,
opts: &quarb::trace::Options,
) -> quarb::Result<quarb_trace::Payload> {
let base = quarb_model::Borrowed(self.base_dyn());
quarb::trace::trace(committed, staged, &base, &|n| self.render(n), opts)
}
pub fn render(&self, node: NodeId) -> String {
match self {
Doc::Json(a) => a.pointer(node),
Doc::Csv(a) => a.locator(node),
Doc::Xml(a) => a.locator(node),
Doc::Html(a) => a.locator(node),
Doc::Text(a) => a.locator(node),
#[cfg(feature = "sqlite")]
Doc::Sqlite(a) => a.locator(node),
Doc::Mount(a) => generic_locator(a, node),
Doc::Boxed(_, render, _) => render(node),
}
}
#[cfg(feature = "sqlite")]
pub fn sqlite_bytes(bytes: &[u8]) -> Result<Doc> {
Ok(Doc::Sqlite(
quarb_sqlite::SqliteAdapter::from_bytes(bytes)
.map_err(|e| anyhow::anyhow!("{e}"))
.context("opening SQLite bytes")?,
))
}
#[cfg(not(feature = "sqlite"))]
pub fn sqlite_bytes(_bytes: &[u8]) -> Result<Doc> {
anyhow::bail!("this quarb-session was built without the `sqlite` feature")
}
pub fn mount_docs(parts: Vec<(String, Doc)>) -> Result<Doc> {
let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
for (name, doc) in parts {
if mounts.iter().any(|m| m.name == name) {
bail!("two sources mount as '{name}'; give each a distinct name");
}
mounts.push(quarb_mount::Mount {
name,
target: None,
adapter: doc.into_boxed()?,
});
}
Ok(Doc::Mount(
quarb_mount::MountAdapter::try_new(mounts)
.map_err(|e| anyhow::anyhow!("mount: {e}"))?,
))
}
pub fn attach_url(&mut self, url: &str) {
match self {
Doc::Html(a) => a.set_document_url(url),
Doc::Text(a) => a.set_document_url(url),
_ => {}
}
}
pub fn url_roots(
&self,
urls: &[(String, String)],
) -> std::collections::HashMap<String, quarb::NodeId> {
let a = self.base_dyn();
let root = a.root();
match self {
Doc::Mount(_) => urls
.iter()
.filter_map(|(name, url)| {
a.children_named(root, name)
.first()
.map(|&n| (url.clone(), n))
})
.collect(),
_ => urls
.iter()
.take(1)
.map(|(_, url)| (url.clone(), root))
.collect(),
}
}
pub fn declare_identities(&mut self, urls: &[(String, String)]) {
if let Doc::Mount(m) = self {
for (name, url) in urls {
if let Some(idx) = (0..m.len()).find(|&i| m.mount_name(i) == name) {
m.declare_identity(idx, url.clone());
}
}
}
}
pub fn mount_texts(parts: &[(String, String, String)]) -> Result<Doc> {
let mut docs: Vec<(String, Doc)> = Vec::new();
for (name, format, text) in parts {
let doc = Doc::parse(text, format)
.with_context(|| format!("parsing '{name}' as {format}"))?;
docs.push((name.clone(), doc));
}
Doc::mount_docs(docs)
}
fn into_boxed(self) -> Result<Box<dyn quarb::AstAdapter>> {
use quarb_mount::Shared;
Ok(match self {
Doc::Json(a) => Box::new(Shared(Rc::new(a))),
Doc::Csv(a) => Box::new(Shared(Rc::new(a))),
Doc::Xml(a) => Box::new(Shared(Rc::new(a))),
Doc::Html(a) => Box::new(Shared(Rc::new(a))),
Doc::Text(a) => Box::new(Shared(Rc::new(a))),
#[cfg(feature = "sqlite")]
Doc::Sqlite(a) => Box::new(Shared(Rc::new(a))),
Doc::Mount(_) => bail!("cannot nest a mount inside a mount"),
Doc::Boxed(a, _, _) => a.0,
})
}
}
#[cfg(feature = "native")]
impl Doc {
pub fn open(path: &Path, opts: &Options) -> Result<Doc> {
let target = path.to_string_lossy();
let m = quarb_open::open_target(&target, &opts.open_opts())?;
Ok(Doc::Boxed(Dyn(m.adapter), m.render, m.family))
}
pub fn mount(paths: &[std::path::PathBuf], opts: &Options) -> Result<Doc> {
let specs: Vec<crate::MountSpec> = paths
.iter()
.map(|p| crate::MountSpec {
name: None,
path: p.clone(),
})
.collect();
Doc::mount_specs(&specs, opts)
}
pub fn mount_specs(specs: &[crate::MountSpec], opts: &Options) -> Result<Doc> {
let mut mounts: Vec<quarb_mount::Mount> = Vec::new();
for (i, spec) in specs.iter().enumerate() {
let name = spec.name.clone().unwrap_or_else(|| {
spec.path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("doc{i}"))
});
if mounts.iter().any(|m| m.name == name) {
bail!(
"input '{}' mounts as '{name}', colliding with an earlier input of the \
same name; give each a distinct basename (or a NAME=TARGET alias)",
spec.path.display()
);
}
let adapter = Doc::open(&spec.path, opts)?.into_boxed()?;
mounts.push(quarb_mount::Mount {
name,
target: Some(spec.path.display().to_string()),
adapter,
});
}
Ok(Doc::Mount(
quarb_mount::MountAdapter::try_new(mounts)
.map_err(|e| anyhow::anyhow!("mount: {e}"))?,
))
}
}
fn generic_locator<A: quarb::AstAdapter>(a: &A, node: NodeId) -> String {
let mut parts = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
if let Some(nm) = a.name(n) {
parts.push(nm);
}
cur = a.parent(n);
}
parts.reverse();
format!("/{}", parts.join("/"))
}