mdbash_tutorial/
lib.rs

1use lazy_static::lazy_static;
2use mdbook::book::{Book, BookItem, Chapter};
3use mdbook::errors::{Error, Result};
4use mdbook::preprocess::{Preprocessor, PreprocessorContext};
5use regex::Regex;
6use std::path::{Path,PathBuf};
7
8pub mod script;
9
10lazy_static! {
11    static ref RE: Regex = Regex::new("^\\{\\{#tutorial (.*)\\}\\}$").unwrap();
12}
13
14/// A no-op preprocessor.
15pub struct BashTutorial;
16
17impl BashTutorial {
18    pub fn new() -> BashTutorial {
19        BashTutorial
20    }
21}
22
23impl Preprocessor for BashTutorial {
24    fn name(&self) -> &str {
25        "bash-tutorial"
26    }
27
28    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
29        env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
30        let src_dir = ctx.root.join(&ctx.config.book.src);
31
32        book.for_each_mut(|section: &mut BookItem| {
33            if let BookItem::Chapter(ref mut chapter) = section {
34                if let Some(ref _source) = chapter.path {
35                    let content = BashTutorial::add_tutorial(&src_dir, chapter)
36                            .expect("error adding tutorial in chapter {chapter}");
37                    chapter.content = content;
38                }
39            }
40        });
41
42        Ok(book)
43    }
44
45    fn supports_renderer(&self, renderer: &str) -> bool {
46        renderer == "html"
47    }
48}
49
50impl BashTutorial {
51    fn add_tutorial(root: &PathBuf, chapter: &mut Chapter) -> Result<String> {
52        add_tutorial(root, &chapter.content)
53    }
54}
55
56fn add_tutorial(root: &PathBuf, content: &str) -> Result<String> {
57    let v = content
58        .lines()
59        .map(|l| {
60            if !RE.is_match(l) {
61                return l.to_string();
62            }
63
64            let f = l
65                .trim_start_matches("{{#tutorial ")
66                .trim_end_matches("}}");
67
68            let sf = Path::new(f);
69            let v = root.join(&sf);
70            if v.exists() {
71                let fp = v.as_path().to_str().expect("error joining file");
72                return script::parse(fp).to_string();
73            }
74            return l.to_string();
75        })
76        .collect::<Vec<String>>()
77        .join("\n");
78    Ok(v)
79}
80
81#[cfg(test)]
82mod test {
83    use super::*;
84
85    #[test]
86    fn nop_preprocessor_run() {
87        let input_json = r##"[
88            {
89                "root": ".",
90                "config": {
91                    "book": {
92                        "authors": ["AUTHOR"],
93                        "language": "en",
94                        "multilingual": false,
95                        "src": ".",
96                        "title": "TITLE"
97                    },
98                    "preprocessor": {
99                        "nop": {}
100                    }
101                },
102                "renderer": "html",
103                "mdbook_version": "0.4.21"
104            },
105            {
106                "sections": [
107                    {
108                        "Chapter": {
109                            "name": "Chapter 1",
110                            "content": "{{#tutorial data/test.sh}}\n",
111                            "number": [1],
112                            "sub_items": [],
113                            "path": "chapter_1.md",
114                            "source_path": "chapter_1.md",
115                            "parent_names": []
116                        }
117                    }
118                ],
119                "__non_exhaustive": null
120            }
121        ]"##;
122        let input_json = input_json.as_bytes();
123
124        let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
125        let output_json = r##"[
126            {
127                "root": ".",
128                "config": {
129                    "book": {
130                        "authors": ["AUTHOR"],
131                        "language": "en",
132                        "multilingual": false,
133                        "src": "src",
134                        "title": "TITLE"
135                    },
136                    "preprocessor": {
137                        "nop": {}
138                    }
139                },
140                "renderer": "html",
141                "mdbook_version": "0.4.21"
142            },
143            {
144                "sections": [
145                    {
146                        "Chapter": {
147                            "name": "Chapter 1",
148                            "content": "1. Test\n\n\t```bash\n\techo \"test\"\n\t```\n",
149                            "number": [1],
150                            "sub_items": [],
151                            "path": "chapter_1.md",
152                            "source_path": "chapter_1.md",
153                            "parent_names": []
154                        }
155                    }
156                ],
157                "__non_exhaustive": null
158            }
159        ]"##;
160        let output_json = output_json.as_bytes();
161
162        let (_ctx, expected_book) = mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
163        let result = BashTutorial::new().run(&ctx, book);
164        assert!(result.is_ok());
165
166        // The nop-preprocessor should not have made any changes to the book content.
167        let actual_book = result.unwrap();
168        assert_eq!(actual_book, expected_book);
169    }
170}