diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index fd0eb5f05..39f10f78a 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -290,6 +290,15 @@ pub struct WorldArgs { )] pub inputs: Vec<(String, String)>, + /// Add a string key-path pairs where the content will visible through `sys.input-files`. + #[clap( + long = "input-file", + value_name = "key=value", + action = ArgAction::Append, + value_parser = ValueParser::new(parse_sys_input_file_pair), + )] + pub input_files: Vec<(String, Vec)>, + /// Common font arguments. #[clap(flatten)] pub font: FontArgs, @@ -607,6 +616,18 @@ fn parse_sys_input_pair(raw: &str) -> Result<(String, String), String> { Ok((key, val)) } +/// Parses key/path pairs split by the first equal sign. +/// +/// This functions works similar to `parse_sys_input_pair`, but the value is +/// interpreted as a path and the content of the file is read. +fn parse_sys_input_file_pair(raw: &str) -> Result<(String, Vec), String> { + let (key, value) = parse_sys_input_pair(raw)?; + match std::fs::read(&value) { + Ok(content) => Ok((key, content)), + Err(err) => Err(format!("could not read file `{value}`: {err}")), + } +} + /// Parses a UNIX timestamp according to fn parse_source_date_epoch(raw: &str) -> Result, String> { let timestamp: i64 = raw diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 2da03d4d5..f3e04f8ae 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -112,6 +112,12 @@ impl SystemWorld { .map(|(k, v)| (k.as_str().into(), v.as_str().into_value())) .collect(); + let input_files: Dict = world_args + .input_files + .iter() + .map(|(k, v)| (k.as_str().into(), Bytes::new(v.to_owned()).into_value())) + .collect(); + let features = process_args .features .iter() @@ -120,7 +126,11 @@ impl SystemWorld { }) .collect(); - Library::builder().with_inputs(inputs).with_features(features).build() + Library::builder() + .with_inputs(inputs) + .with_input_files(input_files) + .with_features(features) + .build() }; let fonts = Fonts::searcher() diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index d42be15b1..d0da1a804 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -87,7 +87,12 @@ use crate::routines::EvalMode; use crate::{Feature, Features}; /// Hook up all `foundations` definitions. -pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { +pub(super) fn define( + global: &mut Scope, + inputs: Dict, + input_files: Dict, + features: &Features, +) { global.start_category(crate::Category::Foundations); global.define_type::(); global.define_type::(); @@ -118,7 +123,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_func::(); } global.define("calc", calc::module()); - global.define("sys", sys::module(inputs)); + global.define("sys", sys::module(inputs, input_files)); global.reset_category(); } diff --git a/crates/typst-library/src/foundations/sys.rs b/crates/typst-library/src/foundations/sys.rs index 7c1281047..83a5c5dec 100644 --- a/crates/typst-library/src/foundations/sys.rs +++ b/crates/typst-library/src/foundations/sys.rs @@ -3,7 +3,7 @@ use crate::foundations::{Dict, Module, Scope, Version}; /// A module with system-related things. -pub fn module(inputs: Dict) -> Module { +pub fn module(inputs: Dict, input_files: Dict) -> Module { let mut scope = Scope::deduplicating(); scope.define( "version", @@ -14,5 +14,6 @@ pub fn module(inputs: Dict) -> Module { ]), ); scope.define("inputs", inputs); + scope.define("input-files", input_files); Module::new("sys", scope) } diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index c39024f71..83c0e03e1 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -174,6 +174,7 @@ impl Default for Library { #[derive(Debug, Clone, Default)] pub struct LibraryBuilder { inputs: Option, + input_files: Option, features: Features, } @@ -184,6 +185,12 @@ impl LibraryBuilder { self } + /// Configure the input files visible through `sys.input-files`. + pub fn with_input_files(mut self, input_files: Dict) -> Self { + self.input_files = Some(input_files); + self + } + /// Configure in-development features that should be enabled. /// /// No guarantees whatsover! @@ -196,7 +203,8 @@ impl LibraryBuilder { pub fn build(self) -> Library { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); - let global = global(math.clone(), inputs, &self.features); + let input_files = self.input_files.unwrap_or_default(); + let global = global(math.clone(), inputs, input_files, &self.features); Library { global: global.clone(), math, @@ -278,10 +286,10 @@ impl Category { } /// Construct the module with global definitions. -fn global(math: Module, inputs: Dict, features: &Features) -> Module { +fn global(math: Module, inputs: Dict, input_files: Dict, features: &Features) -> Module { let mut global = Scope::deduplicating(); - self::foundations::define(&mut global, inputs, features); + self::foundations::define(&mut global, inputs, input_files, features); self::model::define(&mut global); self::text::define(&mut global); self::layout::define(&mut global); diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 8fea3a1f2..d07ba74fa 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -151,6 +151,14 @@ The value is always of type [string]($str). More complex data may be parsed manually using functions like [`json.decode`]($json.decode). + - The `sys.input-files` [dictionary], which makes external files + available to the project. An input specified in the command line as + `--input key=path` becomes available under `sys.input-files.key` as + the content of the specified file. To include spaces in the path, it may + be enclosed with single or double quotes. + + The value is always of type [byte buffers]($bytes). + - name: sym title: General category: symbols