add: plugin system

This commit is contained in:
agryphus 2026-04-27 12:22:19 -04:00
parent aa7b1d0a63
commit 68ed807e95
7 changed files with 119 additions and 15 deletions

20
Cargo.lock generated
View file

@ -1173,6 +1173,25 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@ -2395,6 +2414,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"env_logger", "env_logger",
"include_dir",
"log", "log",
"typst", "typst",
"typst-as-lib", "typst-as-lib",

View file

@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
include_dir = "0.7.4"
clap = { version = "4.6.1", features = ["derive"] } clap = { version = "4.6.1", features = ["derive"] }
env_logger = "0.11.10" env_logger = "0.11.10"
log = "0.4.29" log = "0.4.29"

View file

@ -1,8 +0,0 @@
#let image(source, width: "400px") = {
html.elem("img", attrs: (
src: "/static/articles/" + source,
alt: source,
width: str(width),
))
}

1
prepends/image.typ Normal file
View file

@ -0,0 +1 @@
#let image = (..) => [IMAGE]

View file

@ -1,6 +1,10 @@
mod plugin;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
use typst::ecow::EcoString; use typst::ecow::EcoString;
use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstEngine}; use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstEngine};
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode}; use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode};
@ -10,6 +14,7 @@ use log::info;
pub fn compile_article( pub fn compile_article(
article_dir: &PathBuf, article_dir: &PathBuf,
prepend: &Option<PathBuf>, prepend: &Option<PathBuf>,
plugins: &[impl AsRef<str>],
include_title: bool, include_title: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!("compiling {} ...", article_dir.display()); info!("compiling {} ...", article_dir.display());
@ -18,7 +23,9 @@ pub fn compile_article(
let output = article_dir.join("index.html"); let output = article_dir.join("index.html");
let outline_file = article_dir.join("outline.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| { fs::read_to_string(&prepend_file).map_err(|e| {
format!( format!(
"could not read prepend file {}: {e}", "could not read prepend file {}: {e}",
@ -30,7 +37,11 @@ pub fn compile_article(
}; };
let mut template: EcoString = EcoString::new(); 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( template.push_str(
&fs::read_to_string(&template_file).map_err(|e| { &fs::read_to_string(&template_file).map_err(|e| {
format!( format!(
@ -186,6 +197,7 @@ fn parse_outline(
pub fn compile_all( pub fn compile_all(
root_dir: &PathBuf, root_dir: &PathBuf,
prepend: &Option<PathBuf>, prepend: &Option<PathBuf>,
plugins: &[impl AsRef<str>],
include_title_in_outline: bool, include_title_in_outline: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
for entry in fs::read_dir(root_dir)? { for entry in fs::read_dir(root_dir)? {
@ -193,10 +205,10 @@ pub fn compile_all(
let path = entry.path(); let path = entry.path();
if path.is_dir() { 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") { } else if path.file_name().is_some_and(|n| n == "index.typ") {
let dir = path.parent().unwrap().to_path_buf(); 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)?;
} }
} }

View file

@ -2,7 +2,7 @@ use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use typssg::{compile_article, compile_all}; use typssg::{compile_all, compile_article};
use log::{info, error}; use log::{info, error};
#[derive(Parser)] #[derive(Parser)]
@ -13,6 +13,9 @@ struct Args {
#[arg(long)] #[arg(long)]
prepend: Option<PathBuf>, prepend: Option<PathBuf>,
#[arg(long, value_delimiter = ',')]
plugin: Vec<String>,
#[arg(short)] #[arg(short)]
recursive: bool, recursive: bool,
@ -31,9 +34,19 @@ fn main() {
let args = Args::parse(); let args = Args::parse();
let result = if args.recursive { 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 { } 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 { if let Err(e) = result {

65
src/plugin.rs Normal file
View file

@ -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<String, String> {
static TABLE: OnceLock<HashMap<String, String>> = 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<String> {
let mut v: Vec<String> = prepends_table().keys().cloned().collect();
v.sort();
v
}
pub fn embedded_prepend_source(id: &str) -> Result<String, String> {
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<str>]) -> Result<String, String> {
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)
}