1use mdbook_preprocessor::PreprocessorContext;
2use std::fs;
3use std::io;
4use toml_edit::{DocumentMut, Item, Value};
5
6const CSS_REL_PATH: &str = "theme/gitinfo.css";
7
8pub fn ensure_gitinfo_assets(ctx: &PreprocessorContext, css_contents: &str) {
9 if let Err(e) = ensure_css_file(ctx, css_contents) {
10 eprintln!(
11 "[mdbook-gitinfo] Warning: unable to write {}: {e}",
12 CSS_REL_PATH
13 );
14 }
15
16 if let Err(e) = ensure_book_toml_additional_css(ctx) {
17 eprintln!("[mdbook-gitinfo] Warning: unable to update book.toml additional-css: {e}");
18 }
19}
20
21fn ensure_css_file(ctx: &PreprocessorContext, css_contents: &str) -> io::Result<()> {
22 let theme_dir = ctx.root.join("theme");
25 fs::create_dir_all(&theme_dir)?;
26
27 let css_path = theme_dir.join("gitinfo.css");
28
29 match fs::read_to_string(&css_path) {
31 Ok(existing) if existing == css_contents => Ok(()),
32 _ => fs::write(&css_path, css_contents),
33 }
34}
35
36fn ensure_book_toml_additional_css(ctx: &PreprocessorContext) -> io::Result<()> {
37 let book_toml = ctx.root.join("book.toml");
38
39 if !book_toml.exists() {
41 return Ok(());
42 }
43
44 let raw = fs::read_to_string(&book_toml)?;
45 let mut doc: DocumentMut = raw.parse().map_err(|e| {
46 io::Error::new(
47 io::ErrorKind::InvalidData,
48 format!("invalid book.toml: {e:?}"),
49 )
50 })?;
51
52 if doc.get("output").is_none() {
54 doc["output"] = toml_edit::table().into();
55 }
56 if doc["output"].get("html").is_none() {
57 doc["output"]["html"] = toml_edit::table().into();
58 }
59
60 let item = doc["output"]["html"].get_mut("additional-css");
62
63 match item {
64 None | Some(Item::None) => {
65 let mut arr = toml_edit::Array::default();
66 arr.push(Value::from(CSS_REL_PATH));
67 doc["output"]["html"]["additional-css"] = Item::Value(Value::Array(arr));
68 }
69
70 Some(Item::Value(Value::Array(arr))) => {
71 let already = arr.iter().any(|v| v.as_str() == Some(CSS_REL_PATH));
72 if !already {
73 arr.push(Value::from(CSS_REL_PATH));
74 }
75 }
76
77 Some(Item::Value(Value::String(s))) => {
79 let existing = s.value().to_string();
80 let needs_css = existing != CSS_REL_PATH;
81
82 let mut arr = toml_edit::Array::default();
83 arr.push(Value::from(existing));
84 if needs_css {
85 arr.push(Value::from(CSS_REL_PATH));
86 }
87
88 doc["output"]["html"]["additional-css"] = Item::Value(Value::Array(arr));
89 }
90
91 Some(other) => {
92 return Err(io::Error::new(
93 io::ErrorKind::InvalidData,
94 format!(
95 "output.html.additional-css exists but is not a string or array (found: {:?})",
96 other.type_name()
97 ),
98 ));
99 }
100 }
101
102 let updated = doc.to_string();
103
104 if updated != raw {
106 fs::write(&book_toml, updated)?;
107 }
108
109 Ok(())
110}
111
112#[cfg(test)]
116mod tests {
117 use super::*;
118 use mdbook_preprocessor::{PreprocessorContext, config::Config};
119 use std::fs;
120 use tempfile::TempDir;
121
122 fn ctx_in_dir(dir: &TempDir) -> PreprocessorContext {
123 let mut config = Config::default();
124 PreprocessorContext::new(dir.path().to_path_buf(), config, "html".to_string())
125 }
126
127 #[test]
128 fn creates_theme_css_file() {
129 let dir = TempDir::new().unwrap();
130 let ctx = ctx_in_dir(&dir);
131
132 ensure_gitinfo_assets(&ctx, "/* css */");
133
134 let css_path = dir.path().join("theme/gitinfo.css");
135 assert!(css_path.exists());
136 assert_eq!(fs::read_to_string(css_path).unwrap(), "/* css */");
137 }
138
139 #[test]
140 fn css_file_write_is_idempotent() {
141 let dir = TempDir::new().unwrap();
142 let ctx = ctx_in_dir(&dir);
143
144 ensure_gitinfo_assets(&ctx, "/* css */");
145 ensure_gitinfo_assets(&ctx, "/* css */");
146
147 let css_path = dir.path().join("theme/gitinfo.css");
148 assert!(css_path.exists());
149 assert_eq!(fs::read_to_string(css_path).unwrap(), "/* css */");
150 }
151
152 #[test]
153 fn injects_additional_css_into_book_toml_when_missing() {
154 let dir = TempDir::new().unwrap();
155
156 fs::write(
157 dir.path().join("book.toml"),
158 r#"
159[book]
160title = "Test"
161"#,
162 )
163 .unwrap();
164
165 let ctx = ctx_in_dir(&dir);
166 ensure_gitinfo_assets(&ctx, "/* css */");
167
168 let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
169 assert!(book.contains("additional-css"));
170 assert!(book.contains("theme/gitinfo.css"));
171 }
172
173 #[test]
174 fn does_not_duplicate_additional_css_entry() {
175 let dir = TempDir::new().unwrap();
176
177 fs::write(
178 dir.path().join("book.toml"),
179 r#"
180[output.html]
181additional-css = ["theme/gitinfo.css"]
182"#,
183 )
184 .unwrap();
185
186 let ctx = ctx_in_dir(&dir);
187 ensure_gitinfo_assets(&ctx, "/* css */");
188
189 let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
190 let count = book.matches("theme/gitinfo.css").count();
191 assert_eq!(count, 1);
192 }
193
194 #[test]
195 fn normalizes_single_string_additional_css_to_array() {
196 let dir = TempDir::new().unwrap();
197
198 fs::write(
199 dir.path().join("book.toml"),
200 r#"
201[output.html]
202additional-css = "custom.css"
203"#,
204 )
205 .unwrap();
206
207 let ctx = ctx_in_dir(&dir);
208 ensure_gitinfo_assets(&ctx, "/* css */");
209
210 let book = fs::read_to_string(dir.path().join("book.toml")).unwrap();
211 assert!(book.contains("custom.css"));
212 assert!(book.contains("theme/gitinfo.css"));
213 assert!(book.contains("additional-css = ["));
214 }
215
216 #[test]
217 fn gracefully_handles_missing_book_toml() {
218 let dir = TempDir::new().unwrap();
219 let ctx = ctx_in_dir(&dir);
220
221 ensure_gitinfo_assets(&ctx, "/* css */");
223
224 let css_path = dir.path().join("theme/gitinfo.css");
225 assert!(css_path.exists());
226 }
227}