use mdbook_preprocessor::PreprocessorContext;
use std::fs;
use std::io;
use toml_edit::{DocumentMut, Item, Value};
const CSS_REL_PATH: &str = "theme/gitinfo.css";
pub fn ensure_gitinfo_assets(ctx: &PreprocessorContext, css_contents: &str) {
if let Err(e) = ensure_css_file(ctx, css_contents) {
eprintln!(
"[mdbook-gitinfo] Warning: unable to write {}: {e}",
CSS_REL_PATH
);
}
if let Err(e) = ensure_book_toml_additional_css(ctx) {
eprintln!("[mdbook-gitinfo] Warning: unable to update book.toml additional-css: {e}");
}
}
fn ensure_css_file(ctx: &PreprocessorContext, css_contents: &str) -> io::Result<()> {
let theme_dir = ctx.root.join("theme");
fs::create_dir_all(&theme_dir)?;
let css_path = theme_dir.join("gitinfo.css");
match fs::read_to_string(&css_path) {
Ok(existing) if existing == css_contents => Ok(()),
_ => fs::write(&css_path, css_contents),
}
}
fn ensure_book_toml_additional_css(ctx: &PreprocessorContext) -> io::Result<()> {
let book_toml = ctx.root.join("book.toml");
if !book_toml.exists() {
return Ok(());
}
let raw = fs::read_to_string(&book_toml)?;
let mut doc: DocumentMut = raw.parse().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid book.toml: {e:?}"),
)
})?;
if doc.get("output").is_none() {
doc["output"] = toml_edit::table().into();
}
if doc["output"].get("html").is_none() {
doc["output"]["html"] = toml_edit::table().into();
}
let item = doc["output"]["html"].get_mut("additional-css");
match item {
None | Some(Item::None) => {
let mut arr = toml_edit::Array::default();
arr.push(Value::from(CSS_REL_PATH));
doc["output"]["html"]["additional-css"] = Item::Value(Value::Array(arr));
}
Some(Item::Value(Value::Array(arr))) => {
let already = arr.iter().any(|v| v.as_str() == Some(CSS_REL_PATH));
if !already {
arr.push(Value::from(CSS_REL_PATH));
}
}
Some(Item::Value(Value::String(s))) => {
let existing = s.value().to_string();
let needs_css = existing != CSS_REL_PATH;
let mut arr = toml_edit::Array::default();
arr.push(Value::from(existing));
if needs_css {
arr.push(Value::from(CSS_REL_PATH));
}
doc["output"]["html"]["additional-css"] = Item::Value(Value::Array(arr));
}
Some(other) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"output.html.additional-css exists but is not a string or array (found: {:?})",
other.type_name()
),
));
}
}
let updated = doc.to_string();
if updated != raw {
fs::write(&book_toml, updated)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_preprocessor::{PreprocessorContext, config::Config};
use std::fs;
use tempfile::TempDir;
fn ctx_in_dir(dir: &TempDir) -> PreprocessorContext {
let mut config = Config::default();
PreprocessorContext::new(dir.path().to_path_buf(), config, "html".to_string())
}
#[test]
fn creates_theme_css_file() {
let dir = TempDir::new().unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
let css_path = dir.path().join("theme/gitinfo.css");
assert!(css_path.exists());
assert_eq!(fs::read_to_string(css_path).unwrap(), "/* css */");
}
#[test]
fn css_file_write_is_idempotent() {
let dir = TempDir::new().unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
ensure_gitinfo_assets(&ctx, "/* css */");
let css_path = dir.path().join("theme/gitinfo.css");
assert!(css_path.exists());
assert_eq!(fs::read_to_string(css_path).unwrap(), "/* css */");
}
#[test]
fn injects_additional_css_into_book_toml_when_missing() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("book.toml"),
r#"
[book]
title = "Test"
"#,
)
.unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
assert!(book.contains("additional-css"));
assert!(book.contains("theme/gitinfo.css"));
}
#[test]
fn does_not_duplicate_additional_css_entry() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("book.toml"),
r#"
[output.html]
additional-css = ["theme/gitinfo.css"]
"#,
)
.unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
let count = book.matches("theme/gitinfo.css").count();
assert_eq!(count, 1);
}
#[test]
fn normalizes_single_string_additional_css_to_array() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("book.toml"),
r#"
[output.html]
additional-css = "custom.css"
"#,
)
.unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
assert!(book.contains("custom.css"));
assert!(book.contains("theme/gitinfo.css"));
assert!(book.contains("additional-css = ["));
}
#[test]
fn gracefully_handles_missing_book_toml() {
let dir = TempDir::new().unwrap();
let ctx = ctx_in_dir(&dir);
ensure_gitinfo_assets(&ctx, "/* css */");
let css_path = dir.path().join("theme/gitinfo.css");
assert!(css_path.exists());
}
}