diff --git a/Cargo.lock b/Cargo.lock index dbcbe96e5..c1bb7be01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,7 +450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", @@ -531,6 +531,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -738,6 +744,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hayagriva" version = "0.3.0" @@ -987,8 +999,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "rayon", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", ] [[package]] @@ -1003,7 +1026,7 @@ dependencies = [ "crossbeam-utils", "dashmap", "env_logger", - "indexmap", + "indexmap 1.9.3", "is-terminal", "itoa", "log", @@ -1452,7 +1475,7 @@ dependencies = [ "crossbeam-channel", "filetime", "image", - "indexmap", + "indexmap 1.9.3", "itertools", "libdeflater", "log", @@ -1571,7 +1594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ "base64", - "indexmap", + "indexmap 1.9.3", "line-wrap", "quick-xml 0.28.2", "serde", @@ -2015,12 +2038,25 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", ] +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -2413,7 +2449,7 @@ version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" dependencies = [ - "indexmap", + "indexmap 1.9.3", "serde", "serde_spanned", "toml_datetime", @@ -2523,7 +2559,7 @@ dependencies = [ "fontdb", "if_chain", "image", - "indexmap", + "indexmap 1.9.3", "log", "miniz_oxide", "oklab", @@ -2573,6 +2609,9 @@ dependencies = [ "once_cell", "open", "same-file", + "serde", + "serde_json", + "serde_yaml 0.9.25", "siphasher", "tar", "tempfile", @@ -2596,7 +2635,7 @@ dependencies = [ "once_cell", "pulldown-cmark", "serde", - "serde_yaml", + "serde_yaml 0.8.26", "syntect", "typed-arena", "typst", @@ -2629,7 +2668,7 @@ dependencies = [ "roxmltree", "rustybuzz", "serde_json", - "serde_yaml", + "serde_yaml 0.8.26", "smallvec", "syntect", "time", @@ -2795,6 +2834,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059" +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "unscanny" version = "0.1.0" @@ -3243,7 +3288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a94fb32d2b438e3fddf901fbfe9eb87b34d63853ca6c6da5d2ab7e27031e0bae" dependencies = [ "serde", - "serde_yaml", + "serde_yaml 0.8.26", ] [[package]] diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index e77cbc2ae..808f07a7e 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -34,6 +34,9 @@ notify = "5" once_cell = "1" open = "4.0.2" same-file = "1" +serde = "1" +serde_json = "1" +serde_yaml = "0.9" siphasher = "0.3" tar = "0.4" tempfile = "3.5.0" diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 55cfe8ad3..0d88df9b8 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; -use clap::{ArgAction, Parser, Subcommand, ValueEnum}; +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; /// The Typst compiler. #[derive(Debug, Clone, Parser)] @@ -29,6 +29,9 @@ pub enum Command { #[command(visible_alias = "w")] Watch(CompileCommand), + /// Processes an input file to extract provided metadata + Query(QueryCommand), + /// Lists all discovered fonts in system and custom font paths Fonts(FontsCommand), } @@ -36,12 +39,71 @@ pub enum Command { /// Compiles the input file into a PDF file #[derive(Debug, Clone, Parser)] pub struct CompileCommand { - /// Path to input Typst file - pub input: PathBuf, + /// Shared arguments. + #[clap(flatten)] + pub common: SharedArgs, /// Path to output PDF file or PNG file(s) pub output: Option, + /// Opens the output file using the default viewer after compilation + #[arg(long = "open")] + pub open: Option>, + + /// The PPI (pixels per inch) to use for PNG export + #[arg(long = "ppi", default_value_t = 144.0)] + pub ppi: f32, + + /// Produces a flamegraph of the compilation process + #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] + pub flamegraph: Option>, +} + +impl CompileCommand { + /// The output path. + pub fn output(&self) -> PathBuf { + self.output + .clone() + .unwrap_or_else(|| self.common.input.with_extension("pdf")) + } +} + +/// Processes an input file to extract provided metadata +#[derive(Debug, Clone, Parser)] +pub struct QueryCommand { + /// Shared arguments. + #[clap(flatten)] + pub common: SharedArgs, + + /// Define what elements to retrieve + pub selector: String, + + /// Extract just one field from all retrieved elements + #[clap(long = "field")] + pub field: Option, + + /// Expect and retrieve exactly one element + #[clap(long = "one", default_value = "false")] + pub one: bool, + + /// The format to serialization in + #[clap(long = "format", default_value = "json")] + pub format: SerializationFormat, +} + +// Output file format for query command +#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] +pub enum SerializationFormat { + Json, + Yaml, +} + +/// Common arguments of compile, watch, and query. +#[derive(Debug, Clone, Args)] +pub struct SharedArgs { + /// Path to input Typst file + pub input: PathBuf, + /// Configures the project root #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")] pub root: Option, @@ -55,14 +117,6 @@ pub struct CompileCommand { )] pub font_paths: Vec, - /// Opens the output file using the default viewer after compilation - #[arg(long = "open")] - pub open: Option>, - - /// The PPI (pixels per inch) to use for PNG export - #[arg(long = "ppi", default_value_t = 144.0)] - pub ppi: f32, - /// In which format to emit diagnostics #[clap( long, @@ -70,19 +124,6 @@ pub struct CompileCommand { value_parser = clap::value_parser!(DiagnosticFormat) )] pub diagnostic_format: DiagnosticFormat, - - /// Produces a flamegraph of the compilation process - #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] - pub flamegraph: Option>, -} - -impl CompileCommand { - /// The output path. - pub fn output(&self) -> PathBuf { - self.output - .clone() - .unwrap_or_else(|| self.input.with_extension("pdf")) - } } /// Lists all discovered fonts in system and custom font paths diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2cce13e15..ca088a762 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -21,7 +21,7 @@ type CodespanError = codespan_reporting::files::Error; /// Execute a compilation command. pub fn compile(mut command: CompileCommand) -> StrResult<()> { - let mut world = SystemWorld::new(&command)?; + let mut world = SystemWorld::new(&command.common)?; compile_once(&mut world, &mut command, false)?; Ok(()) } @@ -42,14 +42,12 @@ pub fn compile_once( Status::Compiling.print(command).unwrap(); } - // Reset everything and ensure that the main file is still present. + // 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::default(); - let result = typst::compile(world, &mut tracer); - let warnings = tracer.warnings(); match result { @@ -67,7 +65,7 @@ pub fn compile_once( } } - print_diagnostics(world, &[], &warnings, command.diagnostic_format) + print_diagnostics(world, &[], &warnings, command.common.diagnostic_format) .map_err(|_| "failed to print diagnostics")?; if let Some(open) = command.open.take() { @@ -84,8 +82,13 @@ pub fn compile_once( Status::Error.print(command).unwrap(); } - print_diagnostics(world, &errors, &warnings, command.diagnostic_format) - .map_err(|_| "failed to print diagnostics")?; + print_diagnostics( + world, + &errors, + &warnings, + command.common.diagnostic_format, + ) + .map_err(|_| "failed to print diagnostics")?; } } @@ -152,7 +155,7 @@ fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { } /// Print diagnostic messages to the terminal. -fn print_diagnostics( +pub fn print_diagnostics( world: &SystemWorld, errors: &[SourceDiagnostic], warnings: &[SourceDiagnostic], diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index 425d05fd1..62f145669 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -2,6 +2,7 @@ mod args; mod compile; mod fonts; mod package; +mod query; mod tracing; mod watch; mod world; @@ -36,6 +37,7 @@ fn main() -> ExitCode { let res = match arguments.command { Command::Compile(command) => crate::compile::compile(command), Command::Watch(command) => crate::watch::watch(command), + Command::Query(command) => crate::query::query(command), Command::Fonts(command) => crate::fonts::fonts(command), }; diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs new file mode 100644 index 000000000..faf4e01b2 --- /dev/null +++ b/crates/typst-cli/src/query.rs @@ -0,0 +1,114 @@ +use comemo::Track; +use serde::Serialize; +use typst::diag::{bail, StrResult}; +use typst::eval::{eval_string, EvalMode, Tracer}; +use typst::model::Introspector; +use typst::World; +use typst_library::prelude::*; + +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)?; + tracing::info!("Starting querying"); + + // 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::default(); + 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(|_| "failed to print diagnostics")?; + } + + // Print diagnostics. + Err(errors) => { + set_failed(); + print_diagnostics( + &world, + &errors, + &warnings, + command.common.diagnostic_format, + ) + .map_err(|_| "failed to print diagnostics")?; + } + } + + 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(Introspector::new(&document.pages) + .query(&selector.0) + .into_iter() + .map(|x| x.into_inner()) + .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.field(field), + _ => Some(c.into_value()), + }) + .collect(); + + if command.one { + serialize(&mapped[0], 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}")) + } + } +} diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 759d27ec7..b320c6518 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -17,7 +17,7 @@ use crate::world::SystemWorld; /// Execute a watching compilation command. pub fn watch(mut command: CompileCommand) -> StrResult<()> { // Create the world that serves sources, files, and fonts. - let mut world = SystemWorld::new(&command)?; + let mut world = SystemWorld::new(&command.common)?; // Perform initial compilation. compile_once(&mut world, &mut command, true)?; @@ -159,7 +159,7 @@ impl Status { w.set_color(&color)?; write!(w, "watching")?; w.reset()?; - writeln!(w, " {}", command.input.display())?; + writeln!(w, " {}", command.common.input.display())?; w.set_color(&color)?; write!(w, "writing to")?; diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 2c0ee7d06..fb8fc0c73 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -15,7 +15,7 @@ use typst::syntax::{FileId, Source}; use typst::util::PathExt; use typst::World; -use crate::args::CompileCommand; +use crate::args::SharedArgs; use crate::fonts::{FontSearcher, FontSlot}; use crate::package::prepare_package; @@ -44,7 +44,7 @@ pub struct SystemWorld { impl SystemWorld { /// Create a new system world. - pub fn new(command: &CompileCommand) -> StrResult { + pub fn new(command: &SharedArgs) -> StrResult { let mut searcher = FontSearcher::new(); searcher.search(&command.font_paths); diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst-library/src/meta/metadata.rs new file mode 100644 index 000000000..eed4c71f6 --- /dev/null +++ b/crates/typst-library/src/meta/metadata.rs @@ -0,0 +1,87 @@ +use crate::prelude::*; + +/// Exposes a value to the query system without producing visible content. +/// +/// This element can be queried for with the [`query`]($func/query) function and +/// the command line `typst query` command. Its purpose is to expose an +/// arbitrary value to the introspection system. To identify a metadata value +/// among others, you can attach a [`label`]($type/label) to it and query for +/// that label. +/// +/// ```typ +/// #metadata("This is a note") +/// ``` +/// +/// ## Within Typst: `query` function { #within-typst } +/// Metadata can be retrieved from with the [`query`]($func/query) function +/// (like other elements): +/// +/// ```example +/// // Put metadata somewhere. +/// #metadata("This is a note") +/// +/// // And find it from anywhere else. +/// #locate(loc => { +/// query(, loc).first().value +/// }) +/// ``` +/// +/// ## Outside of Typst: `typst query` command { #outside-of-typst } +/// You can also retrieve the metadata from the command line with the +/// `typst query` command. This command executes an arbitrary query on the +/// document and returns the resulting elements in serialized form. +/// +/// The `metadata` element is especially useful for command line queries because +/// it allows you to expose arbitrary values to the outside world. However, +/// `typst query` also works with other elements `metadata` and complex +/// [selectors]($type/selector) like `{heading.where(level: 1)}`. +/// +/// ```sh +/// $ typst query example.typ "" +/// [ +/// { +/// "func": "metadata", +/// "value": "This is a note", +/// "label": "" +/// } +/// ] +/// ``` +/// +/// Frequently, you're interested in only one specific field of the resulting +/// elements. In the case of the `metadata` element, the `value` field is the +/// interesting one. You can extract just this field with the `--field` +/// argument. +/// +/// ```sh +/// $ typst query example.typ "" --field value +/// ["This is a note"] +/// ``` +/// +/// If you are interested in just a single element, you can use the `--one` +/// flag to extract just it. +/// +/// ```sh +/// $ typst query example.typ "" --field value --one +/// "This is a note" +/// ``` +/// +/// Display: Metadata +/// Category: meta +#[element(Behave, Show, Locatable)] +pub struct MetadataElem { + /// The value to embed into the document. + #[required] + pub value: Value, +} + +impl Show for MetadataElem { + fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult { + Ok(Content::empty()) + } +} + +impl Behave for MetadataElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Ignorant + } +} diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs index dcac63795..bb6ac3d32 100644 --- a/crates/typst-library/src/meta/mod.rs +++ b/crates/typst-library/src/meta/mod.rs @@ -8,6 +8,7 @@ mod figure; mod footnote; mod heading; mod link; +mod metadata; mod numbering; mod outline; mod query; @@ -22,6 +23,7 @@ pub use self::figure::*; pub use self::footnote::*; pub use self::heading::*; pub use self::link::*; +pub use self::metadata::*; pub use self::numbering::*; pub use self::outline::*; pub use self::query::*; @@ -50,6 +52,7 @@ pub(super) fn define(global: &mut Scope) { global.define("state", state_func()); global.define("query", query_func()); global.define("selector", selector_func()); + global.define("metadata", MetadataElem::func()); } /// The named with which an element is referenced. diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index d8f733bf9..f16d33119 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -26,7 +26,7 @@ flate2 = "1" fontdb = "0.13" if_chain = "1" image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } -indexmap = "1.9.3" +indexmap = { version = "1.9.3", features = ["serde"] } log = "0.4" miniz_oxide = "0.7" oklab = "1" diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs index adb3e858e..d93b14e62 100644 --- a/crates/typst/src/eval/array.rs +++ b/crates/typst/src/eval/array.rs @@ -3,9 +3,11 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, AddAssign}; use ecow::{eco_format, EcoString, EcoVec}; +use serde::Serialize; use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; use crate::diag::{At, SourceResult, StrResult}; +use crate::eval::ops::{add, mul}; use crate::syntax::Span; use crate::util::pretty_array_like; @@ -29,12 +31,11 @@ macro_rules! __array { #[doc(inline)] pub use crate::__array as array; -use crate::eval::ops::{add, mul}; #[doc(hidden)] pub use ecow::eco_vec; /// A reference counted array with value semantics. -#[derive(Default, Clone, PartialEq, Hash)] +#[derive(Default, Clone, PartialEq, Hash, Serialize)] pub struct Array(EcoVec); impl Array { diff --git a/crates/typst/src/eval/bytes.rs b/crates/typst/src/eval/bytes.rs index b24b289e3..5921f485d 100644 --- a/crates/typst/src/eval/bytes.rs +++ b/crates/typst/src/eval/bytes.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use comemo::Prehashed; use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; use crate::diag::StrResult; @@ -95,6 +96,19 @@ impl Debug for Bytes { } } +impl Serialize for Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&eco_format!("{self:?}")) + } else { + serializer.serialize_bytes(self) + } + } +} + /// The out of bounds access error message. #[cold] fn out_of_bounds(index: i64, len: usize) -> EcoString { diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/eval/dict.rs index 3b007c759..41d72d981 100644 --- a/crates/typst/src/eval/dict.rs +++ b/crates/typst/src/eval/dict.rs @@ -4,6 +4,7 @@ use std::ops::{Add, AddAssign}; use std::sync::Arc; use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; use super::{array, Array, Str, Value}; use crate::diag::StrResult; @@ -188,6 +189,15 @@ impl Hash for Dict { } } +impl Serialize for Dict { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } +} + impl Extend<(Str, Value)> for Dict { fn extend>(&mut self, iter: T) { Arc::make_mut(&mut self.0).extend(iter); diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs index 1d88b81b5..21dc3b68c 100644 --- a/crates/typst/src/eval/str.rs +++ b/crates/typst/src/eval/str.rs @@ -4,6 +4,7 @@ use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref, Range}; use ecow::EcoString; +use serde::Serialize; use unicode_segmentation::UnicodeSegmentation; use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; @@ -25,7 +26,7 @@ pub use crate::__format_str as format_str; pub use ecow::eco_format; /// An immutable reference counted string. -#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] pub struct Str(EcoString); impl Str { diff --git a/crates/typst/src/eval/symbol.rs b/crates/typst/src/eval/symbol.rs index 0925202ec..58cfd5346 100644 --- a/crates/typst/src/eval/symbol.rs +++ b/crates/typst/src/eval/symbol.rs @@ -4,6 +4,7 @@ use std::fmt::{self, Debug, Display, Formatter, Write}; use std::sync::Arc; use ecow::EcoString; +use serde::{Serialize, Serializer}; use crate::diag::{bail, StrResult}; @@ -135,6 +136,15 @@ impl Display for Symbol { } } +impl Serialize for Symbol { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_char(self.get()) + } +} + impl List { /// The characters that are covered by this list. fn variants(&self) -> Variants<'_> { diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs index 1894bac02..6a3fcd3c1 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/eval/value.rs @@ -5,6 +5,7 @@ use std::hash::{Hash, Hasher}; use std::sync::Arc; use ecow::eco_format; +use serde::{Serialize, Serializer}; use siphasher::sip128::{Hasher128, SipHasher13}; use super::{ @@ -250,6 +251,29 @@ impl Hash for Value { } } +impl Serialize for Value { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::None => serializer.serialize_none(), + Self::Bool(v) => serializer.serialize_bool(*v), + Self::Int(v) => serializer.serialize_i64(*v), + Self::Float(v) => serializer.serialize_f64(*v), + Self::Str(v) => v.serialize(serializer), + Self::Bytes(v) => v.serialize(serializer), + Self::Symbol(v) => v.serialize(serializer), + Self::Content(v) => v.serialize(serializer), + Self::Array(v) => v.serialize(serializer), + Self::Dict(v) => v.serialize(serializer), + + // Fall back to repr() for other things. + other => serializer.serialize_str(&other.repr()), + } + } +} + /// A dynamic value. #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/model/content.rs index 31106ea54..0c79d02ca 100644 --- a/crates/typst/src/model/content.rs +++ b/crates/typst/src/model/content.rs @@ -1,10 +1,11 @@ use std::any::TypeId; use std::fmt::{self, Debug, Formatter, Write}; -use std::iter::Sum; +use std::iter::{self, Sum}; use std::ops::{Add, AddAssign}; use comemo::Prehashed; use ecow::{eco_format, EcoString, EcoVec}; +use serde::{Serialize, Serializer}; use super::{ element, Behave, Behaviour, ElemFunc, Element, Guard, Label, Locatable, Location, @@ -516,6 +517,18 @@ impl Sum for Content { } } +impl Serialize for Content { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_map( + iter::once((&"func".into(), self.func().name().into_value())) + .chain(self.fields()), + ) + } +} + impl Attr { fn child(&self) -> Option<&Content> { match self {