mdbook_chapter_zero/
lib.rs1use 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#[derive(Debug)]
14pub struct Config {
15 pub levels: Vec<usize>,
24 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 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 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 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}