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}}` → nearest tag
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    /// Git branch from which to retrieve commit history.
137    ///
138    /// Default: `"main"`.
139    pub branch: Option<String>,
140
141    /// Flexible align
142    /// - align = "center"
143    /// - align.header = "left", align.footer = "right"
144    /// - [preprocessor.gitinfo.align] both = "center"
145    pub align: Option<AlignSetting>,
146
147    /// CSS option to adjust margin between body and footer 
148    pub margin: Option<MarginConfig>,
149
150    /// CSS option provides a hyperlink to the respective branch and commit  
151    /// in the footer
152    ///
153    /// Options: "true | false" 
154    /// Default: `false`.
155    pub hyperlink: Option<bool>,
156
157    pub timezone: Option<String>,        // "local" | "utc" | "source" | "fixed:+01:00" | "rfc3339"
158    pub datetime_format: Option<String>, // optional: if set, overrides date/time format join
159    pub show_offset: Option<bool>,       // optional: if true and no %z/%:z/%Z, append %:z
160}
161
162/// Load and deserialize the `[preprocessor.gitinfo]` table from `book.toml`.
163///
164/// # Arguments
165///
166/// * `ctx` — The [`PreprocessorContext`] provided by `mdbook`, containing
167///   the configuration tree.
168///
169/// # Errors
170///
171/// Returns an [`Error`] if the section is missing or cannot be parsed.
172///
173/// # Examples
174///
175/// ```no_run
176/// use mdbook::preprocess::PreprocessorContext;
177/// use mdbook_gitinfo::config::load_config;
178///
179/// # fn example(ctx: &PreprocessorContext) -> Result<(), mdbook::errors::Error> {
180/// let cfg = load_config(ctx)?;
181/// if let Some(template) = cfg.template {
182///     println!("Using template: {}", template);
183/// }
184/// # Ok(())
185/// # }
186/// ```
187pub fn load_config(ctx: &PreprocessorContext) -> Result<GitInfoConfig, Error> {
188    ctx.config
189        .get("preprocessor.gitinfo")
190        .and_then(|t| t.clone().try_into().ok())
191        .ok_or_else(|| Error::msg("Missing or invalid [preprocessor.gitinfo] config"))
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use mdbook::Config;
198
199    fn ctx(toml: &str) -> mdbook::preprocess::PreprocessorContext {
200        let parsed: toml::Value = toml::from_str(toml).unwrap();
201        let mut config = Config::default();
202        config.set("preprocessor.gitinfo", parsed);
203        mdbook::preprocess::PreprocessorContext { config, ..Default::default() }
204    }
205
206    #[test]
207    fn parses_legacy_align() {
208        let c = load_config(&ctx(r#"align = "left""#)).unwrap();
209        match c.align.unwrap() {
210            AlignSetting::One(s) => assert_eq!(s, "left"),
211            _ => panic!("expected One"),
212        }
213    }
214
215    #[test]
216    fn parses_split_align() {
217        let c = load_config(&ctx(r#"
218            [align]
219            both = "center"
220            header = "left"
221        "#)).unwrap();
222        match c.align.unwrap() {
223            AlignSetting::Split { header, footer, both } => {
224                assert_eq!(header.as_deref(), Some("left"));
225                assert_eq!(footer, None);
226                assert_eq!(both.as_deref(), Some("center"));
227            }
228            _ => panic!("expected Split"),
229        }
230    }
231
232    #[test]
233    fn message_resolution_parses() {
234        let c = load_config(&ctx(r#"
235            [message]
236            both = "D: {{date}}"
237            header = "H: {{date}}"
238        "#)).unwrap();
239        assert_eq!(c.message.unwrap().header.unwrap(), "H: {{date}}");
240    }
241}