feat: support creation-timestamp configuration for exporting PDF (#439)

* feat: support creation-timestamp configuration for exporting PDF

* fix: respect config
This commit is contained in:
Myriad-Dreamin 2024-07-20 06:58:27 +08:00 committed by GitHub
parent 25c449c2b2
commit 103e0f3b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 97 additions and 16 deletions

4
Cargo.lock generated
View file

@ -439,8 +439,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -3740,6 +3742,7 @@ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"cargo_metadata", "cargo_metadata",
"chrono",
"clap", "clap",
"clap_builder", "clap_builder",
"clap_complete", "clap_complete",
@ -3807,6 +3810,7 @@ version = "0.11.15"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"biblatex", "biblatex",
"chrono",
"comemo 0.4.0", "comemo 0.4.0",
"dashmap", "dashmap",
"ecow 0.2.2", "ecow 0.2.2",

View file

@ -40,6 +40,7 @@ tokio-util = { version = "0.7.10", features = ["compat"] }
open = { version = "5.1.3" } open = { version = "5.1.3" }
parking_lot = "0.12.1" parking_lot = "0.12.1"
walkdir = "2" walkdir = "2"
chrono = "0.4"
# Networking # Networking
hyper = { version = "0.14", features = ["full"] } hyper = { version = "0.14", features = ["full"] }

View file

@ -31,6 +31,7 @@ walkdir.workspace = true
indexmap.workspace = true indexmap.workspace = true
ecow.workspace = true ecow.workspace = true
siphasher.workspace = true siphasher.workspace = true
chrono.workspace = true
typst.workspace = true typst.workspace = true

View file

@ -143,10 +143,11 @@ mod polymorphic {
Merged, Merged,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone)]
pub enum ExportKind { pub enum ExportKind {
#[default] Pdf {
Pdf, creation_timestamp: Option<chrono::DateTime<chrono::Utc>>,
},
Svg { Svg {
page: PageSelection, page: PageSelection,
}, },
@ -155,10 +156,18 @@ mod polymorphic {
}, },
} }
impl Default for ExportKind {
fn default() -> Self {
Self::Pdf {
creation_timestamp: None,
}
}
}
impl ExportKind { impl ExportKind {
pub fn extension(&self) -> &str { pub fn extension(&self) -> &str {
match self { match self {
Self::Pdf => "pdf", Self::Pdf { .. } => "pdf",
Self::Svg { .. } => "svg", Self::Svg { .. } => "svg",
Self::Png { .. } => "png", Self::Png { .. } => "png",
} }

View file

@ -16,6 +16,7 @@ tinymist-assets = { workspace = true }
tinymist-query.workspace = true tinymist-query.workspace = true
tinymist-render.workspace = true tinymist-render.workspace = true
sync-lsp.workspace = true sync-lsp.workspace = true
chrono.workspace = true
once_cell.workspace = true once_cell.workspace = true
anyhow.workspace = true anyhow.workspace = true

View file

@ -59,7 +59,9 @@ impl LanguageState {
output: self.compile_config().output_path.clone(), output: self.compile_config().output_path.clone(),
mode: self.compile_config().export_pdf, 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, count_words: self.config.compile.notify_status,
}); });

View file

@ -27,7 +27,13 @@ struct ExportOpts {
impl LanguageState { impl LanguageState {
/// Export the current document as PDF file(s). /// Export the current document as PDF file(s).
pub fn export_pdf(&mut self, req_id: RequestId, args: Vec<JsonValue>) -> ScheduledResult { pub fn export_pdf(&mut self, req_id: RequestId, args: Vec<JsonValue>) -> 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). /// Export the current document as Svg file(s).

View file

@ -500,7 +500,8 @@ impl CompileConfig {
entry: command.input.map(|e| Path::new(&e).into()), entry: command.input.map(|e| Path::new(&e).into()),
root_dir: command.root, root_dir: command.root,
inputs: Arc::new(Prehashed::new(inputs)), 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 font = || {
let mut opts = self.font_opts.clone(); 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; opts.ignore_system_fonts = !system_fonts;
} }
let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths); let font_paths = (!self.font_paths.is_empty()).then_some(&self.font_paths);
let 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 { if let Some(paths) = font_paths {
opts.font_paths.clone_from(paths); opts.font_paths.clone_from(paths);
} }
@ -669,6 +674,11 @@ impl CompileConfig {
combine(user_inputs, self.lsp_inputs.clone()) combine(user_inputs, self.lsp_inputs.clone())
} }
/// Determines the creation timestamp.
pub fn determine_creation_timestamp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.typst_extra_args.as_ref()?.creation_timestamp
}
fn determine_user_inputs(&self) -> ImmutDict { fn determine_user_inputs(&self) -> ImmutDict {
static EMPTY: Lazy<ImmutDict> = Lazy::new(ImmutDict::default); static EMPTY: Lazy<ImmutDict> = Lazy::new(ImmutDict::default);
@ -686,13 +696,13 @@ impl CompileConfig {
) -> ( ) -> (
Option<bool>, Option<bool>,
&Vec<PathBuf>, &Vec<PathBuf>,
Option<&Vec<PathBuf>>, Option<&CompileFontArgs>,
Option<Arc<Path>>, Option<Arc<Path>>,
) { ) {
( (
self.system_fonts, self.system_fonts,
&self.font_paths, &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()), self.determine_root(self.determine_default_entry_path().as_ref()),
) )
} }
@ -766,8 +776,10 @@ pub struct CompileExtraOpts {
pub entry: Option<ImmutPath>, pub entry: Option<ImmutPath>,
/// Additional input arguments to compile the entry file. /// Additional input arguments to compile the entry file.
pub inputs: ImmutDict, pub inputs: ImmutDict,
/// will remove later /// Additional font paths.
pub font_paths: Vec<PathBuf>, pub font: CompileFontArgs,
/// The creation timestamp for various output.
pub creation_timestamp: Option<chrono::DateTime<chrono::Utc>>,
} }
/// The path pattern that could be substituted. /// The path pattern that could be substituted.

