use std::fmt::Write;
use comemo::Track;
use ecow::{EcoString, eco_format};
use typst::World;
use typst::diag::{HintedStrResult, SourceDiagnostic, StrResult, Warned, bail, warning};
use typst::engine::Sink;
use typst::foundations::{
Content, Context, IntoValue, LocatableSelector, Output, Repr, Scope,
};
use typst::introspection::{EmptyIntrospector, Introspector};
use typst::routines::SpanMode;
use typst::syntax::{Span, SyntaxMode};
use typst_bundle::Bundle;
use typst_eval::eval_string;
use typst_html::HtmlDocument;
use typst_layout::PagedDocument;
use crate::args::{Input, QueryCommand, Target};
use crate::compile::print_diagnostics;
use crate::set_failed;
use crate::world::SystemWorld;
pub fn query(command: &'static QueryCommand) -> HintedStrResult<()> {
let mut world =
SystemWorld::new(Some(&command.input), &command.world, &command.process)?;
world.reset();
world.source(world.main()).map_err(|err| err.to_string())?;
let Warned { output, mut warnings } = match command.target {
Target::Paged => typst::compile::<PagedDocument>(&world)
.map(|result| result.map(|output| Box::new(output) as Box<dyn Output>)),
Target::Html => typst::compile::<HtmlDocument>(&world)
.map(|result| result.map(|output| Box::new(output) as Box<dyn Output>)),
Target::Bundle => typst::compile::<Bundle>(&world)
.map(|result| result.map(|output| Box::new(output) as Box<dyn Output>)),
};
warnings.push(deprecation_warning(command));
match output {
Ok(output) => {
let data = retrieve(&world, command, output.introspector())?;
let serialized = format(data, command)?;
println!("{serialized}");
print_diagnostics(&world, &[], &warnings, command.process.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
}
Err(errors) => {
set_failed();
print_diagnostics(
&world,
&errors,
&warnings,
command.process.diagnostic_format,
)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
}
}
Ok(())
}
fn retrieve(
world: &dyn World,
command: &QueryCommand,
introspector: &dyn Introspector,
) -> HintedStrResult<Vec<Content>> {
let selector = eval_string(
world.track(),
world.library(),
Sink::new().track_mut(),
EmptyIntrospector.track(),
Context::none().track(),
&command.selector,
SpanMode::Uniform(Span::detached()),
SyntaxMode::Code,
Scope::default(),
)
.map_err(|errors| {
let mut message = EcoString::from("failed to evaluate selector");
for (i, error) in errors.into_iter().enumerate() {
message.push_str(if i == 0 { ": " } else { ", " });
message.push_str(&error.message);
}
message
})?
.cast::<LocatableSelector>()?;
Ok(introspector.query(&selector.0).into_iter().collect::<Vec<_>>())
}
fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
if command.one && elements.len() != 1 {
bail!("expected exactly one element, found {}", elements.len());
}
let mapped: Vec<_> = elements
.into_iter()
.filter_map(|c| match &command.field {
Some(field) => c.get_by_name(field).ok(),
_ => Some(c.into_value()),
})
.collect();
if command.one {
let Some(value) = mapped.first() else {
bail!("no such field found for element");
};
crate::serialize(value, command.format, command.pretty)
} else {
crate::serialize(&mapped, command.format, command.pretty)
}
}
fn deprecation_warning(command: &QueryCommand) -> SourceDiagnostic {
let query = {
let mut buf = format!("query({})", command.selector);
let access = |field: &str| {
if typst::syntax::is_ident(field) {
eco_format!(".{field}")
} else {
eco_format!(".at({})", field.repr())
}
};
match (command.one, &command.field) {
(false, None) => {}
(false, Some(field)) => {
write!(buf, ".map(it => it{})", access(field)).unwrap()
}
(true, None) => write!(buf, ".first()").unwrap(),
(true, Some(field)) => write!(buf, ".first(){}", access(field)).unwrap(),
}
shell_escape::escape(buf.into())
};
let eval_command = match &command.input {
Input::Path(path) => {
eco_format!("typst eval {query} --in {}", path.display())
}
Input::Stdin => eco_format!("typst eval {query}"),
};
warning!(
Span::detached(),
"the `typst query` subcommand is deprecated";
hint: "use `{eval_command}` instead";
)
}