diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-12-04 14:17:38 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-12-04 13:17:38 +0000 |
| commit | bf1c7db6fcee967ea802a19430c1e27444e16da1 (patch) | |
| tree | 17e9228d3844fa964e88ee88f6fa4b8a2ca9a3c1 /crates/typst-cli/src/compile.rs | |
| parent | 884c02872ce9d7432c3b67a920011cf385ce70b2 (diff) | |
Clean up CLI argument parsing (#5521)
Diffstat (limited to 'crates/typst-cli/src/compile.rs')
| -rw-r--r-- | crates/typst-cli/src/compile.rs | 242 |
1 files changed, 130 insertions, 112 deletions
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2a319424..7d650fc8 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<T> = Result<T, CodespanError>; 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))? +} + +/// 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<PageRanges>, + /// The document's creation date formatted as a UNIX timestamp. + pub creation_timestamp: Option<DateTime<Utc>>, + /// 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<Option<String>>, + /// 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<PathBuf>, + /// 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<Self> { + let input = args.input.clone(); - /// 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<OutputFormat> { - Ok(if let Some(specified) = self.format { + 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 - }) - } + }; - /// 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<PageRanges> { - self.pages.as_ref().map(|export_ranges| { - PageRanges::new( - export_ranges.iter().map(PageRangeArgument::to_range).collect(), - ) - }) - } + 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", + }, + )) + }); - /// The PDF standards to try to conform with. - pub fn pdf_standards(&self) -> StrResult<PdfStandards> { - 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::<Vec<_>>(); - PdfStandards::new(&list) - } -} + let pages = args.pages.as_ref().map(|export_ranges| { + PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) + }); -/// 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(()) + 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::<Vec<_>>(); + 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(), + }) + } } /// 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<SourceResult<()>> { - let format = command.output_format().unwrap(); - - match format { + match config.output_format { OutputFormat::Html => { let Warned { output, warnings } = typst::compile::<HtmlDocument>(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::<PagedDocument>(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::<Vec<_>>(); 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::<Result<Vec<()>, 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<Item = PathBuf>, ) -> 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) = |
