1#![deny(
2 warnings,
3 clippy::all,
4 clippy::cargo,
5 clippy::nursery,
6 clippy::pedantic
7)]
8#![allow(clippy::module_name_repetitions, clippy::multiple_crate_versions)]
9
10mod context;
11
12use std::path::Path;
13
14use anyhow::Context as _;
15use globwalk::GlobWalkerBuilder;
16use mdbook::book::{Book, BookItem};
17use mdbook::errors::Error;
18use mdbook::preprocess::{Preprocessor, PreprocessorContext};
19use tera::{Context, Tera};
20
21pub use self::context::{ContextSource, StaticContextSource};
22
23#[derive(Clone)]
25pub struct TeraPreprocessor<C = StaticContextSource> {
26 tera: Tera,
27 context: C,
28}
29
30impl<C> TeraPreprocessor<C> {
31 pub fn new(context: C) -> Self {
33 Self {
34 context,
35 tera: Tera::default(),
36 }
37 }
38
39 #[allow(clippy::missing_panics_doc)]
47 pub fn include_templates<P>(&mut self, root: P, glob_str: &str) -> Result<(), Error>
48 where
49 P: AsRef<Path>,
50 {
51 let root = &root.as_ref().canonicalize()?;
52
53 let paths = GlobWalkerBuilder::from_patterns(root, &[glob_str])
54 .build()?
55 .filter_map(Result::ok)
56 .map(|p| {
57 let path = p.into_path();
58 let name = path
59 .strip_prefix(root)
60 .expect("failed to strip root path prefix")
61 .to_string_lossy()
62 .replace('\\', "/");
63 (path, Some(name))
64 });
65
66 self.tera.add_template_files(paths)?;
67
68 Ok(())
69 }
70
71 pub const fn tera_mut(&mut self) -> &mut Tera {
73 &mut self.tera
74 }
75}
76
77impl<C: Default> Default for TeraPreprocessor<C> {
78 fn default() -> Self {
79 Self::new(Default::default())
80 }
81}
82
83impl<C> Preprocessor for TeraPreprocessor<C>
84where
85 C: ContextSource,
86{
87 fn name(&self) -> &'static str {
88 "tera"
89 }
90
91 fn run(&self, book_ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
92 let mut tera = Tera::default();
93 tera.extend(&self.tera).unwrap();
94
95 let mut ctx = Context::new();
96 ctx.insert("ctx", &book_ctx);
97 ctx.extend(self.context.context());
98
99 render_book_items(&mut book, &mut tera, &ctx)?;
100
101 Ok(book)
102 }
103}
104
105fn render_book_items(book: &mut Book, tera: &mut Tera, context: &Context) -> Result<(), Error> {
106 let mut templates = Vec::new();
107 collect_item_chapters(&mut templates, book.sections.as_slice())?;
109 tera.add_raw_templates(templates)?;
111 render_item_chapters(tera, context, book.sections.as_mut_slice())
113}
114
115fn collect_item_chapters<'a>(
116 templates: &mut Vec<(&'a str, &'a str)>,
117 items: &'a [BookItem],
118) -> Result<(), Error> {
119 for item in items {
120 match item {
121 BookItem::Chapter(chapter) => {
122 if let Some(ref path) = chapter.path {
123 let path = path.to_str().context("invalid chapter path")?;
124 templates.push((path, chapter.content.as_str()));
125 }
126 collect_item_chapters(templates, chapter.sub_items.as_slice())?;
127 }
128 BookItem::PartTitle(_) | BookItem::Separator => (),
129 }
130 }
131 Ok(())
132}
133
134fn render_item_chapters(
135 tera: &mut Tera,
136 context: &Context,
137 items: &mut [BookItem],
138) -> Result<(), Error> {
139 for item in items {
140 match item {
141 BookItem::Chapter(chapter) => {
142 if let Some(ref path) = chapter.path {
143 let path = path.to_str().context("invalid chapter path")?;
144 chapter.content = tera.render(path, context)?;
145 }
146 render_item_chapters(tera, context, chapter.sub_items.as_mut_slice())?;
147 }
148 BookItem::PartTitle(_) | BookItem::Separator => (),
149 }
150 }
151 Ok(())
152}