summaryrefslogtreecommitdiff
path: root/crates/typst-cli
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-cli')
-rw-r--r--crates/typst-cli/src/args.rs64
-rw-r--r--crates/typst-cli/src/compile.rs44
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(());
}