use comemo::Track; use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, StrResult}; use typst::eval::{eval_string, EvalMode, Tracer}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::model::Document; use typst::syntax::Span; use typst::World; use crate::args::{QueryCommand, SerializationFormat}; use crate::compile::print_diagnostics; use crate::set_failed; use crate::world::SystemWorld; /// Execute a query command. pub fn query(command: &QueryCommand) -> StrResult<()> { let mut world = SystemWorld::new(&command.common)?; // Reset everything and ensure that the main file is present. world.reset(); world.source(world.main()).map_err(|err| err.to_string())?; let mut tracer = Tracer::new(); let result = typst::compile(&world, &mut tracer); let warnings = tracer.warnings(); match result { // Retrieve and print query results. Ok(document) => { let data = retrieve(&world, command, &document)?; let serialized = format(data, command)?; println!("{serialized}"); print_diagnostics(&world, &[], &warnings, command.common.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; } // Print diagnostics. Err(errors) => { set_failed(); print_diagnostics( &world, &errors, &warnings, command.common.diagnostic_format, ) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; } } Ok(()) } /// Retrieve the matches for the selector. fn retrieve( world: &dyn World, command: &QueryCommand, document: &Document, ) -> StrResult> { let selector = eval_string( world.track(), &command.selector, Span::detached(), EvalMode::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::()?; Ok(document .introspector .query(&selector.0) .into_iter() .collect::>()) } /// Format the query result in the output format. fn format(elements: Vec, command: &QueryCommand) -> StrResult { 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), _ => Some(c.into_value()), }) .collect(); if command.one { let Some(value) = mapped.first() else { bail!("no such field found for element"); }; serialize(value, command.format) } else { serialize(&mapped, command.format) } } /// Serialize data to the output format. fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult { match format { SerializationFormat::Json => { serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}")) } SerializationFormat::Yaml => { serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}")) } } }