View file

@ -8,6 +8,7 @@ use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color}; use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color};
use typst_ts_compiler::{EntryReader, EntryState, TaskInputs}; use typst_ts_compiler::{EntryReader, EntryState, TaskInputs};
use typst_ts_core::TypstDatetime;
use crate::{ use crate::{
actor::{ actor::{
@ -196,10 +197,12 @@ impl ExportConfig {
static BLANK: Lazy<Frame> = Lazy::new(Frame::default); static BLANK: Lazy<Frame> = Lazy::new(Frame::default);
let first_frame = || doc.pages.first().map(|f| &f.frame).unwrap_or(&*BLANK); let first_frame = || doc.pages.first().map(|f| &f.frame).unwrap_or(&*BLANK);
Ok(match kind2 { 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: Some(pdf_uri.as_str())
// todo: timestamp world.now() // 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: First } => typst_svg::svg(first_frame()).into_bytes(),
Svg { page: Merged } => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(), Svg { page: Merged } => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(),
@ -233,6 +236,19 @@ fn log_err<T>(artifact: anyhow::Result<T>) -> Option<T> {
} }
} }
/// Convert [`chrono::DateTime`] to [`TypstDatetime`]
fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<TypstDatetime> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,5 +1,6 @@
use std::{borrow::Cow, path::PathBuf, sync::Arc}; use std::{borrow::Cow, path::PathBuf, sync::Arc};
use chrono::{DateTime, Utc};
use clap::{builder::ValueParser, ArgAction, Parser}; use clap::{builder::ValueParser, ArgAction, Parser};
use comemo::Prehashed; use comemo::Prehashed;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -20,7 +21,7 @@ use typst_ts_compiler::{
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' }; const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
/// The font arguments for the compiler. /// 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")] #[serde(rename_all = "camelCase")]
pub struct CompileFontArgs { pub struct CompileFontArgs {
/// Font paths /// Font paths
@ -62,6 +63,18 @@ pub struct CompileOnceArgs {
/// Font related arguments. /// Font related arguments.
#[clap(flatten)] #[clap(flatten)]
pub font: CompileFontArgs, pub font: CompileFontArgs,
/// The document's creation date formatted as a UNIX timestamp.
///
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
#[clap(
long = "creation-timestamp",
env = "SOURCE_DATE_EPOCH",
value_name = "UNIX_TIMESTAMP",
value_parser = parse_source_date_epoch,
hide(true),
)]
pub creation_timestamp: Option<DateTime<Utc>>,
} }
/// Compiler feature for LSP world. /// 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(); let val = val.trim().to_owned();
Ok((key, val)) Ok((key, val))
} }
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, 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())
}

View file

@ -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 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:** 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. Note: the arguments has quite low priority, and that may be overridden by other settings.