mdbook_gitinfo/config.rs
1//! Configuration module for the `mdbook-gitinfo` preprocessor.
2//!
3//! This module defines [`GitInfoConfig`], the structure that holds all
4//! user-defined configuration options from the `[preprocessor.gitinfo]`
5//! section in `book.toml`. It also provides [`load_config`] to deserialize
6//! these values into the struct for use by the preprocessor.
7//!
8//! # Example `book.toml`
9//!
10//! ```toml
11//! [preprocessor.gitinfo]
12//! template = "Date: {{date}} • Commit: {{hash}}"
13//! font-size = "0.8em"
14//! separator = " | "
15//! date-format = "%Y-%m-%d"
16//! time-format = "%H:%M:%S"
17//! branch = "main"
18//! ```
19
20use mdbook::errors::Error;
21use mdbook::preprocess::PreprocessorContext;
22use serde::Deserialize;
23
24#[derive(Debug, Deserialize, Default)]
25pub struct MessageConfig {
26 /// Header message template
27 pub header: Option<String>,
28 /// Footer message template
29 pub footer: Option<String>,
30 /// Default for both (used if header/footer not set)
31 pub both: Option<String>,
32}
33
34#[derive(Debug, Deserialize)]
35#[serde(untagged)]
36pub enum MarginSetting {
37 /// "1em"
38 One(String),
39 /// ["top", "right", "bottom", "left"] — supports 1–4 entries like CSS shorthand
40 Quad(Vec<String>),
41 /// { top = "...", right = "...", bottom = "...", left = "..." }
42 Sides {
43 top: Option<String>,
44 right: Option<String>,
45 bottom: Option<String>,
46 left: Option<String>,
47 },
48}
49
50impl Default for MarginSetting {
51 fn default() -> Self {
52 MarginSetting::One("0".to_string())
53 }
54}
55
56#[derive(Debug, Deserialize, Default)]
57pub struct MarginConfig {
58 pub header: Option<MarginSetting>,
59 pub footer: Option<MarginSetting>,
60 pub both: Option<MarginSetting>,
61}
62
63#[derive(Debug, Deserialize)]
64#[serde(untagged)]
65pub enum AlignSetting {
66 /// Legacy: align = "center"
67 One(String),
68 /// New: align = { header = "...", footer = "...", both = "..." }
69 Split {
70 header: Option<String>,
71 footer: Option<String>,
72 both: Option<String>,
73 },
74}
75
76impl Default for AlignSetting {
77 fn default() -> Self {
78 AlignSetting::One("center".to_string())
79 }
80}
81
82#[derive(Debug, Deserialize, Clone, Copy)]
83#[serde(rename_all = "lowercase")]
84pub enum ContributorsSource {
85 Git,
86 File,
87 Inline,
88}
89
90impl Default for ContributorsSource {
91 fn default() -> Self {
92 ContributorsSource::Git
93 }
94}
95
96/// Represents the user-defined configuration options under `[preprocessor.gitinfo]`
97/// in `book.toml`.
98///
99/// Each field is optional; defaults are handled in the preprocessor logic.
100/// The configuration allows users to control how commit metadata is formatted
101/// and rendered in the generated book.
102#[derive(Debug, Deserialize, Default)]
103pub struct GitInfoConfig {
104 /// Gate to turn the preprocessor on/off without removing the section.
105 /// Default: true (when omitted).
106 pub enable: Option<bool>,
107
108 /// The formatting style of the git data (currently unused, reserved for future use).
109 pub format: Option<String>,
110
111 /// Template string defining how git metadata is rendered.
112 ///
113 /// Supported placeholders:
114 /// - `{{hash}}` → short commit hash
115 /// - `{{long}}` → full commit hash
116 /// - `{{tag}}` → lastest tag or user defined
117 /// - `{{date}}` → commit date
118 /// - `{{sep}}` → separator string
119 /// (Deprecated) Old single template. If present, used as a fallback for footer_message.
120 pub template: Option<String>,
121
122 // Placement switches
123 pub header: Option<bool>,
124 pub footer: Option<bool>,
125
126 /// Message templates in a table: message.header/message.footer/message.both
127 pub message: Option<MessageConfig>,
128
129 /// CSS font size for the rendered footer text.
130 ///
131 /// Default: `"0.8em"`.
132 #[serde(rename = "font-size")]
133 pub font_size: Option<String>,
134
135 /// String separator inserted between elements (e.g., date and hash).
136 ///
137 /// Default: `" • "`.
138 pub separator: Option<String>,
139
140 /// Format string for the date component.
141 ///
142 /// Uses the [`chrono`] crate formatting syntax.
143 /// Default: `"%Y-%m-%d"`.
144 #[serde(rename = "date-format")]
145 pub date_format: Option<String>,
146
147 /// Format string for the time component.
148 ///
149 /// Uses the [`chrono`] crate formatting syntax.
150 /// Default: `"%H:%M:%S"`.
151 #[serde(rename = "time-format")]
152 pub time_format: Option<String>,
153
154 pub timezone: Option<String>, // "local" | "utc" | "source" | "fixed:+01:00" | "rfc3339"
155 pub datetime_format: Option<String>, // optional: if set, overrides date/time format join
156 pub show_offset: Option<bool>, // optional: if true and no %z/%:z/%Z, append %:z
157
158 /// Git branch from which to retrieve commit history.
159 ///
160 /// Default: `"main"`.
161 pub branch: Option<String>,
162
163 /// Flexible align
164 /// - align = "center"
165 /// - align.header = "left", align.footer = "right"
166 /// - [preprocessor.gitinfo.align] both = "center"
167 pub align: Option<AlignSetting>,
168
169 /// CSS option to adjust margin between body and footer
170 pub margin: Option<MarginConfig>,
171
172 // explicit tag override (if set, use this instead of auto-detect)
173 pub tag: Option<String>,
174
175 /// CSS option provides a hyperlink to the respective branch and commit
176 /// in the footer
177 ///
178 /// Options: "true | false"
179 /// Default: `false`.
180 pub hyperlink: Option<bool>,
181
182 /// Git Contributor switch
183 pub contributors: Option<bool>,
184
185 /// Optional title for the contributors block.
186 ///
187 /// Default: "Contributors:"
188 #[serde(rename = "contributors-title")]
189 pub contributors_title: Option<String>,
190
191 /// Optional message that can be set under the title
192 ///
193 /// Default: ""
194 #[serde(rename = "contributors-message")]
195 pub contributors_message: Option<String>,
196
197 /// Where to source contributors from.
198 ///
199 /// Options: "git" (default), "file", "inline"
200 ///
201 /// - git: derive from `git shortlog -sne --all`
202 /// - file: read from CONTRIBUTORS.md (or contributors-file)
203 /// - inline: only `{% contributors a b %}` tokens are used
204 #[serde(rename = "contributors-source")]
205 pub contributors_source: Option<ContributorsSource>,
206
207 /// File path (relative to book root) used when contributors-source = "file".
208 /// Default: "CONTRIBUTORS.md"
209 #[serde(rename = "contributors-file")]
210 pub contributors_file: Option<String>,
211
212 /// List of contributor author names to exclude.
213 ///
214 /// Matches against the git author name (treated as GitHub username).
215 ///
216 /// Example:
217 /// contributors-exclude = ["github-actions[bot]", "template-author"]
218 #[serde(rename = "contributors-exclude")]
219 pub contributors_exclude: Option<Vec<String>>,
220
221 /// Maximum number of contributor avatars shown before collapsing into a "Show all" expander.
222 /// Default: 24
223 #[serde(rename = "contributors-max-visible")]
224 pub contributors_max_visible: Option<usize>,
225}
226
227/// Load and deserialize the `[preprocessor.gitinfo]` table from `book.toml`.
228///
229/// # Arguments
230///
231/// * `ctx` — The [`PreprocessorContext`] provided by `mdbook`, containing
232/// the configuration tree.
233///
234/// # Errors
235///
236/// Returns an [`Error`] if the section is missing or cannot be parsed.
237///
238/// # Examples
239///
240/// ```no_run
241/// use mdbook::preprocess::PreprocessorContext;
242/// use mdbook_gitinfo::config::load_config;
243///
244/// # fn example(ctx: &PreprocessorContext) -> Result<(), mdbook::errors::Error> {
245/// let cfg = load_config(ctx)?;
246/// if let Some(template) = cfg.template {
247/// println!("Using template: {}", template);
248/// }
249/// # Ok(())
250/// # }
251/// ```
252pub fn load_config(ctx: &PreprocessorContext) -> Result<GitInfoConfig, Error> {
253 ctx.config
254 .get("preprocessor.gitinfo")
255 .and_then(|t| t.clone().try_into().ok())
256 .ok_or_else(|| Error::msg("Missing or invalid [preprocessor.gitinfo] config"))
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use mdbook::Config;
263
264 fn ctx(toml: &str) -> mdbook::preprocess::PreprocessorContext {
265 let parsed: toml::Value = toml::from_str(toml).unwrap();
266 let mut config = Config::default();
267 config.set("preprocessor.gitinfo", parsed);
268 mdbook::preprocess::PreprocessorContext {
269 config,
270 ..Default::default()
271 }
272 }
273
274 #[test]
275 fn parses_legacy_align() {
276 let c = load_config(&ctx(r#"align = "left""#)).unwrap();
277 match c.align.unwrap() {
278 AlignSetting::One(s) => assert_eq!(s, "left"),
279 _ => panic!("expected One"),
280 }
281 }
282
283 #[test]
284 fn parses_split_align() {
285 let c = load_config(&ctx(r#"
286 [align]
287 both = "center"
288 header = "left"
289 "#))
290 .unwrap();
291 match c.align.unwrap() {
292 AlignSetting::Split {
293 header,
294 footer,
295 both,
296 } => {
297 assert_eq!(header.as_deref(), Some("left"));
298 assert_eq!(footer, None);
299 assert_eq!(both.as_deref(), Some("center"));
300 }
301 _ => panic!("expected Split"),
302 }
303 }
304
305 #[test]
306 fn message_resolution_parses() {
307 let c = load_config(&ctx(r#"
308 [message]
309 both = "D: {{date}}"
310 header = "H: {{date}}"
311 "#))
312 .unwrap();
313 assert_eq!(c.message.unwrap().header.unwrap(), "H: {{date}}");
314 }
315}