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_preprocessor::PreprocessorContext;
21use mdbook_preprocessor::errors::Error;
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/// This function reads configuration values from the mdBook configuration
230/// tree and deserializes them into [`GitInfoConfig`].
231///
232/// If the `[preprocessor.gitinfo]` section is missing, default values are
233/// returned. Invalid configuration values result in an error.
234///
235/// # Arguments
236///
237/// * `ctx` — The [`PreprocessorContext`] provided by `mdbook-preprocessor`,
238///   containing the parsed `book.toml` configuration.
239///
240/// # Errors
241///
242/// Returns an [`Error`] if the configuration section exists but cannot be
243/// deserialized into [`GitInfoConfig`].
244///
245/// # Examples
246///
247/// ```no_run
248/// use mdbook_preprocessor::PreprocessorContext;
249/// use mdbook_gitinfo::config::load_config;
250///
251/// fn example(ctx: &PreprocessorContext) -> Result<(), mdbook_preprocessor::errors::Error> {
252///     let cfg = load_config(ctx)?;
253///
254///     if let Some(template) = cfg.template {
255///         println!("Using template: {}", template);
256///     }
257///
258///     Ok(())
259/// }
260/// ```
261pub fn load_config(ctx: &PreprocessorContext) -> Result<GitInfoConfig, Error> {
262    ctx.config
263        .get::<GitInfoConfig>("preprocessor.gitinfo")?
264        .ok_or_else(|| Error::msg("Missing or invalid [preprocessor.gitinfo] config"))
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use mdbook_preprocessor::{PreprocessorContext, config::Config};
271    use std::path::PathBuf;
272
273    fn ctx(toml_str: &str) -> PreprocessorContext {
274        let parsed: toml::Value = toml::from_str(toml_str).unwrap();
275
276        let mut config = Config::default();
277        // Config::set returns Result in mdBook 0.5.x
278        config.set("preprocessor.gitinfo", parsed).unwrap();
279
280        // PreprocessorContext is non_exhaustive; use constructor
281        PreprocessorContext::new(PathBuf::from("."), config, "html".to_string())
282    }
283
284    #[test]
285    fn parses_legacy_align() {
286        let c = load_config(&ctx(r#"align = "left""#)).unwrap();
287        match c.align.unwrap() {
288            AlignSetting::One(s) => assert_eq!(s, "left"),
289            _ => panic!("expected One"),
290        }
291    }
292
293    #[test]
294    fn parses_split_align() {
295        let c = load_config(&ctx(r#"
296            [align]
297            both = "center"
298            header = "left"
299        "#))
300        .unwrap();
301
302        match c.align.unwrap() {
303            AlignSetting::Split {
304                header,
305                footer,
306                both,
307            } => {
308                assert_eq!(header.as_deref(), Some("left"));
309                assert_eq!(footer, None);
310                assert_eq!(both.as_deref(), Some("center"));
311            }
312            _ => panic!("expected Split"),
313        }
314    }
315
316    #[test]
317    fn message_resolution_parses() {
318        let c = load_config(&ctx(r#"
319            [message]
320            both = "D: {{date}}"
321            header = "H: {{date}}"
322        "#))
323        .unwrap();
324
325        assert_eq!(c.message.unwrap().header.unwrap(), "H: {{date}}");
326    }
327}