diff options
| author | PgBiel <9021226+PgBiel@users.noreply.github.com> | 2024-05-10 11:47:02 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-05-10 14:47:02 +0000 |
| commit | 7905de67bcf3ca9b65c076ca02ec4726ba02d22c (patch) | |
| tree | de9952fe57152796b4524b70b913eb5e6aa67864 /crates/typst-cli | |
| parent | be12762d942e978ddf2e0ac5c34125264ab483b7 (diff) | |
Add parameter to select pages to be exported by CLI (#4039)
Diffstat (limited to 'crates/typst-cli')
| -rw-r--r-- | crates/typst-cli/src/args.rs | 64 | ||||
| -rw-r--r-- | crates/typst-cli/src/compile.rs | 44 |
2 files changed, 99 insertions, 9 deletions
diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index c115d5a9..f49d35c7 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -1,5 +1,8 @@ use std::fmt::{self, Display, Formatter}; +use std::num::NonZeroUsize; +use std::ops::RangeInclusive; use std::path::PathBuf; +use std::str::FromStr; use chrono::{DateTime, Utc}; use clap::builder::ValueParser; @@ -76,6 +79,18 @@ pub struct CompileCommand { #[clap(required_if_eq("input", "-"), value_parser = ValueParser::new(output_value_parser))] pub output: Option<Output>, + /// 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<Vec<PageRangeArgument>>, + /// Output a Makefile rule describing the current compilation #[clap(long = "make-deps", value_name = "PATH")] pub make_deps: Option<PathBuf>, @@ -271,6 +286,55 @@ 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<Option<NonZeroUsize>>); + +impl PageRangeArgument { + pub fn to_range(&self) -> RangeInclusive<Option<NonZeroUsize>> { + self.0.clone() + } +} + +impl FromStr for PageRangeArgument { + type Err = &'static str; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + match value.split('-').map(str::trim).collect::<Vec<_>>().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<NonZeroUsize, &'static str> { + 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 { diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index abe8768f..bf9afc35 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -7,17 +7,19 @@ use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; use ecow::{eco_format, EcoString}; use parking_lot::RwLock; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult}; use typst::eval::Tracer; use typst::foundations::{Datetime, Smart}; -use typst::layout::Frame; +use typst::layout::{Frame, PageRanges}; use typst::model::Document; use typst::syntax::{FileId, Source, Span}; use typst::visualize::Color; use typst::{World, WorldExt}; -use crate::args::{CompileCommand, DiagnosticFormat, Input, Output, OutputFormat}; +use crate::args::{ + CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument, +}; use crate::timings::Timer; use crate::watch::Status; use crate::world::SystemWorld; @@ -60,6 +62,17 @@ impl CompileCommand { 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(), + ) + }) + } } /// Execute a compilation command. @@ -171,7 +184,8 @@ fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> { let timestamp = convert_datetime( command.common.creation_timestamp.unwrap_or_else(chrono::Utc::now), ); - let buffer = typst_pdf::pdf(document, Smart::Auto, timestamp); + let exported_page_ranges = command.exported_page_ranges(); + let buffer = typst_pdf::pdf(document, Smart::Auto, timestamp, exported_page_ranges); command .output() .write(&buffer) @@ -214,7 +228,21 @@ fn export_image( output_template::has_indexable_template(output.to_str().unwrap_or_default()) } }; - if !can_handle_multiple && document.pages.len() > 1 { + + 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| { + exported_page_ranges.includes_page_index(*i) + }) + }) + .collect::<Vec<_>>(); + + if !can_handle_multiple && exported_pages.len() > 1 { let err = match output { Output::Stdout => "to stdout", Output::Path(_) => { @@ -227,10 +255,8 @@ fn export_image( let cache = world.export_cache(); // The results are collected in a `Vec<()>` which does not allocate. - document - .pages + exported_pages .par_iter() - .enumerate() .map(|(i, page)| { // Use output with converted path. let output = match output { @@ -250,7 +276,7 @@ fn export_image( // If we are not watching, don't use the cache. // If the frame is in the cache, skip it. // If the file does not exist, always create it. - if watching && cache.is_cached(i, &page.frame) && path.exists() { + if watching && cache.is_cached(*i, &page.frame) && path.exists() { return Ok(()); } |
