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 { MarginSetting::One("0".to_string()) }
52}
53
54#[derive(Debug, Deserialize, Default)]
55pub struct MarginConfig {
56 pub header: Option<MarginSetting>,
57 pub footer: Option<MarginSetting>,
58 pub both: Option<MarginSetting>,
59}
60
61#[derive(Debug, Deserialize)]
62#[serde(untagged)]
63pub enum AlignSetting {
64 /// Legacy: align = "center"
65 One(String),
66 /// New: align = { header = "...", footer = "...", both = "..." }
67 Split {
68 header: Option<String>,
69 footer: Option<String>,
70 both: Option<String>,
71 },
72}
73
74impl Default for AlignSetting {
75 fn default() -> Self { AlignSetting::One("center".to_string()) }
76}
77
78/// Represents the user-defined configuration options under `[preprocessor.gitinfo]`
79/// in `book.toml`.
80///
81/// Each field is optional; defaults are handled in the preprocessor logic.
82/// The configuration allows users to control how commit metadata is formatted
83/// and rendered in the generated book.
84#[derive(Debug, Deserialize,Default)]
85pub struct GitInfoConfig {
86 /// Gate to turn the preprocessor on/off without removing the section.
87 /// Default: true (when omitted).
88 pub enable: Option<bool>,
89
90 /// The formatting style of the git data (currently unused, reserved for future use).
91 pub format: Option<String>,
92
93 /// Template string defining how git metadata is rendered.
94 ///
95 /// Supported placeholders:
96 /// - `{{hash}}` → short commit hash
97 /// - `{{long}}` → full commit hash
98 /// - `{{tag}}` → lastest tag or user defined
99 /// - `{{date}}` → commit date
100 /// - `{{sep}}` → separator string
101 /// (Deprecated) Old single template. If present, used as a fallback for footer_message.
102 pub template: Option<String>,
103
104 // Placement switches
105 pub header: Option<bool>,
106 pub footer: Option<bool>,
107
108 /// Message templates in a table: message.header/message.footer/message.both
109 pub message: Option<MessageConfig>,
110
111 /// CSS font size for the rendered footer text.
112 ///
113 /// Default: `"0.8em"`.
114 #[serde(rename = "font-size")]
115 pub font_size: Option<String>,
116
117 /// String separator inserted between elements (e.g., date and hash).
118 ///
119 /// Default: `" • "`.
120 pub separator: Option<String>,
121
122 /// Format string for the date component.
123 ///
124 /// Uses the [`chrono`] crate formatting syntax.
125 /// Default: `"%Y-%m-%d"`.
126 #[serde(rename = "date-format")]
127 pub date_format: Option<String>,
128
129 /// Format string for the time component.
130 ///
131 /// Uses the [`chrono`] crate formatting syntax.
132 /// Default: `"%H:%M:%S"`.
133 #[serde(rename = "time-format")]
134 pub time_format: Option<String>,
135
136
137 pub timezone: Option<String>, // "local" | "utc" | "source" | "fixed:+01:00" | "rfc3339"
138 pub datetime_format: Option<String>, // optional: if set, overrides date/time format join
139 pub show_offset: Option<bool>, // optional: if true and no %z/%:z/%Z, append %:z
140
141 /// Git branch from which to retrieve commit history.
142 ///
143 /// Default: `"main"`.
144 pub branch: Option<String>,
145
146 /// Flexible align
147 /// - align = "center"
148 /// - align.header = "left", align.footer = "right"
149 /// - [preprocessor.gitinfo.align] both = "center"
150 pub align: Option<AlignSetting>,
151
152 /// CSS option to adjust margin between body and footer
153 pub margin: Option<MarginConfig>,
154
155 // explicit tag override (if set, use this instead of auto-detect)
156 pub tag: Option<String>,
157
158 /// CSS option provides a hyperlink to the respective branch and commit
159 /// in the footer
160 ///
161 /// Options: "true | false"
162 /// Default: `false`.
163 pub hyperlink: Option<bool>,
164
165}
166
167/// Load and deserialize the `[preprocessor.gitinfo]` table from `book.toml`.
168///
169/// # Arguments
170///
171/// * `ctx` — The [`PreprocessorContext`] provided by `mdbook`, containing
172/// the configuration tree.
173///
174/// # Errors
175///
176/// Returns an [`Error`] if the section is missing or cannot be parsed.
177///
178/// # Examples
179///
180/// ```no_run
181/// use mdbook::preprocess::PreprocessorContext;
182/// use mdbook_gitinfo::config::load_config;
183///
184/// # fn example(ctx: &PreprocessorContext) -> Result<(), mdbook::errors::Error> {
185/// let cfg = load_config(ctx)?;
186/// if let Some(template) = cfg.template {
187/// println!("Using template: {}", template);
188/// }
189/// # Ok(())
190/// # }
191/// ```
192pub fn load_config(ctx: &PreprocessorContext) -> Result<GitInfoConfig, Error> {
193 ctx.config
194 .get("preprocessor.gitinfo")
195 .and_then(|t| t.clone().try_into().ok())
196 .ok_or_else(|| Error::msg("Missing or invalid [preprocessor.gitinfo] config"))
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use mdbook::Config;
203
204 fn ctx(toml: &str) -> mdbook::preprocess::PreprocessorContext {
205 let parsed: toml::Value = toml::from_str(toml).unwrap();
206 let mut config = Config::default();
207 config.set("preprocessor.gitinfo", parsed);
208 mdbook::preprocess::PreprocessorContext { config, ..Default::default() }
209 }
210
211 #[test]
212 fn parses_legacy_align() {
213 let c = load_config(&ctx(r#"align = "left""#)).unwrap();
214 match c.align.unwrap() {
215 AlignSetting::One(s) => assert_eq!(s, "left"),
216 _ => panic!("expected One"),
217 }
218 }
219
220 #[test]
221 fn parses_split_align() {
222 let c = load_config(&ctx(r#"
223 [align]
224 both = "center"
225 header = "left"
226 "#)).unwrap();
227 match c.align.unwrap() {
228 AlignSetting::Split { header, footer, both } => {
229 assert_eq!(header.as_deref(), Some("left"));
230 assert_eq!(footer, None);
231 assert_eq!(both.as_deref(), Some("center"));
232 }
233 _ => panic!("expected Split"),
234 }
235 }
236
237 #[test]
238 fn message_resolution_parses() {
239 let c = load_config(&ctx(r#"
240 [message]
241 both = "D: {{date}}"
242 header = "H: {{date}}"
243 "#)).unwrap();
244 assert_eq!(c.message.unwrap().header.unwrap(), "H: {{date}}");
245 }
246}