From 68ed807e951804e5b711b362d51552e55a904e1b Mon Sep 17 00:00:00 2001 From: agryphus Date: Mon, 27 Apr 2026 12:22:19 -0400 Subject: [PATCH] add: plugin system --- Cargo.lock | 20 ++++++++++++++ Cargo.toml | 1 + common.typ | 8 ------ prepends/image.typ | 1 + src/lib.rs | 20 +++++++++++--- src/main.rs | 19 +++++++++++--- src/plugin.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 15 deletions(-) delete mode 100644 common.typ create mode 100644 prepends/image.typ create mode 100644 src/plugin.rs diff --git a/Cargo.lock b/Cargo.lock index b0b1af1..c6be5de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1173,6 +1173,25 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -2395,6 +2414,7 @@ version = "0.1.0" dependencies = [ "clap", "env_logger", + "include_dir", "log", "typst", "typst-as-lib", diff --git a/Cargo.toml b/Cargo.toml index cabf63d..28eeeb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +include_dir = "0.7.4" clap = { version = "4.6.1", features = ["derive"] } env_logger = "0.11.10" log = "0.4.29" diff --git a/common.typ b/common.typ deleted file mode 100644 index eabfd0a..0000000 --- a/common.typ +++ /dev/null @@ -1,8 +0,0 @@ -#let image(source, width: "400px") = { - html.elem("img", attrs: ( - src: "/static/articles/" + source, - alt: source, - width: str(width), - )) -} - diff --git a/prepends/image.typ b/prepends/image.typ new file mode 100644 index 0000000..10456fb --- /dev/null +++ b/prepends/image.typ @@ -0,0 +1 @@ +#let image = (..) => [IMAGE] diff --git a/src/lib.rs b/src/lib.rs index 7c5d9dd..dbf877e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ +mod plugin; + use std::fs; use std::path::PathBuf; +pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids}; + use typst::ecow::EcoString; use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstEngine}; use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode}; @@ -10,6 +14,7 @@ use log::info; pub fn compile_article( article_dir: &PathBuf, prepend: &Option, + plugins: &[impl AsRef], include_title: bool, ) -> Result<(), Box> { info!("compiling {} ...", article_dir.display()); @@ -18,7 +23,9 @@ pub fn compile_article( let output = article_dir.join("index.html"); let outline_file = article_dir.join("outline.html"); - let prepend_content = if let Some(prepend_file) = prepend { + let plugin_block = concat_plugin_sources(plugins)?; + + let user_prepend = if let Some(prepend_file) = prepend { fs::read_to_string(&prepend_file).map_err(|e| { format!( "could not read prepend file {}: {e}", @@ -30,7 +37,11 @@ pub fn compile_article( }; let mut template: EcoString = EcoString::new(); - template.push_str(&prepend_content); + template.push_str(&plugin_block); + if !plugin_block.is_empty() && !user_prepend.is_empty() { + template.push('\n'); + } + template.push_str(&user_prepend); template.push_str( &fs::read_to_string(&template_file).map_err(|e| { format!( @@ -186,6 +197,7 @@ fn parse_outline( pub fn compile_all( root_dir: &PathBuf, prepend: &Option, + plugins: &[impl AsRef], include_title_in_outline: bool, ) -> Result<(), Box> { for entry in fs::read_dir(root_dir)? { @@ -193,10 +205,10 @@ pub fn compile_all( let path = entry.path(); if path.is_dir() { - compile_all(&path, prepend, include_title_in_outline)?; + compile_all(&path, prepend, plugins, include_title_in_outline)?; } else if path.file_name().is_some_and(|n| n == "index.typ") { let dir = path.parent().unwrap().to_path_buf(); - compile_article(&dir, prepend, include_title_in_outline)?; + compile_article(&dir, prepend, plugins, include_title_in_outline)?; } } diff --git a/src/main.rs b/src/main.rs index 787f433..ba7c1a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::env; use std::path::PathBuf; use clap::Parser; -use typssg::{compile_article, compile_all}; +use typssg::{compile_all, compile_article}; use log::{info, error}; #[derive(Parser)] @@ -13,6 +13,9 @@ struct Args { #[arg(long)] prepend: Option, + #[arg(long, value_delimiter = ',')] + plugin: Vec, + #[arg(short)] recursive: bool, @@ -31,9 +34,19 @@ fn main() { let args = Args::parse(); let result = if args.recursive { - compile_all(&args.dir, &args.prepend, args.include_title_in_outline) + compile_all( + &args.dir, + &args.prepend, + &args.plugin, + args.include_title_in_outline, + ) } else { - compile_article(&args.dir, &args.prepend, args.include_title_in_outline) + compile_article( + &args.dir, + &args.prepend, + &args.plugin, + args.include_title_in_outline, + ) }; if let Err(e) = result { diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 0000000..3f17a7d --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +use include_dir::{include_dir, Dir}; + +static PREPENDS_DIR: Dir<'static> = include_dir!("prepends"); + +fn prepends_table() -> &'static HashMap { + static TABLE: OnceLock> = OnceLock::new(); + TABLE.get_or_init(|| { + let mut m = HashMap::new(); + for file in PREPENDS_DIR.files() { + let Some(path_str) = file.path().to_str() else { + continue; + }; + if !path_str.ends_with(".typ") { + continue; + } + let id = path_str[..path_str.len() - 4].replace('\\', "/"); + let text = file + .contents_utf8() + .unwrap_or_else(|| panic!("prepends/{path_str} is not valid UTF-8")) + .to_string(); + if m.insert(id.clone(), text).is_some() { + panic!("duplicate prepend plugin id after normalize: {id}"); + } + } + m + }) +} + +pub fn list_embedded_plugin_ids() -> Vec { + let mut v: Vec = prepends_table().keys().cloned().collect(); + v.sort(); + v +} + +pub fn embedded_prepend_source(id: &str) -> Result { + let id = id.trim().replace('\\', "/"); + if id.is_empty() { + return Err("empty plugin id".into()); + } + prepends_table() + .get(&id) + .cloned() + .ok_or_else(|| { + let known = list_embedded_plugin_ids().join(", "); + if known.is_empty() { + format!("unknown plugin '{id}' (no embedded prepends in this build)") + } else { + format!("unknown plugin '{id}' (embedded: {known})") + } + }) +} + +pub fn concat_plugin_sources(plugin_ids: &[impl AsRef]) -> Result { + let mut out = String::new(); + for (i, id) in plugin_ids.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + out.push_str(&embedded_prepend_source(id.as_ref())?); + } + Ok(out) +}