mdbook_chapter_zero/
lib.rs

1use mdbook::book::Book;
2use mdbook::errors::{Error, Result};
3use mdbook::preprocess::{Preprocessor, PreprocessorContext};
4use mdbook::BookItem;
5use std::convert::{TryFrom, TryInto};
6use toml::value::Table;
7
8static DEFAULT_MARKER: &str = "<!-- ch0 -->\n";
9
10pub struct ChapterZeroPreprocessor;
11
12/// Configuration for Table of Contents generation
13#[derive(Debug)]
14pub struct Config {
15    /// Levels for which chapter zero should be applied globally.
16    /// Defaults to [], which does not apply any global changes.
17    /// If set to [0], then the top level chapters will be 0 indexed.
18    /// If set to [1], then the first level of subchapters of ALL chapters
19    /// will be 0 indexed.
20    /// If set to [0, 1], then the top level chapters and the first level of
21    /// subchapters of ALL chapters will be 0 indexed.
22    /// All (sub-)chapters affected by this setting will ignore the `marker`.
23    pub levels: Vec<usize>,
24    /// Marker to signify that the direct children of this chapter should be 0 indexed.
25    /// Defaults to `<!-- ch0 -->\n`.
26    pub marker: String,
27}
28
29impl Default for Config {
30    fn default() -> Self {
31        Config {
32            levels: vec![],
33            marker: DEFAULT_MARKER.to_string(),
34        }
35    }
36}
37
38impl<'a> TryFrom<Option<&'a Table>> for Config {
39    type Error = Error;
40
41    fn try_from(mdbook_cfg: Option<&Table>) -> Result<Config> {
42        let mut cfg = Config::default();
43        let mdbook_cfg = match mdbook_cfg {
44            Some(c) => c,
45            None => return Ok(cfg),
46        };
47
48        if let Some(levels) = mdbook_cfg.get("levels") {
49            let levels_array = match levels.as_array() {
50                Some(array) => array,
51                None => {
52                    return Err(Error::msg(format!(
53                        "Levels {levels:?} is not a valid array"
54                    )))
55                }
56            };
57
58            let mut levels: Vec<usize> = Vec::new();
59            for level_val in levels_array {
60                match level_val.as_integer() {
61                    Some(level) if level >= 0 => levels.push(level as usize),
62                    _ => {
63                        return Err(Error::msg(format!(
64                            "Level {level_val} is not a valid usize"
65                        )))
66                    }
67                };
68            }
69
70            cfg.levels = levels.try_into()?;
71        }
72
73        if let Some(marker) = mdbook_cfg.get("marker") {
74            let marker = match marker.as_str() {
75                Some(m) => m,
76                None => {
77                    return Err(Error::msg(format!(
78                        "Marker {marker:?} is not a valid string"
79                    )))
80                }
81            };
82            cfg.marker = marker.into();
83        }
84
85        Ok(cfg)
86    }
87}
88
89impl ChapterZeroPreprocessor {
90    pub fn new() -> ChapterZeroPreprocessor {
91        ChapterZeroPreprocessor
92    }
93}
94impl Preprocessor for ChapterZeroPreprocessor {
95    fn name(&self) -> &str {
96        "chapter-zero"
97    }
98
99    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
100        let cfg: Config = ctx.config.get_preprocessor(self.name()).try_into()?;
101        log::debug!("Config: {cfg:?}");
102
103        let mut local_ch0_vec: Vec<Vec<u32>> = Vec::new();
104
105        book.for_each_mut(|item: &mut BookItem| {
106            if let BookItem::Chapter(chapter) = item {
107                match chapter.number.as_mut() {
108                    Some(sn) => {
109                        // Handle global chapter zero
110                        let chapter_levels = (0..sn.0.len()).collect::<Vec<usize>>();
111                        for chl in &chapter_levels {
112                            if cfg.levels.contains(chl) {
113                                sn.0[*chl] -= 1;
114                            }
115                        }
116
117                        // Save chapters marked for local chapter zero
118                        let content = &chapter.content.replace("\r\n", "\n");
119                        if content.contains(cfg.marker.as_str()) {
120                            if !cfg.levels.contains(&sn.0.len()) {
121                                local_ch0_vec.push(sn.0.clone());
122                                chapter.content = content.replace(cfg.marker.as_str(), "");
123                            }
124                        }
125                    }
126                    None => {}
127                }
128            }
129        });
130        log::debug!("Local chapter zero will be applied to: {local_ch0_vec:?}");
131
132        // Apply local chapter zero
133        book.for_each_mut(|item: &mut BookItem| {
134            if let BookItem::Chapter(chapter) = item {
135                match chapter.number.as_mut() {
136                    Some(sn) => {
137                        for local_ch0_sn in &local_ch0_vec {
138                            if sn.0.starts_with(local_ch0_sn) && sn.0.len() > local_ch0_sn.len() {
139                                sn.0[local_ch0_sn.len()] -= 1;
140                            }
141                        }
142                    }
143                    None => {}
144                }
145            }
146        });
147
148        Ok(book)
149    }
150
151    fn supports_renderer(&self, renderer: &str) -> bool {
152        renderer != "not-supported"
153    }
154}