mdbook_tera/
lib.rs

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/// A mdBook preprocessor that renders Tera.
24#[derive(Clone)]
25pub struct TeraPreprocessor<C = StaticContextSource> {
26    tera: Tera,
27    context: C,
28}
29
30impl<C> TeraPreprocessor<C> {
31    /// Construct a Tera preprocessor given a context source.
32    pub fn new(context: C) -> Self {
33        Self {
34            context,
35            tera: Tera::default(),
36        }
37    }
38
39    /// Includes Tera templates given a glob pattern and a root directory.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the provided path cannot be canonicalized or the
44    /// inheritance chain can't be built, such as adding a child template
45    /// without the parent one.
46    #[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    /// Returns a mutable reference to the internal Tera engine.
72    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    // Build the list of templates
108    collect_item_chapters(&mut templates, book.sections.as_slice())?;
109    // Register them
110    tera.add_raw_templates(templates)?;
111    // Render chapters
112    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}