summaryrefslogtreecommitdiff
path: root/crates/typst-cli/src/compile.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-12-04 14:17:38 +0100
committerGitHub <noreply@github.com>2024-12-04 13:17:38 +0000
commitbf1c7db6fcee967ea802a19430c1e27444e16da1 (patch)
tree17e9228d3844fa964e88ee88f6fa4b8a2ca9a3c1 /crates/typst-cli/src/compile.rs
parent884c02872ce9d7432c3b67a920011cf385ce70b2 (diff)
Clean up CLI argument parsing (#5521)
Diffstat (limited to 'crates/typst-cli/src/compile.rs')
-rw-r--r--crates/typst-cli/src/compile.rs242
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) =