From bf1c7db6fcee967ea802a19430c1e27444e16da1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 4 Dec 2024 14:17:38 +0100 Subject: [PATCH] Clean up CLI argument parsing (#5521) --- crates/typst-cli/src/args.rs | 614 +++++++++++++++++--------------- crates/typst-cli/src/compile.rs | 244 +++++++------ crates/typst-cli/src/fonts.rs | 4 +- crates/typst-cli/src/init.rs | 2 +- crates/typst-cli/src/main.rs | 6 +- crates/typst-cli/src/package.rs | 4 +- crates/typst-cli/src/query.rs | 6 +- crates/typst-cli/src/timings.rs | 4 +- crates/typst-cli/src/watch.rs | 31 +- crates/typst-cli/src/world.rs | 26 +- 10 files changed, 501 insertions(+), 440 deletions(-) diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index d9ecc9217..7faaf6017 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -33,7 +33,7 @@ const AFTER_HELP: &str = color_print::cstr!("\ Forum for questions: https://forum.typst.app/ "); -/// The Typst compiler +/// The Typst compiler. #[derive(Debug, Clone, Parser)] #[clap( name = "typst", @@ -44,24 +44,16 @@ const AFTER_HELP: &str = color_print::cstr!("\ max_term_width = 80, )] pub struct CliArguments { - /// The command to run + /// The command to run. #[command(subcommand)] pub command: Command, - /// Set when to use color. - /// auto = use color if a capable terminal is detected - #[clap( - long, - value_name = "WHEN", - require_equals = true, - num_args = 0..=1, - default_value = "auto", - default_missing_value = "always", - )] + /// Whether to use color. When set to `auto` if the terminal to supports it. + #[clap(long, default_value_t = ColorChoice::Auto, default_missing_value = "always")] pub color: ColorChoice, /// Path to a custom CA certificate to use when making network requests. - #[clap(long = "cert", env = "TYPST_CERT")] + #[clap(long, env = "TYPST_CERT")] pub cert: Option, } @@ -69,109 +61,48 @@ pub struct CliArguments { #[derive(Debug, Clone, Subcommand)] #[command()] pub enum Command { - /// Compiles an input file into a supported output format + /// Compiles an input file into a supported output format. #[command(visible_alias = "c")] Compile(CompileCommand), - /// Watches an input file and recompiles on changes + /// Watches an input file and recompiles on changes. #[command(visible_alias = "w")] - Watch(CompileCommand), + Watch(WatchCommand), - /// Initializes a new project from a template + /// Initializes a new project from a template. Init(InitCommand), - /// Processes an input file to extract provided metadata + /// Processes an input file to extract provided metadata. Query(QueryCommand), - /// Lists all discovered fonts in system and custom font paths + /// Lists all discovered fonts in system and custom font paths. Fonts(FontsCommand), - /// Self update the Typst CLI + /// Self update the Typst CLI. #[cfg_attr(not(feature = "self-update"), clap(hide = true))] Update(UpdateCommand), } -/// Compiles an input file into a supported output format +/// Compiles an input file into a supported output format. #[derive(Debug, Clone, Parser)] pub struct CompileCommand { - /// Shared arguments + /// Arguments for compilation. #[clap(flatten)] - pub common: SharedArgs, - - /// Path to output file (PDF, PNG or SVG). Use `-` to write output to stdout. - /// - /// For output formats emitting one file per page (PNG & SVG), a page number template - /// must be present if the source document renders to multiple pages. Use `{p}` for page - /// numbers, `{0p}` for zero padded page numbers and `{t}` for page count. For example, - /// `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png` and so on. - #[clap( - required_if_eq("input", "-"), - value_parser = make_output_value_parser(), - value_hint = ValueHint::FilePath, - )] - pub output: Option, - - /// Which pages to export. When unspecified, all document pages are exported. - /// - /// Pages to export are separated by commas, and can be either simple page - /// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges - /// (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and - /// any pages after it). - /// - /// Page numbers are one-indexed and correspond to real page numbers in the - /// document (therefore not being affected by the document's page counter). - #[arg(long = "pages", value_delimiter = ',')] - pub pages: Option>, - - /// Output a Makefile rule describing the current compilation - #[clap(long = "make-deps", value_name = "PATH")] - pub make_deps: Option, - - /// The format of the output file, inferred from the extension by default - #[arg(long = "format", short = 'f')] - pub format: Option, - - /// Opens the output file with the default viewer or a specific program after - /// compilation - /// - /// Ignored if output is stdout. - #[arg(long = "open", value_name = "VIEWER")] - 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 performance timings of the compilation process (experimental) - /// - /// The resulting JSON file can be loaded into a tracing tool such as - /// https://ui.perfetto.dev. It does not contain any sensitive information - /// apart from file names and line numbers. - #[arg(long = "timings", value_name = "OUTPUT_JSON")] - pub timings: Option>, - - /// One (or multiple comma-separated) PDF standards that Typst will enforce - /// conformance with. - #[arg(long = "pdf-standard", value_delimiter = ',')] - pub pdf_standard: Vec, + pub args: CompileArgs, } -/// A PDF standard that Typst can enforce conformance with. -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -#[allow(non_camel_case_types)] -pub enum PdfStandard { - /// PDF 1.7. - #[value(name = "1.7")] - V_1_7, - /// PDF/A-2b. - #[value(name = "a-2b")] - A_2b, +/// Compiles an input file into a supported output format. +#[derive(Debug, Clone, Parser)] +pub struct WatchCommand { + /// Arguments for compilation. + #[clap(flatten)] + pub args: CompileArgs, } -/// Initializes a new project from a template +/// Initializes a new project from a template. #[derive(Debug, Clone, Parser)] pub struct InitCommand { - /// The template to use, e.g. `@preview/charged-ieee` + /// The template to use, e.g. `@preview/charged-ieee`. /// /// You can specify the version by appending e.g. `:0.1.0`. If no version is /// specified, Typst will default to the latest version. @@ -179,34 +110,34 @@ pub struct InitCommand { /// Supports both local and published templates. pub template: String, - /// The project directory, defaults to the template's name + /// The project directory, defaults to the template's name. pub dir: Option, - /// Arguments related to storage of packages in the system + /// Arguments related to storage of packages in the system. #[clap(flatten)] - pub package_storage_args: PackageStorageArgs, + pub package: PackageArgs, } -/// Processes an input file to extract provided metadata +/// Processes an input file to extract provided metadata. #[derive(Debug, Clone, Parser)] pub struct QueryCommand { - /// Shared arguments - #[clap(flatten)] - pub common: SharedArgs, + /// Path to input Typst file. Use `-` to read input from stdin. + #[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)] + pub input: Input, - /// Defines which elements to retrieve + /// Defines which elements to retrieve. pub selector: String, - /// Extracts just one field from all retrieved elements + /// Extracts just one field from all retrieved elements. #[clap(long = "field")] pub field: Option, - /// Expects and retrieves exactly one element + /// Expects and retrieves exactly one element. #[clap(long = "one", default_value = "false")] pub one: bool, - /// The format to serialize in - #[clap(long = "format", default_value = "json")] + /// The format to serialize in. + #[clap(long = "format", default_value_t)] pub format: SerializationFormat, /// Whether to pretty-print the serialized output. @@ -214,38 +145,153 @@ pub struct QueryCommand { /// Only applies to JSON format. #[clap(long)] pub pretty: bool, + + /// World arguments. + #[clap(flatten)] + pub world: WorldArgs, + + /// Processing arguments. + #[clap(flatten)] + pub process: ProcessArgs, } -// Output file format for query command -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -pub enum SerializationFormat { - Json, - Yaml, +/// Lists all discovered fonts in system and custom font paths. +#[derive(Debug, Clone, Parser)] +pub struct FontsCommand { + /// Common font arguments. + #[clap(flatten)] + pub font: FontArgs, + + /// Also lists style variants of each font family. + #[arg(long)] + pub variants: bool, } -/// Common arguments of compile, watch, and query. +/// Update the CLI using a pre-compiled binary from a Typst GitHub release. +#[derive(Debug, Clone, Parser)] +pub struct UpdateCommand { + /// Which version to update to (defaults to latest). + pub version: Option, + + /// Forces a downgrade to an older version (required for downgrading). + #[clap(long, default_value_t = false)] + pub force: bool, + + /// Reverts to the version from before the last update (only possible if + /// `typst update` has previously ran). + #[clap( + long, + default_value_t = false, + conflicts_with = "version", + conflicts_with = "force" + )] + pub revert: bool, + + /// Custom path to the backup file created on update and used by `--revert`, + /// defaults to system-dependent location + #[clap(long = "backup-path", env = "TYPST_UPDATE_BACKUP_PATH", value_name = "FILE")] + pub backup_path: Option, +} + +/// Arguments for compilation and watching. #[derive(Debug, Clone, Args)] -pub struct SharedArgs { - /// Path to input Typst file. Use `-` to read input from stdin - #[clap(value_parser = make_input_value_parser(), value_hint = ValueHint::FilePath)] +pub struct CompileArgs { + /// Path to input Typst file. Use `-` to read input from stdin. + #[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)] pub input: Input, - /// Configures the project root (for absolute paths) + /// Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to + /// stdout. + /// + /// For output formats emitting one file per page (PNG & SVG), a page number + /// template must be present if the source document renders to multiple + /// pages. Use `{p}` for page numbers, `{0p}` for zero padded page numbers + /// and `{t}` for page count. For example, `page-{0p}-of-{t}.png` creates + /// `page-01-of-10.png`, `page-02-of-10.png`, and so on. + #[clap( + required_if_eq("input", "-"), + value_parser = output_value_parser(), + value_hint = ValueHint::FilePath, + )] + pub output: Option, + + /// The format of the output file, inferred from the extension by default. + #[arg(long = "format", short = 'f')] + pub format: Option, + + /// World arguments. + #[clap(flatten)] + pub world: WorldArgs, + + /// Which pages to export. When unspecified, all pages are exported. + /// + /// Pages to export are separated by commas, and can be either simple page + /// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g. + /// '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any + /// pages after it). + /// + /// Page numbers are one-indexed and correspond to physical page numbers in + /// the document (therefore not being affected by the document's page + /// counter). + #[arg(long = "pages", value_delimiter = ',')] + pub pages: Option>, + + /// One (or multiple comma-separated) PDF standards that Typst will enforce + /// conformance with. + #[arg(long = "pdf-standard", value_delimiter = ',')] + pub pdf_standard: Vec, + + /// The PPI (pixels per inch) to use for PNG export. + #[arg(long = "ppi", default_value_t = 144.0)] + pub ppi: f32, + + /// File path to which a Makefile with the current compilation's + /// dependencies will be written. + #[clap(long = "make-deps", value_name = "PATH")] + pub make_deps: Option, + + /// Processing arguments. + #[clap(flatten)] + pub process: ProcessArgs, + + /// Opens the output file with the default viewer or a specific program + /// after compilation. Ignored if output is stdout. + #[arg(long = "open", value_name = "VIEWER")] + pub open: Option>, + + /// Produces performance timings of the compilation process. (experimental) + /// + /// The resulting JSON file can be loaded into a tracing tool such as + /// https://ui.perfetto.dev. It does not contain any sensitive information + /// apart from file names and line numbers. + #[arg(long = "timings", value_name = "OUTPUT_JSON")] + pub timings: Option>, +} + +/// Arguments for the construction of a world. Shared by compile, watch, and +/// query. +#[derive(Debug, Clone, Args)] +pub struct WorldArgs { + /// Configures the project root (for absolute paths). #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")] pub root: Option, - /// Add a string key-value pair visible through `sys.inputs` + /// Add a string key-value pair visible through `sys.inputs`. #[clap( long = "input", value_name = "key=value", action = ArgAction::Append, - value_parser = ValueParser::new(parse_input_pair), + value_parser = ValueParser::new(parse_sys_input_pair), )] pub inputs: Vec<(String, String)>, - /// Common font arguments + /// Common font arguments. #[clap(flatten)] - pub font_args: FontArgs, + pub font: FontArgs, + + /// Arguments related to storage of packages in the system. + #[clap(flatten)] + pub package: PackageArgs, /// The document's creation date formatted as a UNIX timestamp. /// @@ -257,21 +303,13 @@ pub struct SharedArgs { value_parser = parse_source_date_epoch, )] pub creation_timestamp: Option>, +} - /// The format to emit diagnostics in - #[clap( - long, - default_value_t = DiagnosticFormat::Human, - value_parser = clap::value_parser!(DiagnosticFormat) - )] - pub diagnostic_format: DiagnosticFormat, - - /// Arguments related to storage of packages in the system - #[clap(flatten)] - pub package_storage_args: PackageStorageArgs, - - /// Number of parallel jobs spawned during compilation, - /// defaults to number of CPUs. Setting it to 1 disables parallelism. +/// Arguments for configuration the process of compilation itself. +#[derive(Debug, Clone, Args)] +pub struct ProcessArgs { + /// Number of parallel jobs spawned during compilation. Defaults to number + /// of CPUs. Setting it to 1 disables parallelism. #[clap(long, short)] pub jobs: Option, @@ -279,22 +317,20 @@ pub struct SharedArgs { /// time. #[arg(long = "feature", value_delimiter = ',')] pub feature: Vec, -} -/// An in-development feature that may be changed or removed at any time. -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -pub enum Feature { - Html, + /// The format to emit diagnostics in. + #[clap(long, default_value_t)] + pub diagnostic_format: DiagnosticFormat, } /// Arguments related to where packages are stored in the system. #[derive(Debug, Clone, Args)] -pub struct PackageStorageArgs { - /// Custom path to local packages, defaults to system-dependent location +pub struct PackageArgs { + /// Custom path to local packages, defaults to system-dependent location. #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")] pub package_path: Option, - /// Custom path to package cache, defaults to system-dependent location + /// Custom path to package cache, defaults to system-dependent location. #[clap( long = "package-cache-path", env = "TYPST_PACKAGE_CACHE_PATH", @@ -303,13 +339,38 @@ pub struct PackageStorageArgs { pub package_cache_path: Option, } -/// Parses a UNIX timestamp according to -fn parse_source_date_epoch(raw: &str) -> Result, String> { - let timestamp: i64 = raw - .parse() - .map_err(|err| format!("timestamp must be decimal integer ({err})"))?; - DateTime::from_timestamp(timestamp, 0) - .ok_or_else(|| "timestamp out of range".to_string()) +/// Common arguments to customize available fonts +#[derive(Debug, Clone, Parser)] +pub struct FontArgs { + /// Adds additional directories that are recursively searched for fonts. + /// + /// If multiple paths are specified, they are separated by the system's path + /// separator (`:` on Unix-like systems and `;` on Windows). + #[clap( + long = "font-path", + env = "TYPST_FONT_PATHS", + value_name = "DIR", + value_delimiter = ENV_PATH_SEP, + )] + pub font_paths: Vec, + + /// Ensures system fonts won't be searched, unless explicitly included via + /// `--font-path`. + #[arg(long)] + pub ignore_system_fonts: bool, +} + +macro_rules! display_possible_values { + ($ty:ty) => { + impl Display for $ty { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } + } + }; } /// An input that is either stdin or a real path. @@ -321,6 +382,15 @@ pub enum Input { Path(PathBuf), } +impl Display for Input { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Input::Stdin => f.pad("stdin"), + Input::Path(path) => path.display().fmt(f), + } + } +} + /// An output that is either stdout or a real path. #[derive(Debug, Clone)] pub enum Output { @@ -339,8 +409,105 @@ impl Display for Output { } } +/// Which format to use for the generated output file. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] +pub enum OutputFormat { + Pdf, + Png, + Svg, + Html, +} + +display_possible_values!(OutputFormat); + +/// Which format to use for diagnostics. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] +pub enum DiagnosticFormat { + #[default] + Human, + Short, +} + +display_possible_values!(DiagnosticFormat); + +/// An in-development feature that may be changed or removed at any time. +#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] +pub enum Feature { + Html, +} + +display_possible_values!(Feature); + +/// A PDF standard that Typst can enforce conformance with. +#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] +#[allow(non_camel_case_types)] +pub enum PdfStandard { + /// PDF 1.7. + #[value(name = "1.7")] + V_1_7, + /// PDF/A-2b. + #[value(name = "a-2b")] + A_2b, +} + +display_possible_values!(PdfStandard); + +// Output file format for query command +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, ValueEnum)] +pub enum SerializationFormat { + #[default] + Json, + Yaml, +} + +display_possible_values!(SerializationFormat); + +/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the +/// `CompileCommand.pages` argument, through the `FromStr` trait instead of a +/// value parser, in order to generate better errors. +/// +/// See also: https://github.com/clap-rs/clap/issues/5065 +#[derive(Debug, Clone)] +pub struct Pages(pub RangeInclusive>); + +impl FromStr for Pages { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + match value.split('-').map(str::trim).collect::>().as_slice() { + [] | [""] => Err("page export range must not be empty"), + [single_page] => { + let page_number = parse_page_number(single_page)?; + Ok(Pages(Some(page_number)..=Some(page_number))) + } + ["", ""] => Err("page export range must have start or end"), + [start, ""] => Ok(Pages(Some(parse_page_number(start)?)..=None)), + ["", end] => Ok(Pages(None..=Some(parse_page_number(end)?))), + [start, end] => { + let start = parse_page_number(start)?; + let end = parse_page_number(end)?; + if start > end { + Err("page export range must end at a page after the start") + } else { + Ok(Pages(Some(start)..=Some(end))) + } + } + [_, _, _, ..] => Err("page export range must have a single hyphen"), + } + } +} + +/// Parses a single page number. +fn parse_page_number(value: &str) -> Result { + if value == "0" { + Err("page numbers start at one") + } else { + NonZeroUsize::from_str(value).map_err(|_| "not a valid page number") + } +} + /// The clap value parser used by `SharedArgs.input` -fn make_input_value_parser() -> impl TypedValueParser { +fn input_value_parser() -> impl TypedValueParser { clap::builder::OsStringValueParser::new().try_map(|value| { if value.is_empty() { Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) @@ -353,7 +520,7 @@ fn make_input_value_parser() -> impl TypedValueParser { } /// The clap value parser used by `CompileCommand.output` -fn make_output_value_parser() -> impl TypedValueParser { +fn output_value_parser() -> impl TypedValueParser { clap::builder::OsStringValueParser::new().try_map(|value| { // Empty value also handled by clap for `Option` if value.is_empty() { @@ -370,7 +537,7 @@ fn make_output_value_parser() -> impl TypedValueParser { /// /// This function will return an error if the argument contains no equals sign /// or contains the key (before the equals sign) is empty. -fn parse_input_pair(raw: &str) -> Result<(String, String), String> { +fn parse_sys_input_pair(raw: &str) -> Result<(String, String), String> { let (key, val) = raw .split_once('=') .ok_or("input must be a key and a value separated by an equal sign")?; @@ -382,144 +549,11 @@ fn parse_input_pair(raw: &str) -> Result<(String, String), String> { Ok((key, val)) } -/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the -/// `CompileCommand.pages` argument, through the `FromStr` trait instead of -/// a value parser, in order to generate better errors. -/// -/// See also: https://github.com/clap-rs/clap/issues/5065 -#[derive(Debug, Clone)] -pub struct PageRangeArgument(RangeInclusive>); - -impl PageRangeArgument { - pub fn to_range(&self) -> RangeInclusive> { - self.0.clone() - } -} - -impl FromStr for PageRangeArgument { - type Err = &'static str; - - fn from_str(value: &str) -> Result { - match value.split('-').map(str::trim).collect::>().as_slice() { - [] | [""] => Err("page export range must not be empty"), - [single_page] => { - let page_number = parse_page_number(single_page)?; - Ok(PageRangeArgument(Some(page_number)..=Some(page_number))) - } - ["", ""] => Err("page export range must have start or end"), - [start, ""] => Ok(PageRangeArgument(Some(parse_page_number(start)?)..=None)), - ["", end] => Ok(PageRangeArgument(None..=Some(parse_page_number(end)?))), - [start, end] => { - let start = parse_page_number(start)?; - let end = parse_page_number(end)?; - if start > end { - Err("page export range must end at a page after the start") - } else { - Ok(PageRangeArgument(Some(start)..=Some(end))) - } - } - [_, _, _, ..] => Err("page export range must have a single hyphen"), - } - } -} - -fn parse_page_number(value: &str) -> Result { - if value == "0" { - Err("page numbers start at one") - } else { - NonZeroUsize::from_str(value).map_err(|_| "not a valid page number") - } -} - -/// Lists all discovered fonts in system and custom font paths -#[derive(Debug, Clone, Parser)] -pub struct FontsCommand { - /// Common font arguments - #[clap(flatten)] - pub font_args: FontArgs, - - /// Also lists style variants of each font family - #[arg(long)] - pub variants: bool, -} - -/// Common arguments to customize available fonts -#[derive(Debug, Clone, Parser)] -pub struct FontArgs { - /// Adds additional directories that are recursively searched for fonts - /// - /// If multiple paths are specified, they are separated by the system's path - /// separator (`:` on Unix-like systems and `;` on Windows). - #[clap( - long = "font-path", - env = "TYPST_FONT_PATHS", - value_name = "DIR", - value_delimiter = ENV_PATH_SEP, - )] - pub font_paths: Vec, - - /// Ensures system fonts won't be searched, unless explicitly included via - /// `--font-path` - #[arg(long)] - pub ignore_system_fonts: bool, -} - -/// Which format to use for diagnostics. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] -pub enum DiagnosticFormat { - Human, - Short, -} - -impl Display for DiagnosticFormat { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } -} - -/// Update the CLI using a pre-compiled binary from a Typst GitHub release. -#[derive(Debug, Clone, Parser)] -pub struct UpdateCommand { - /// Which version to update to (defaults to latest) - pub version: Option, - - /// Forces a downgrade to an older version (required for downgrading) - #[clap(long, default_value_t = false)] - pub force: bool, - - /// Reverts to the version from before the last update (only possible if - /// `typst update` has previously ran) - #[clap( - long, - default_value_t = false, - conflicts_with = "version", - conflicts_with = "force" - )] - pub revert: bool, - - /// Custom path to the backup file created on update and used by `--revert`, - /// defaults to system-dependent location - #[clap(long = "backup-path", env = "TYPST_UPDATE_BACKUP_PATH", value_name = "FILE")] - pub backup_path: Option, -} - -/// Which format to use for the generated output file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] -pub enum OutputFormat { - Pdf, - Png, - Svg, - Html, -} - -impl Display for OutputFormat { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } +/// Parses a UNIX timestamp according to +fn parse_source_date_epoch(raw: &str) -> Result, String> { + let timestamp: i64 = raw + .parse() + .map_err(|err| format!("timestamp must be decimal integer ({err})"))?; + DateTime::from_timestamp(timestamp, 0) + .ok_or_else(|| "timestamp out of range".to_string()) } diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2a319424c..7d650fc80 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -2,7 +2,7 @@ use std::fs::{self, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use chrono::{Datelike, Timelike}; +use chrono::{DateTime, Datelike, Timelike, Utc}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; use ecow::{eco_format, EcoString}; @@ -19,10 +19,11 @@ use typst::WorldExt; use typst_pdf::{PdfOptions, PdfStandards}; use crate::args::{ - CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument, + CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PdfStandard, }; use crate::timings::Timer; + use crate::watch::Status; use crate::world::SystemWorld; use crate::{set_failed, terminal}; @@ -30,31 +31,49 @@ use crate::{set_failed, terminal}; type CodespanResult = Result; type CodespanError = codespan_reporting::files::Error; -impl CompileCommand { - /// The output path. - pub fn output(&self) -> Output { - self.output.clone().unwrap_or_else(|| { - let Input::Path(path) = &self.common.input else { - panic!("output must be specified when input is from stdin, as guarded by the CLI"); - }; - Output::Path(path.with_extension( - match self.output_format().unwrap_or(OutputFormat::Pdf) { - OutputFormat::Pdf => "pdf", - OutputFormat::Png => "png", - OutputFormat::Svg => "svg", - OutputFormat::Html => "html", - }, - )) - }) - } +/// Execute a compilation command. +pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> { + let mut config = CompileConfig::new(&command.args)?; + let mut world = + SystemWorld::new(&command.args.input, &command.args.world, &command.args.process) + .map_err(|err| eco_format!("{err}"))?; + timer.record(&mut world, |world| compile_once(world, &mut config, false))? +} - /// The format to use for generated output, either specified by the user or inferred from the extension. - /// - /// Will return `Err` if the format was not specified and could not be inferred. - pub fn output_format(&self) -> StrResult { - Ok(if let Some(specified) = self.format { +/// A preprocessed `CompileCommand`. +pub struct CompileConfig { + /// Path to input Typst file or stdin. + pub input: Input, + /// Path to output file (PDF, PNG, SVG, or HTML). + pub output: Output, + /// The format of the output file. + pub output_format: OutputFormat, + /// Which pages to export. + pub pages: Option, + /// The document's creation date formatted as a UNIX timestamp. + pub creation_timestamp: Option>, + /// The format to emit diagnostics in. + pub diagnostic_format: DiagnosticFormat, + /// Opens the output file with the default viewer or a specific program after + /// compilation. + pub open: Option>, + /// One (or multiple comma-separated) PDF standards that Typst will enforce + /// conformance with. + pub pdf_standards: PdfStandards, + /// A path to write a Makefile rule describing the current compilation. + pub make_deps: Option, + /// The PPI (pixels per inch) to use for PNG export. + pub ppi: f32, +} + +impl CompileConfig { + /// Preprocess a `CompileCommand`, producing a compilation config. + pub fn new(args: &CompileArgs) -> StrResult { + let input = args.input.clone(); + + let output_format = if let Some(specified) = args.format { specified - } else if let Some(Output::Path(output)) = &self.output { + } else if let Some(Output::Path(output)) = &args.output { match output.extension() { Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf, Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png, @@ -68,40 +87,51 @@ impl CompileCommand { } } else { OutputFormat::Pdf + }; + + let output = args.output.clone().unwrap_or_else(|| { + let Input::Path(path) = &input else { + panic!("output must be specified when input is from stdin, as guarded by the CLI"); + }; + Output::Path(path.with_extension( + match output_format { + OutputFormat::Pdf => "pdf", + OutputFormat::Png => "png", + OutputFormat::Svg => "svg", + OutputFormat::Html => "html", + }, + )) + }); + + let pages = args.pages.as_ref().map(|export_ranges| { + PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) + }); + + let pdf_standards = { + let list = args + .pdf_standard + .iter() + .map(|standard| match standard { + PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, + PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + }) + .collect::>(); + PdfStandards::new(&list)? + }; + + Ok(Self { + input, + output, + output_format, + pages, + pdf_standards, + creation_timestamp: args.world.creation_timestamp, + make_deps: args.make_deps.clone(), + ppi: args.ppi, + diagnostic_format: args.process.diagnostic_format, + open: args.open.clone(), }) } - - /// The ranges of the pages to be exported as specified by the user. - /// - /// This returns `None` if all pages should be exported. - pub fn exported_page_ranges(&self) -> Option { - self.pages.as_ref().map(|export_ranges| { - PageRanges::new( - export_ranges.iter().map(PageRangeArgument::to_range).collect(), - ) - }) - } - - /// The PDF standards to try to conform with. - pub fn pdf_standards(&self) -> StrResult { - let list = self - .pdf_standard - .iter() - .map(|standard| match standard { - PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, - PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, - }) - .collect::>(); - PdfStandards::new(&list) - } -} - -/// Execute a compilation command. -pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { - let mut world = - SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?; - timer.record(&mut world, |world| compile_once(world, &mut command, false))??; - Ok(()) } /// Compile a single time. @@ -110,17 +140,15 @@ pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { #[typst_macros::time(name = "compile once")] pub fn compile_once( world: &mut SystemWorld, - command: &mut CompileCommand, + config: &mut CompileConfig, watching: bool, ) -> StrResult<()> { - _ = command.output_format()?; - let start = std::time::Instant::now(); if watching { - Status::Compiling.print(command).unwrap(); + Status::Compiling.print(config).unwrap(); } - let Warned { output, warnings } = compile_and_export(world, command, watching); + let Warned { output, warnings } = compile_and_export(world, config, watching); match output { // Export the PDF / PNG. @@ -129,20 +157,20 @@ pub fn compile_once( if watching { if warnings.is_empty() { - Status::Success(duration).print(command).unwrap(); + Status::Success(duration).print(config).unwrap(); } else { - Status::PartialSuccess(duration).print(command).unwrap(); + Status::PartialSuccess(duration).print(config).unwrap(); } } - print_diagnostics(world, &[], &warnings, command.common.diagnostic_format) + print_diagnostics(world, &[], &warnings, config.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; - write_make_deps(world, command)?; + write_make_deps(world, config)?; - if let Some(open) = command.open.take() { - if let Output::Path(file) = command.output() { - open_file(open.as_deref(), &file)?; + if let Some(open) = config.open.take() { + if let Output::Path(file) = &config.output { + open_file(open.as_deref(), file)?; } } } @@ -152,16 +180,11 @@ pub fn compile_once( set_failed(); if watching { - Status::Error.print(command).unwrap(); + Status::Error.print(config).unwrap(); } - print_diagnostics( - world, - &errors, - &warnings, - command.common.diagnostic_format, - ) - .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; + print_diagnostics(world, &errors, &warnings, config.diagnostic_format) + .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; } } @@ -170,17 +193,15 @@ pub fn compile_once( fn compile_and_export( world: &mut SystemWorld, - command: &mut CompileCommand, + config: &mut CompileConfig, watching: bool, ) -> Warned> { - let format = command.output_format().unwrap(); - - match format { + match config.output_format { OutputFormat::Html => { let Warned { output, warnings } = typst::compile::(world); let result = output.and_then(|document| { - command - .output() + config + .output .write(typst_html::html(&document)?.as_bytes()) .map_err(|err| eco_format!("failed to write HTML file ({err})")) .at(Span::detached()) @@ -190,7 +211,7 @@ fn compile_and_export( _ => { let Warned { output, warnings } = typst::compile::(world); let result = output - .and_then(|document| export_paged(world, &document, command, watching)); + .and_then(|document| export_paged(world, &document, config, watching)); Warned { output: result, warnings } } } @@ -200,17 +221,17 @@ fn compile_and_export( fn export_paged( world: &mut SystemWorld, document: &PagedDocument, - command: &CompileCommand, + config: &CompileConfig, watching: bool, ) -> SourceResult<()> { - match command.output_format().at(Span::detached())? { - OutputFormat::Pdf => export_pdf(document, command), + match config.output_format { + OutputFormat::Pdf => export_pdf(document, config), OutputFormat::Png => { - export_image(world, document, command, watching, ImageExportFormat::Png) + export_image(world, document, config, watching, ImageExportFormat::Png) .at(Span::detached()) } OutputFormat::Svg => { - export_image(world, document, command, watching, ImageExportFormat::Svg) + export_image(world, document, config, watching, ImageExportFormat::Svg) .at(Span::detached()) } OutputFormat::Html => unreachable!(), @@ -218,18 +239,18 @@ fn export_paged( } /// Export to a PDF. -fn export_pdf(document: &PagedDocument, command: &CompileCommand) -> SourceResult<()> { +fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { let options = PdfOptions { ident: Smart::Auto, timestamp: convert_datetime( - command.common.creation_timestamp.unwrap_or_else(chrono::Utc::now), + config.creation_timestamp.unwrap_or_else(chrono::Utc::now), ), - page_ranges: command.exported_page_ranges(), - standards: command.pdf_standards().at(Span::detached())?, + page_ranges: config.pages.clone(), + standards: config.pdf_standards.clone(), }; let buffer = typst_pdf::pdf(document, &options)?; - command - .output() + config + .output .write(&buffer) .map_err(|err| eco_format!("failed to write PDF file ({err})")) .at(Span::detached())?; @@ -259,34 +280,31 @@ enum ImageExportFormat { fn export_image( world: &mut SystemWorld, document: &PagedDocument, - command: &CompileCommand, + config: &CompileConfig, watching: bool, fmt: ImageExportFormat, ) -> StrResult<()> { - let output = command.output(); // Determine whether we have indexable templates in output - let can_handle_multiple = match output { + let can_handle_multiple = match config.output { Output::Stdout => false, Output::Path(ref output) => { output_template::has_indexable_template(output.to_str().unwrap_or_default()) } }; - let exported_page_ranges = command.exported_page_ranges(); - let exported_pages = document .pages .iter() .enumerate() .filter(|(i, _)| { - exported_page_ranges.as_ref().map_or(true, |exported_page_ranges| { + config.pages.as_ref().map_or(true, |exported_page_ranges| { exported_page_ranges.includes_page_index(*i) }) }) .collect::>(); if !can_handle_multiple && exported_pages.len() > 1 { - let err = match output { + let err = match config.output { Output::Stdout => "to stdout", Output::Path(_) => { "without a page number template ({p}, {0p}) in the output path" @@ -302,8 +320,8 @@ fn export_image( .par_iter() .map(|(i, page)| { // Use output with converted path. - let output = match output { - Output::Path(ref path) => { + let output = match &config.output { + Output::Path(path) => { let storage; let path = if can_handle_multiple { storage = output_template::format( @@ -328,7 +346,7 @@ fn export_image( Output::Stdout => Output::Stdout, }; - export_image_page(command, page, &output, fmt)?; + export_image_page(config, page, &output, fmt)?; Ok(()) }) .collect::, EcoString>>()?; @@ -367,14 +385,14 @@ mod output_template { /// Export single image. fn export_image_page( - command: &CompileCommand, + config: &CompileConfig, page: &Page, output: &Output, fmt: ImageExportFormat, ) -> StrResult<()> { match fmt { ImageExportFormat::Png => { - let pixmap = typst_render::render(page, command.ppi / 72.0); + let pixmap = typst_render::render(page, config.ppi / 72.0); let buf = pixmap .encode_png() .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; @@ -438,12 +456,12 @@ impl ExportCache { /// Writes a Makefile rule describing the relationship between the output and /// its dependencies to the path specified by the --make-deps argument, if it /// was provided. -fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResult<()> { - let Some(ref make_deps_path) = command.make_deps else { return Ok(()) }; - let Output::Path(output_path) = command.output() else { +fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { + let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; + let Output::Path(output_path) = &config.output else { bail!("failed to create make dependencies file because output was stdout") }; - let Ok(output_path) = output_path.into_os_string().into_string() else { + let Some(output_path) = output_path.as_os_str().to_str() else { bail!("failed to create make dependencies file because output path was not valid unicode") }; @@ -481,13 +499,13 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu fn write( make_deps_path: &Path, - output_path: String, + output_path: &str, root: PathBuf, dependencies: impl Iterator, ) -> io::Result<()> { let mut file = File::create(make_deps_path)?; - file.write_all(munge(&output_path).as_bytes())?; + file.write_all(munge(output_path).as_bytes())?; file.write_all(b":")?; for dependency in dependencies { let Some(dependency) = diff --git a/crates/typst-cli/src/fonts.rs b/crates/typst-cli/src/fonts.rs index 01b0d9f75..dbe28d6f5 100644 --- a/crates/typst-cli/src/fonts.rs +++ b/crates/typst-cli/src/fonts.rs @@ -6,8 +6,8 @@ use crate::args::FontsCommand; /// Execute a font listing command. pub fn fonts(command: &FontsCommand) { let fonts = Fonts::searcher() - .include_system_fonts(!command.font_args.ignore_system_fonts) - .search_with(&command.font_args.font_paths); + .include_system_fonts(!command.font.ignore_system_fonts) + .search_with(&command.font.font_paths); for (name, infos) in fonts.book.families() { println!("{name}"); diff --git a/crates/typst-cli/src/init.rs b/crates/typst-cli/src/init.rs index 842419fc1..9a77fb470 100644 --- a/crates/typst-cli/src/init.rs +++ b/crates/typst-cli/src/init.rs @@ -15,7 +15,7 @@ use crate::package; /// Execute an initialization command. pub fn init(command: &InitCommand) -> StrResult<()> { - let package_storage = package::storage(&command.package_storage_args); + let package_storage = package::storage(&command.package); // Parse the package specification. If the user didn't specify the version, // we try to figure it out automatically by downloading the package index diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index e4861d6f9..8cef14157 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -60,11 +60,11 @@ fn main() -> ExitCode { /// Execute the requested command. fn dispatch() -> HintedStrResult<()> { - let timer = Timer::new(&ARGS); + let mut timer = Timer::new(&ARGS); match &ARGS.command { - Command::Compile(command) => crate::compile::compile(timer, command.clone())?, - Command::Watch(command) => crate::watch::watch(timer, command.clone())?, + Command::Compile(command) => crate::compile::compile(&mut timer, command)?, + Command::Watch(command) => crate::watch::watch(&mut timer, command)?, Command::Init(command) => crate::init::init(command)?, Command::Query(command) => crate::query::query(command)?, Command::Fonts(command) => crate::fonts::fonts(command), diff --git a/crates/typst-cli/src/package.rs b/crates/typst-cli/src/package.rs index b4965f89d..6099ecaa9 100644 --- a/crates/typst-cli/src/package.rs +++ b/crates/typst-cli/src/package.rs @@ -1,10 +1,10 @@ use typst_kit::package::PackageStorage; -use crate::args::PackageStorageArgs; +use crate::args::PackageArgs; use crate::download; /// Returns a new package storage for the given args. -pub fn storage(args: &PackageStorageArgs) -> PackageStorage { +pub fn storage(args: &PackageArgs) -> PackageStorage { PackageStorage::new( args.package_cache_path.clone(), args.package_path.clone(), diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index 947a64850..610f23cd4 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -15,7 +15,7 @@ use crate::world::SystemWorld; /// Execute a query command. pub fn query(command: &QueryCommand) -> HintedStrResult<()> { - let mut world = SystemWorld::new(&command.common)?; + let mut world = SystemWorld::new(&command.input, &command.world, &command.process)?; // Reset everything and ensure that the main file is present. world.reset(); @@ -29,7 +29,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> { let data = retrieve(&world, command, &document)?; let serialized = format(data, command)?; println!("{serialized}"); - print_diagnostics(&world, &[], &warnings, command.common.diagnostic_format) + print_diagnostics(&world, &[], &warnings, command.process.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; } @@ -40,7 +40,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> { &world, &errors, &warnings, - command.common.diagnostic_format, + command.process.diagnostic_format, ) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; } diff --git a/crates/typst-cli/src/timings.rs b/crates/typst-cli/src/timings.rs index 4446bbf98..9f017dc12 100644 --- a/crates/typst-cli/src/timings.rs +++ b/crates/typst-cli/src/timings.rs @@ -22,8 +22,8 @@ impl Timer { /// record timings for a specific function invocation. pub fn new(args: &CliArguments) -> Timer { let record = match &args.command { - Command::Compile(command) => command.timings.clone(), - Command::Watch(command) => command.timings.clone(), + Command::Compile(command) => command.args.timings.clone(), + Command::Watch(command) => command.args.timings.clone(), _ => None, }; diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index f3ca329b8..f5569b466 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -13,32 +13,38 @@ use same_file::is_same_file; use typst::diag::{bail, StrResult}; use typst::utils::format_duration; -use crate::args::{CompileCommand, Input, Output}; -use crate::compile::compile_once; +use crate::args::{Input, Output, WatchCommand}; +use crate::compile::{compile_once, CompileConfig}; use crate::timings::Timer; use crate::world::{SystemWorld, WorldCreationError}; use crate::{print_error, terminal}; /// Execute a watching compilation command. -pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { - let Output::Path(output) = command.output() else { +pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { + let mut config = CompileConfig::new(&command.args)?; + + let Output::Path(output) = &config.output else { bail!("cannot write document to stdout in watch mode"); }; // Create a file system watcher. - let mut watcher = Watcher::new(output)?; + let mut watcher = Watcher::new(output.clone())?; // Create the world that serves sources, files, and fonts. // Additionally, if any files do not exist, wait until they do. let mut world = loop { - match SystemWorld::new(&command.common) { + match SystemWorld::new( + &command.args.input, + &command.args.world, + &command.args.process, + ) { Ok(world) => break world, Err( ref err @ (WorldCreationError::InputNotFound(ref path) | WorldCreationError::RootNotFound(ref path)), ) => { watcher.update([path.clone()])?; - Status::Error.print(&command).unwrap(); + Status::Error.print(&config).unwrap(); print_error(&err.to_string()).unwrap(); watcher.wait()?; } @@ -47,7 +53,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { }; // Perform initial compilation. - timer.record(&mut world, |world| compile_once(world, &mut command, true))??; + timer.record(&mut world, |world| compile_once(world, &mut config, true))??; // Watch all dependencies of the initial compilation. watcher.update(world.dependencies())?; @@ -61,7 +67,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { world.reset(); // Recompile. - timer.record(&mut world, |world| compile_once(world, &mut command, true))??; + timer.record(&mut world, |world| compile_once(world, &mut config, true))??; // Evict the cache. comemo::evict(10); @@ -267,8 +273,7 @@ pub enum Status { impl Status { /// Clear the terminal and render the status message. - pub fn print(&self, command: &CompileCommand) -> io::Result<()> { - let output = command.output(); + pub fn print(&self, config: &CompileConfig) -> io::Result<()> { let timestamp = chrono::offset::Local::now().format("%H:%M:%S"); let color = self.color(); @@ -278,7 +283,7 @@ impl Status { out.set_color(&color)?; write!(out, "watching")?; out.reset()?; - match &command.common.input { + match &config.input { Input::Stdin => writeln!(out, " "), Input::Path(path) => writeln!(out, " {}", path.display()), }?; @@ -286,7 +291,7 @@ impl Status { out.set_color(&color)?; write!(out, "writing to")?; out.reset()?; - writeln!(out, " {output}")?; + writeln!(out, " {}", config.output)?; writeln!(out)?; writeln!(out, "[{timestamp}] {}", self.message())?; diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 6e6de0dcb..e5e31a902 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -17,7 +17,7 @@ use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; use typst_timing::timed; -use crate::args::{Feature, Input, SharedArgs}; +use crate::args::{Feature, Input, ProcessArgs, WorldArgs}; use crate::compile::ExportCache; use crate::download::PrintDownload; use crate::package; @@ -56,9 +56,13 @@ pub struct SystemWorld { impl SystemWorld { /// Create a new system world. - pub fn new(command: &SharedArgs) -> Result { + pub fn new( + input: &Input, + world_args: &WorldArgs, + process_args: &ProcessArgs, + ) -> Result { // Set up the thread pool. - if let Some(jobs) = command.jobs { + if let Some(jobs) = process_args.jobs { rayon::ThreadPoolBuilder::new() .num_threads(jobs) .use_current_thread() @@ -67,7 +71,7 @@ impl SystemWorld { } // Resolve the system-global input path. - let input = match &command.input { + let input = match input { Input::Stdin => None, Input::Path(path) => { Some(path.canonicalize().map_err(|err| match err.kind() { @@ -81,7 +85,7 @@ impl SystemWorld { // Resolve the system-global root directory. let root = { - let path = command + let path = world_args .root .as_deref() .or_else(|| input.as_deref().and_then(|i| i.parent())) @@ -106,13 +110,13 @@ impl SystemWorld { let library = { // Convert the input pairs to a dictionary. - let inputs: Dict = command + let inputs: Dict = world_args .inputs .iter() .map(|(k, v)| (k.as_str().into(), v.as_str().into_value())) .collect(); - let features = command + let features = process_args .feature .iter() .map(|&feature| match feature { @@ -124,10 +128,10 @@ impl SystemWorld { }; let fonts = Fonts::searcher() - .include_system_fonts(!command.font_args.ignore_system_fonts) - .search_with(&command.font_args.font_paths); + .include_system_fonts(!world_args.font.ignore_system_fonts) + .search_with(&world_args.font.font_paths); - let now = match command.creation_timestamp { + let now = match world_args.creation_timestamp { Some(time) => Now::Fixed(time), None => Now::System(OnceLock::new()), }; @@ -140,7 +144,7 @@ impl SystemWorld { book: LazyHash::new(fonts.book), fonts: fonts.fonts, slots: Mutex::new(HashMap::new()), - package_storage: package::storage(&command.package_storage_args), + package_storage: package::storage(&world_args.package), now, export_cache: ExportCache::new(), })