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

View file

@ -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<PathBuf>,
plugins: &[impl AsRef<str>],
include_title: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<PathBuf>,
plugins: &[impl AsRef<str>],
include_title_in_outline: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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)?;
}
}

View file

@ -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<PathBuf>,
#[arg(long, value_delimiter = ',')]
plugin: Vec<String>,
#[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 {

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)
}