diff --git a/Cargo.lock b/Cargo.lock index 50431aa2..03ddcd8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -439,8 +439,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -3740,6 +3742,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "cargo_metadata", + "chrono", "clap", "clap_builder", "clap_complete", @@ -3807,6 +3810,7 @@ version = "0.11.15" dependencies = [ "anyhow", "biblatex", + "chrono", "comemo 0.4.0", "dashmap", "ecow 0.2.2", diff --git a/Cargo.toml b/Cargo.toml index d1e6331b..bea7d01b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ tokio-util = { version = "0.7.10", features = ["compat"] } open = { version = "5.1.3" } parking_lot = "0.12.1" walkdir = "2" +chrono = "0.4" # Networking hyper = { version = "0.14", features = ["full"] } diff --git a/crates/tinymist-query/Cargo.toml b/crates/tinymist-query/Cargo.toml index b4801ce5..4d3332f3 100644 --- a/crates/tinymist-query/Cargo.toml +++ b/crates/tinymist-query/Cargo.toml @@ -31,6 +31,7 @@ walkdir.workspace = true indexmap.workspace = true ecow.workspace = true siphasher.workspace = true +chrono.workspace = true typst.workspace = true diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index cb8aa38d..412f858e 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -143,10 +143,11 @@ mod polymorphic { Merged, } - #[derive(Debug, Clone, Default)] + #[derive(Debug, Clone)] pub enum ExportKind { - #[default] - Pdf, + Pdf { + creation_timestamp: Option>, + }, Svg { page: PageSelection, }, @@ -155,10 +156,18 @@ mod polymorphic { }, } + impl Default for ExportKind { + fn default() -> Self { + Self::Pdf { + creation_timestamp: None, + } + } + } + impl ExportKind { pub fn extension(&self) -> &str { match self { - Self::Pdf => "pdf", + Self::Pdf { .. } => "pdf", Self::Svg { .. } => "svg", Self::Png { .. } => "png", } diff --git a/crates/tinymist/Cargo.toml b/crates/tinymist/Cargo.toml index bb11e025..59e3dfbe 100644 --- a/crates/tinymist/Cargo.toml +++ b/crates/tinymist/Cargo.toml @@ -16,6 +16,7 @@ tinymist-assets = { workspace = true } tinymist-query.workspace = true tinymist-render.workspace = true sync-lsp.workspace = true +chrono.workspace = true once_cell.workspace = true anyhow.workspace = true diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index e3bcf961..2c034f3e 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -59,7 +59,9 @@ impl LanguageState { output: self.compile_config().output_path.clone(), mode: self.compile_config().export_pdf, }, - kind: ExportKind::Pdf, + kind: ExportKind::Pdf { + creation_timestamp: self.config.compile.determine_creation_timestamp(), + }, count_words: self.config.compile.notify_status, }); diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index e6e5893f..1856744c 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -27,7 +27,13 @@ struct ExportOpts { impl LanguageState { /// Export the current document as PDF file(s). pub fn export_pdf(&mut self, req_id: RequestId, args: Vec) -> ScheduledResult { - self.export(req_id, ExportKind::Pdf, args) + self.export( + req_id, + ExportKind::Pdf { + creation_timestamp: self.config.compile.determine_creation_timestamp(), + }, + args, + ) } /// Export the current document as Svg file(s). diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index eaa67e16..c093ef96 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -500,7 +500,8 @@ impl CompileConfig { entry: command.input.map(|e| Path::new(&e).into()), root_dir: command.root, inputs: Arc::new(Prehashed::new(inputs)), - font_paths: command.font.font_paths, + font: command.font, + creation_timestamp: command.creation_timestamp, }); } } @@ -621,13 +622,17 @@ impl CompileConfig { let font = || { let mut opts = self.font_opts.clone(); - if let Some(system_fonts) = self.system_fonts { + if let Some(system_fonts) = self.system_fonts.or_else(|| { + self.typst_extra_args + .as_ref() + .map(|x| x.font.ignore_system_fonts) + }) { opts.ignore_system_fonts = !system_fonts; } let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths); let font_paths = - font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font_paths)); + font_paths.or_else(|| self.typst_extra_args.as_ref().map(|x| &x.font.font_paths)); if let Some(paths) = font_paths { opts.font_paths.clone_from(paths); } @@ -669,6 +674,11 @@ impl CompileConfig { combine(user_inputs, self.lsp_inputs.clone()) } + /// Determines the creation timestamp. + pub fn determine_creation_timestamp(&self) -> Option> { + self.typst_extra_args.as_ref()?.creation_timestamp + } + fn determine_user_inputs(&self) -> ImmutDict { static EMPTY: Lazy = Lazy::new(ImmutDict::default); @@ -686,13 +696,13 @@ impl CompileConfig { ) -> ( Option, &Vec, - Option<&Vec>, + Option<&CompileFontArgs>, Option>, ) { ( self.system_fonts, &self.font_paths, - self.typst_extra_args.as_ref().map(|e| &e.font_paths), + self.typst_extra_args.as_ref().map(|e| &e.font), self.determine_root(self.determine_default_entry_path().as_ref()), ) } @@ -766,8 +776,10 @@ pub struct CompileExtraOpts { pub entry: Option, /// Additional input arguments to compile the entry file. pub inputs: ImmutDict, - /// will remove later - pub font_paths: Vec, + /// Additional font paths. + pub font: CompileFontArgs, + /// The creation timestamp for various output. + pub creation_timestamp: Option>, } /// The path pattern that could be substituted. diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index 3838b6f8..1f014efe 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -8,6 +8,7 @@ use tinymist_query::{ExportKind, PageSelection}; use tokio::sync::mpsc; use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color}; use typst_ts_compiler::{EntryReader, EntryState, TaskInputs}; +use typst_ts_core::TypstDatetime; use crate::{ actor::{ @@ -196,10 +197,12 @@ impl ExportConfig { static BLANK: Lazy = Lazy::new(Frame::default); let first_frame = || doc.pages.first().map(|f| &f.frame).unwrap_or(&*BLANK); Ok(match kind2 { - Pdf => { + Pdf { creation_timestamp } => { + let timestamp = + convert_datetime(creation_timestamp.unwrap_or_else(chrono::Utc::now)); // todo: Some(pdf_uri.as_str()) // todo: timestamp world.now() - typst_pdf::pdf(doc, Smart::Auto, None) + typst_pdf::pdf(doc, Smart::Auto, timestamp) } Svg { page: First } => typst_svg::svg(first_frame()).into_bytes(), Svg { page: Merged } => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(), @@ -233,6 +236,19 @@ fn log_err(artifact: anyhow::Result) -> Option { } } +/// Convert [`chrono::DateTime`] to [`TypstDatetime`] +fn convert_datetime(date_time: chrono::DateTime) -> Option { + use chrono::{Datelike, Timelike}; + TypstDatetime::from_ymd_hms( + date_time.year(), + date_time.month().try_into().ok()?, + date_time.day().try_into().ok()?, + date_time.hour().try_into().ok()?, + date_time.minute().try_into().ok()?, + date_time.second().try_into().ok()?, + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/tinymist/src/world.rs b/crates/tinymist/src/world.rs index 5764d189..253b3f2d 100644 --- a/crates/tinymist/src/world.rs +++ b/crates/tinymist/src/world.rs @@ -1,5 +1,6 @@ use std::{borrow::Cow, path::PathBuf, sync::Arc}; +use chrono::{DateTime, Utc}; use clap::{builder::ValueParser, ArgAction, Parser}; use comemo::Prehashed; use serde::{Deserialize, Serialize}; @@ -20,7 +21,7 @@ use typst_ts_compiler::{ const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' }; /// The font arguments for the compiler. -#[derive(Debug, Clone, Default, Parser, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompileFontArgs { /// Font paths @@ -62,6 +63,18 @@ pub struct CompileOnceArgs { /// Font related arguments. #[clap(flatten)] pub font: CompileFontArgs, + + /// The document's creation date formatted as a UNIX timestamp. + /// + /// For more information, see . + #[clap( + long = "creation-timestamp", + env = "SOURCE_DATE_EPOCH", + value_name = "UNIX_TIMESTAMP", + value_parser = parse_source_date_epoch, + hide(true), + )] + pub creation_timestamp: Option>, } /// Compiler feature for LSP world. @@ -121,3 +134,11 @@ fn parse_input_pair(raw: &str) -> Result<(String, String), String> { let val = val.trim().to_owned(); Ok((key, val)) } + +/// 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/editors/vscode/README.md b/editors/vscode/README.md index 66a77a10..fb5ba058 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -116,6 +116,14 @@ There is a **global** configuration `tinymist.typstExtraArgs` to pass extra argu typst watch --input=awa=1 --input=abaaba=2 main.typ ``` +Supported arguments: +- entry file: The last string in the array will be treated as the entry file. + - This is used to specify the **default** entry file for the compiler, which may be overridden by other settings. +- `--input`: Add a string key-value pair visible through `sys.inputs`. +- `--font-path` (environment variable: `TYPST_FONT_PATHS`), Font paths, maybe overriden by `tinymist.fontPaths`. +- `--ignore-system-fonts`: Ensures system fonts won't be searched, maybe overriden by `tinymist.systemFonts`. +- `--creation-timestamp` (environment variable: `SOURCE_DATE_EPOCH`): The document's creation date formatted as a [UNIX timestamp](https://reproducible-builds.org/specs/source-date-epoch/). + **Note:** Fix entry to `main.typ` may help multiple-file projects but you may loss diagnostics and autocompletions in unrelated files. Note: the arguments has quite low priority, and that may be overridden by other settings.