// The `query` command (upstream `typst-cli/src/query.rs`).
///|
/// A compiled document of any target (upstream `Box`).
priv struct AnyOutput {
introspector : @library.Introspector
}
///|
/// Compiles the main file for a target.
fn compile_target(
world : &@library.World,
target : Target,
) -> @library.Warned[Result[AnyOutput, Array[@library.SourceDiagnostic]]] {
match target {
Paged => {
let w : @library.Warned[
Result[@layout.PagedDocument, Array[@library.SourceDiagnostic]],
] = @typst.compile(world)
{
output: w.output.map(d => { introspector: d.introspector(), }),
warnings: w.warnings,
}
}
Html => {
let w : @library.Warned[
Result[@html.HtmlDocument, Array[@library.SourceDiagnostic]],
] = @typst.compile(world)
{
output: w.output.map(d => {
introspector: @library.Output::introspector(d),
}),
warnings: w.warnings,
}
}
Bundle => {
let w : @library.Warned[
Result[@bundle.Bundle, Array[@library.SourceDiagnostic]],
] = @typst.compile(world)
{
output: w.output.map(d => {
introspector: @library.Output::introspector(d),
}),
warnings: w.warnings,
}
}
}
}
///|
/// Execute a query command.
async fn query(command : QueryCommand) -> Unit raise HintedFailure {
let world = SystemWorld::new(
Some(command.input),
command.world,
command.process,
) catch {
err => raise bail("\{err}")
}
// Reset everything and ensure that the main file is present.
world.reset()
(@library.World::source(world, @library.World::main(world)) |> ignore) catch {
err => raise bail("\{err}")
}
let warned = compile_target(world, command.target)
let warnings = warned.warnings.copy()
// Add deprecation warning.
warnings.push(deprecation_warning(command))
match warned.output {
// Retrieve and print query results.
Ok(output) => {
let data = retrieve(world, command, output.introspector)
let serialized = format_query(data, command)
print_out(serialized + "\n")
print_diagnostics(world, [], warnings, command.process.diagnostic_format) catch {
err => raise bail("failed to print diagnostics (\{err})")
}
}
// Print diagnostics.
Err(errors) => {
set_failed()
print_diagnostics(
world,
errors,
warnings,
command.process.diagnostic_format,
) catch {
err => raise bail("failed to print diagnostics (\{err})")
}
}
}
}
///|
/// Retrieve the matches for the selector.
fn retrieve(
world : SystemWorld,
command : QueryCommand,
introspector : @library.Introspector,
) -> Array[@library.Content] raise HintedFailure {
let library = @library.World::library(world)
let engine : @library.Engine = {
world: @library.TrackedWorld::new(world),
library,
introspector: @library.Introspector::empty(),
traced: @library.Traced::new(),
// TODO: propagate warnings
sink: @library.Sink::new(),
route: @library.Route::root(),
}
let value = @eval.eval_string(
engine,
@library.Context::none(),
command.selector,
@syntax.Span::detached(),
Code,
@library.Scope::new(),
) catch {
@library.SourceError(errors) => {
let message = StringBuilder()
message.write_string("failed to evaluate selector")
for i, error in errors {
message.write_string(if i == 0 { ": " } else { ", " })
message.write_string(error.message)
}
raise bail(message.to_string())
}
}
let selector : @library.LocatableSelector = @library.FromValue::from_value(
value,
) catch {
@library.HintedError(err) => raise HintedFailure(err)
}
introspector.query(selector.0).to_owned()
}
///|
/// Format the query result in the output format.
fn format_query(
elements : Array[@library.Content],
command : QueryCommand,
) -> String raise HintedFailure {
if command.one && elements.length() != 1 {
raise bail("expected exactly one element, found \{elements.length()}")
}
let mapped : Array[@library.Value] = []
for c in elements {
match command.field {
Some(field) =>
try c.get_by_name(field) catch {
_ => ()
} noraise {
value => mapped.push(value)
}
None => mapped.push(Content(c))
}
}
if command.one {
guard mapped.length() > 0 else {
raise bail("no such field found for element")
}
serialize(mapped[0], command.format, command.pretty)
} else {
serialize(
Array(@library.Arr::from_array(mapped)),
command.format,
command.pretty,
)
}
}
///|
/// Serialize data to the output format and convert the error to a string
/// (upstream `crate::serialize`).
fn serialize(
data : @library.Value,
format : SerializationFormat,
pretty : Bool,
) -> String raise HintedFailure {
match format {
Json => @tjson.to_string(data.to_serde(true), pretty~)
Yaml =>
@yaml.to_string(data.to_serde(true)) catch {
@yaml.YamlError(message~, ..) => raise bail(message)
}
}
}
///|
/// Rust's `shell_escape::escape` (Unix).
fn shell_escape(s : String) -> String {
let allowed = (c : Char) => {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c is ('-' | '_' | '=' | '/' | ',' | '.' | '+')
}
if s != "" && s.all(allowed) {
return s
}
let es = StringBuilder()
es.write_char('\'')
for ch in s {
match ch {
'\'' | '!' => {
es.write_string("'\\")
es.write_char(ch)
es.write_char('\'')
}
_ => es.write_char(ch)
}
}
es.write_char('\'')
es.to_string()
}
///|
/// Format the deprecation warning with the specific invocation of `typst
/// eval` needed to replace `typst query`.
fn deprecation_warning(command : QueryCommand) -> @library.SourceDiagnostic {
let query = {
let buf = StringBuilder()
buf.write_string("query(\{command.selector})")
let access = (field : String) => {
if @syntax.is_ident(field) {
".\{field}"
} else {
".at(\{@library.repr_str(field)})"
}
}
match (command.one, command.field) {
(false, None) => ()
(false, Some(field)) => buf.write_string(".map(it => it\{access(field)})")
(true, None) => buf.write_string(".first()")
(true, Some(field)) => buf.write_string(".first()\{access(field)}")
}
shell_escape(buf.to_string())
}
let eval_command = match command.input {
Path(path) => "typst eval \{query} --in \{path}"
Stdin => "typst eval \{query}"
}
let diag = @library.SourceDiagnostic::warning(
@syntax.DiagSpan::detached(),
"the `typst query` subcommand is deprecated",
)
diag.hint("use `\{eval_command}` instead")
diag
}