lib/render/
renderer.rs

1//! Defines types to build and manage templates.
2
3use std::collections::HashSet;
4use std::fs::File;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use serde::Serialize;
9use walkdir::DirEntry;
10
11use crate::contexts::annotation::AnnotationContext;
12use crate::contexts::book::BookContext;
13use crate::contexts::entry::EntryContext;
14use crate::models::entry::Entry;
15use crate::result::{Error, Result};
16
17use super::engine::RenderEngine;
18use super::names::NamesRender;
19use super::template::{ContextMode, Render, StructureMode, Template, TemplatePartial};
20use super::utils;
21
22/// A struct providing a simple interface to build and render templates.
23#[derive(Debug, Default)]
24pub struct Renderer {
25    /// The render engine containing the parsed templates ready for rendering.
26    engine: RenderEngine,
27
28    /// The default template to use when no templates directory is specified.
29    template_default: String,
30
31    /// A list of all registed templates.
32    templates: Vec<Template>,
33
34    /// A list of all registed partial templates.
35    templates_partial: Vec<TemplatePartial>,
36
37    /// A list of all rendered templates.
38    renders: Vec<Render>,
39
40    /// An instance of [`RenderOptions`].
41    options: RenderOptions,
42}
43
44impl Renderer {
45    /// Returns a new instance of [`Renderer`].
46    ///
47    /// # Arguments
48    ///
49    /// * `options` - The render options.
50    /// * `default` - A string representing the contents of a template to build as the default. Used
51    ///   when no templates directory is specified.
52    #[must_use]
53    pub fn new<O>(options: O, default: String) -> Self
54    where
55        O: Into<RenderOptions>,
56    {
57        Self {
58            template_default: default,
59            options: options.into(),
60            ..Default::default()
61        }
62    }
63
64    /// Initializes [`Renderer`] by building [`Template`]s depending on whether a templates
65    /// directory is provided or not. If none is provided then the default template is built.
66    ///
67    /// # Errors
68    ///
69    /// Will return `Err` if:
70    /// * A template contains either syntax errors or contains variables that reference non-existent
71    ///   fields in a [`Book`][book]/[`Annotation`][annotation].
72    /// * A template's config block isn't formatted correctly, has syntax errors or is missing
73    ///   required fields.
74    /// * A requested template-group does not exist.
75    /// * Any IO errors are encountered.
76    ///
77    /// [book]: crate::models::book::Book
78    /// [annotation]: crate::models::annotation::Annotation
79    pub fn init(&mut self) -> Result<()> {
80        if let Some(path) = &self.options.templates_directory {
81            self.build_from_directory(&path.clone())?;
82            // +----------------------^^^^^^^^^^^^^
83            // +---- Cloning here to prevent mutable & immutable borrows.
84        } else {
85            self.build_default()?;
86        }
87
88        self.validate_requested_template_groups()?;
89
90        Ok(())
91    }
92
93    /// Iterates through all [`Template`]s and renders them based on their [`StructureMode`] and
94    /// [`ContextMode`]. See respective enums for more information.
95    ///
96    /// # Arguments
97    ///
98    /// * `entry` - The entry to be rendered.
99    ///
100    /// # Errors
101    ///
102    /// Will return `Err` if any IO errors are encountered.
103    pub fn render(&mut self, entry: &Entry) -> Result<()> {
104        let mut renders = Vec::with_capacity(self.templates.len());
105
106        let entry = EntryContext::from(entry);
107
108        for template in self.iter_requested_templates() {
109            let names = NamesRender::new(&entry, template)?;
110
111            // Builds a the template's output path, relative to the [output-directory].
112            let path = match template.structure_mode {
113                StructureMode::Flat => {
114                    // -> [output-directory]
115                    PathBuf::new()
116                }
117                StructureMode::FlatGrouped => {
118                    // -> [output-directory]/[template-group]
119                    PathBuf::from(&template.group)
120                }
121                StructureMode::Nested => {
122                    // -> [output-directory]/[author-title]
123                    PathBuf::from(&names.directory)
124                }
125                StructureMode::NestedGrouped => {
126                    // -> [output-directory]/[template-group]/[author-title]
127                    PathBuf::from(&template.group).join(&names.directory)
128                }
129            };
130
131            match template.context_mode {
132                ContextMode::Book => {
133                    renders.push(self.render_book(template, &entry, &names, &path)?);
134                }
135                ContextMode::Annotation => {
136                    renders.extend(self.render_annotations(template, &entry, &names, &path)?);
137                }
138            }
139        }
140
141        self.renders.extend(renders);
142
143        Ok(())
144    }
145
146    /// Iterates through all [`Render`]s and writes them to disk.
147    ///
148    /// # Arguments
149    ///
150    /// * `path` - The path to the write the rendered templates to. Each rendered template's path is
151    ///   appened to this path to determine its full path.
152    ///
153    /// # Errors
154    ///
155    /// Will return `Err` if any IO errors are encountered.
156    pub fn write(&self, path: &Path) -> Result<()> {
157        for render in &self.renders {
158            // -> [ouput-directory]/[template-subdirectory]
159            let root = path.join(&render.path);
160
161            std::fs::create_dir_all(&root)?;
162
163            // -> [ouput-directory]/[template-subdirectory]/[template-filename]
164            let file = root.join(&render.filename);
165
166            if !self.options.overwrite_existing && file.exists() {
167                log::debug!("skipped writing {}", file.display());
168            } else {
169                let mut file = File::create(file)?;
170                write!(file, "{}", &render.contents)?;
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Returns an iterator over all [`Render`]s.
178    pub fn templates_rendered(&self) -> impl Iterator<Item = &Render> {
179        self.renders.iter()
180    }
181
182    /// Returns a mutable iterator over all [`Render`]s.
183    pub fn templates_rendered_mut(&mut self) -> impl Iterator<Item = &mut Render> {
184        self.renders.iter_mut()
185    }
186
187    /// Returns the number of [`Template`]s.
188    #[must_use]
189    pub fn count_templates(&self) -> usize {
190        self.templates.len()
191    }
192
193    /// Returns the number of [`Render`]s.
194    #[must_use]
195    pub fn count_templates_rendered(&self) -> usize {
196        self.renders.len()
197    }
198
199    /// Validates that all requested template-groups exist.
200    ///
201    /// # Errors
202    ///
203    /// Will return `Err` if a requested template-group does not exist.
204    fn validate_requested_template_groups(&self) -> Result<()> {
205        if self.options.template_groups.is_empty() {
206            return Ok(());
207        }
208
209        let available_template_groups: HashSet<&str> = self
210            .templates
211            .iter()
212            .map(|template| template.group.as_str())
213            .collect();
214
215        for template_group in &self.options.template_groups {
216            if !available_template_groups.contains(template_group.as_str()) {
217                return Err(Error::NonexistentTemplateGroup {
218                    name: template_group.to_string(),
219                });
220            }
221        }
222
223        Ok(())
224    }
225
226    /// Returns an iterator over all the requested templates.
227    fn iter_requested_templates(&self) -> impl Iterator<Item = &Template> {
228        let templates: Vec<&Template> = self.templates.iter().collect();
229
230        if self.options.template_groups.is_empty() {
231            return templates.into_iter();
232        }
233
234        templates
235            .into_iter()
236            .filter(|template| self.options.template_groups.contains(&template.group))
237            .collect::<Vec<_>>()
238            .into_iter()
239    }
240
241    /// Builds and registers [`Template`]s from a directory containing user-generated templates.
242    ///
243    /// # Arguments
244    ///
245    /// * `path` - A path to a directory containing user-generated templates.
246    ///
247    /// # Errors
248    ///
249    /// Will return `Err` if:
250    /// * A template contains either syntax errors or contains variables that reference non-existent
251    ///   fields in a [`Book`][book]/[`Annotation`][annotation].
252    /// * A template's config block isn't formatted correctly, has syntax errors or is missing
253    ///   required fields.
254    /// * Any IO errors are encountered.
255    ///
256    /// [book]: crate::models::book::Book
257    /// [annotation]: crate::models::annotation::Annotation
258    fn build_from_directory(&mut self, path: &Path) -> Result<()> {
259        // When a normal template is registered, it's validated to make sure it contains no syntax
260        // errors or variables that reference non-existent fields. Partial templates however are
261        // registered without directly being validation as their validation happens when a normal
262        // template includes them. Therefore it's important that partial templates are registered
263        // before normal ones.
264
265        for item in Self::iter_templates_directory(&path, TemplateKind::Partial) {
266            // Returns the path to the template relative to the root templates directory.
267            //
268            // --> /path/to/templates/
269            // --> /path/to/templates/nested/template.md
270            // -->                    nested/template.md
271            //
272            // This is used to uniquely identify each template.
273            //
274            // This unwrap is safe seeing as both `item` and `path` should both be absolute paths.
275            let path = pathdiff::diff_paths(&item, path).unwrap();
276
277            let template = std::fs::read_to_string(&item)?;
278            let template = TemplatePartial::new(&path, &template);
279
280            self.engine
281                .register_template(&template.id, &template.contents)?;
282
283            self.templates_partial.push(template);
284
285            log::debug!("added partial template: {}", path.display());
286        }
287
288        for item in Self::iter_templates_directory(&path, TemplateKind::Normal) {
289            // See above.
290            //
291            // This unwrap is safe seeing as both `item` and `path` should both be absolute paths.
292            let path = pathdiff::diff_paths(&item, path).unwrap();
293
294            let template = std::fs::read_to_string(&item)?;
295            let template = Template::new(&path, &template)?;
296
297            self.engine
298                .register_template(&template.id, &template.contents)?;
299
300            // Templates are validated *after* being registered. The registry handles building
301            // template inheritances. We need to register the templates before validating them so
302            // ensure that any partial templates they reference are properly resolved.
303            self.validate_template(&template)?;
304
305            self.templates.push(template);
306
307            log::debug!("added template: {}", path.display());
308        }
309
310        log::debug!("registed partial templates: {:#?}", self.templates_partial);
311        log::debug!("registed templates: {:#?}", self.templates);
312
313        log::debug!(
314            "built {} template(s) and {} partial template(s) from {}",
315            self.templates.len(),
316            self.templates_partial.len(),
317            path.display()
318        );
319
320        Ok(())
321    }
322
323    /// Builds and registers the default [`Template`].
324    fn build_default(&mut self) -> Result<()> {
325        let template = Template::new("__default", &self.template_default)?;
326
327        self.engine
328            .register_template(&template.id, &template.contents)?;
329
330        self.templates.push(template);
331
332        log::debug!("built the default template");
333
334        Ok(())
335    }
336
337    /// Validates that a template does not contain variables that reference non-existent fields in
338    /// an [`Entry`], [`Book`][book], [`Annotation`][annotation] and [`NamesRender`].
339    ///
340    /// # Arguments
341    ///
342    /// * `template` - The template to validate.
343    ///
344    /// # Errors
345    ///
346    /// Will return `Err` if the template contains variables that reference non-existent fields in
347    /// an [`Entry`]/[`Book`][book]/[`Annotation`][annotation].
348    ///
349    /// [book]: crate::models::book::Book
350    /// [annotation]: crate::models::annotation::Annotation
351    fn validate_template(&mut self, template: &Template) -> Result<()> {
352        let entry = Entry::dummy();
353        let entry = EntryContext::from(&entry);
354        let names = NamesRender::new(&entry, template)?;
355
356        match template.context_mode {
357            ContextMode::Book => {
358                let context = TemplateContext::book(&entry.book, &entry.annotations, &names);
359
360                self.engine.render(&template.id, context)?;
361            }
362            ContextMode::Annotation => {
363                // This should be safe as a dummy `Entry` contains three annotations.
364                let annotation = &entry.annotations[0];
365                let context = TemplateContext::annotation(&entry.book, annotation, &names);
366
367                self.engine.render(&template.id, context)?;
368            }
369        };
370
371        Ok(())
372    }
373
374    /// Renders an [`Entry`]'s [`Book`][book] to a single [`Render`].
375    ///
376    /// # Arguments
377    ///
378    /// * `template` - The template to render.
379    /// * `entry` - The context to inject into the template.
380    /// * `names` - The names to inject into the template context.
381    /// * `path` - The path to where the template will be written to. This path should be relative
382    ///   to the final output directory.
383    ///
384    /// # Errors
385    ///
386    /// Will return `Err` if the template renderer encounters an error.
387    ///
388    /// [book]: crate::models::book::Book
389    fn render_book(
390        &self,
391        template: &Template,
392        entry: &EntryContext<'_>,
393        names: &NamesRender,
394        path: &Path,
395    ) -> Result<Render> {
396        let filename = names.book.clone();
397        let context = TemplateContext::book(&entry.book, &entry.annotations, names);
398        let string = self.engine.render(&template.id, context)?;
399        let render = Render::new(path.to_owned(), filename, string);
400
401        Ok(render)
402    }
403
404    /// Renders an [`Entry`]'s [`Annotation`][annotation]s to multiple [`Render`]s.
405    ///
406    /// # Arguments
407    ///
408    /// * `template` - The template to render.
409    /// * `entry` - The context to inject into the template.
410    /// * `names` - The names to inject into the template context.
411    /// * `path` - The path to where the template will be written to. This path should be relative
412    ///   to the final output directory.
413    ///
414    /// # Errors
415    ///
416    /// Will return `Err` if the template renderer encounters an error.
417    ///
418    /// [annotation]: crate::models::annotation::Annotation
419    fn render_annotations(
420        &self,
421        template: &Template,
422        entry: &EntryContext<'_>,
423        names: &NamesRender,
424        path: &Path,
425    ) -> Result<Vec<Render>> {
426        let mut renders = Vec::with_capacity(entry.annotations.len());
427
428        for annotation in &entry.annotations {
429            let filename = names.get_annotation_filename(&annotation.metadata.id);
430            let context = TemplateContext::annotation(&entry.book, annotation, names);
431            let string = self.engine.render(&template.id, context)?;
432            let render = Render::new(path.to_owned(), filename, string);
433
434            renders.push(render);
435        }
436
437        Ok(renders)
438    }
439
440    /// Returns an iterator over all template-like files in a directory.
441    ///
442    /// # Arguments
443    ///
444    /// * `path` - The path to to iterate.
445    /// * `kind` - The kind of template the iterator should return.
446    fn iter_templates_directory<P>(path: P, kind: TemplateKind) -> impl Iterator<Item = PathBuf>
447    where
448        P: AsRef<Path>,
449    {
450        let template_filter: fn(&DirEntry) -> bool = match kind {
451            TemplateKind::Normal => utils::is_normal_template,
452            TemplateKind::Partial => utils::is_partial_template,
453        };
454
455        // Avoids traversing hidden directories, ignores `.hidden` files, returns non-directory
456        // entries and filters the them by whether are normal or partial tempaltes.
457        walkdir::WalkDir::new(path)
458            .into_iter()
459            .filter_entry(utils::is_hidden)
460            .filter_map(std::result::Result::ok)
461            .filter(|e| !e.path().is_dir())
462            .filter(template_filter)
463            .map(|e| e.path().to_owned())
464    }
465}
466
467/// A struct representing options for the [`Renderer`] struct.
468#[derive(Debug, Default)]
469pub struct RenderOptions {
470    /// A path to a directory containing user-generated templates.
471    pub templates_directory: Option<PathBuf>,
472
473    /// A list of template-groups to render. All template-groups are rendered if none are specified.
474    ///
475    /// These are considered 'requested' template-groups. If they exist, their respective templates
476    /// are considered 'requested' templates and are set to be rendered.
477    pub template_groups: Vec<String>,
478
479    /// Toggles whether or not to overwrite existing files.
480    pub overwrite_existing: bool,
481}
482
483/// An enum representing the two different template types.
484#[derive(Debug, Clone, Copy)]
485enum TemplateKind {
486    /// A [`Template`]. Requires a configuration block and should not start with an underscore.
487    Normal,
488
489    /// A [`TemplatePartial`]. Must start with an underscore `_` but does not require a configuration block.
490    Partial,
491}
492
493/// An enum representing all possible template contexts.
494///
495/// This primarily used to shuffle data to fit a certain shape before it's injected into a template.
496#[derive(Debug, Serialize)]
497#[serde(untagged)]
498enum TemplateContext<'a> {
499    /// Used when rendering both a [`Book`][book] and its [`Annotation`][annotation]s in a template.
500    /// Includes all the output filenames and the nested directory name.
501    ///
502    /// [book]: crate::models::book::Book
503    /// [annotation]: crate::models::annotation::Annotation
504    Book {
505        book: &'a BookContext<'a>,
506        annotations: &'a [AnnotationContext<'a>],
507        names: &'a NamesRender,
508    },
509    /// Used when rendering a single [`Annotation`][annotation] in a template. Includes all the
510    /// output filenames and the nested directory name.
511    ///
512    /// [annotation]: crate::models::annotation::Annotation
513    Annotation {
514        book: &'a BookContext<'a>,
515        annotation: &'a AnnotationContext<'a>,
516        names: &'a NamesRender,
517    },
518}
519
520impl<'a> TemplateContext<'a> {
521    fn book(
522        book: &'a BookContext<'a>,
523        annotations: &'a [AnnotationContext<'a>],
524        names: &'a NamesRender,
525    ) -> Self {
526        Self::Book {
527            book,
528            annotations,
529            names,
530        }
531    }
532
533    fn annotation(
534        book: &'a BookContext<'a>,
535        annotation: &'a AnnotationContext<'a>,
536        names: &'a NamesRender,
537    ) -> Self {
538        Self::Annotation {
539            book,
540            annotation,
541            names,
542        }
543    }
544}
545
546#[cfg(test)]
547mod test {
548
549    use super::*;
550
551    use crate::defaults::test::TemplatesDirectory;
552    use crate::result::Error;
553    use crate::utils;
554
555    // Validates that a template does not contain variables that reference non-existent fields.
556    fn validate_template_context(template: &str) -> Result<()> {
557        let template = Template::new("validate_template_context", template).unwrap();
558
559        let mut renderer = Renderer::default();
560
561        renderer
562            .engine
563            .register_template(&template.id, &template.contents)
564            .unwrap();
565
566        renderer.validate_template(&template)
567    }
568
569    // Validates that a template does not contain syntax errors.
570    fn validate_template_syntax(template: &str) -> Result<()> {
571        let template = Template::new("validate_template_syntax", template).unwrap();
572
573        let mut renderer = Renderer::default();
574
575        renderer
576            .engine
577            .register_template(&template.id, &template.contents)?;
578
579        Ok(())
580    }
581
582    mod invalid_context {
583
584        use super::*;
585
586        // Tests that an invalid object (`invalid.[attribute]`) returns an error.
587        #[test]
588        fn invalid_object() {
589            let template = utils::testing::load_template_str(
590                TemplatesDirectory::InvalidContext,
591                "invalid-object.txt",
592            );
593            let result = validate_template_context(&template);
594
595            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
596        }
597
598        // Tests that an invalid attribute (`[object].invalid`) returns an error.
599        #[test]
600        fn invalid_attribute() {
601            let template = utils::testing::load_template_str(
602                TemplatesDirectory::InvalidContext,
603                "invalid-attribute.txt",
604            );
605            let result = validate_template_context(&template);
606
607            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
608        }
609
610        // Tests that an invalid annotation attribute within a `book` context returns an error.
611        #[test]
612        fn invalid_book_annotations() {
613            let template = utils::testing::load_template_str(
614                TemplatesDirectory::InvalidContext,
615                "invalid-book-annotations.txt",
616            );
617            let result = validate_template_context(&template);
618
619            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
620        }
621
622        // Tests that an invalid names attribute within a `book` context returns an error.
623        #[test]
624        fn invalid_book_names() {
625            let template = utils::testing::load_template_str(
626                TemplatesDirectory::InvalidContext,
627                "invalid-book-names.txt",
628            );
629            let result = validate_template_context(&template);
630
631            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
632        }
633
634        // Tests that an invalid names attribute within an `annotation` context returns an error.
635        #[test]
636        fn invalid_annotation_names() {
637            let template = utils::testing::load_template_str(
638                TemplatesDirectory::InvalidContext,
639                "invalid-annotation-names.txt",
640            );
641            let result = validate_template_context(&template);
642
643            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
644        }
645    }
646
647    mod valid_context {
648
649        use super::*;
650
651        // Tests that all `Book` fields are valid.
652        #[test]
653        fn valid_book() {
654            let template = utils::testing::load_template_str(
655                TemplatesDirectory::ValidContext,
656                "valid-book.txt",
657            );
658            let result = validate_template_syntax(&template);
659
660            assert!(result.is_ok());
661        }
662
663        // Tests that all `Annotation` fields are valid.
664        #[test]
665        fn valid_annotation() {
666            let template = utils::testing::load_template_str(
667                TemplatesDirectory::ValidContext,
668                "valid-annotation.txt",
669            );
670            let result = validate_template_syntax(&template);
671
672            assert!(result.is_ok());
673        }
674    }
675
676    mod invalid_syntax {
677
678        use super::*;
679
680        // Tests that invalid syntax returns an error.
681        #[test]
682        fn invalid_syntax() {
683            let template = utils::testing::load_template_str(
684                TemplatesDirectory::InvalidSyntax,
685                "invalid-syntax.txt",
686            );
687            let result = validate_template_syntax(&template);
688
689            assert!(matches!(result, Err(Error::InvalidTemplate(_))));
690        }
691    }
692
693    mod valid_syntax {
694
695        use super::*;
696
697        // Tests that valid syntax returns no errors.
698        #[test]
699        fn valid_syntax() {
700            let template = utils::testing::load_template_str(
701                TemplatesDirectory::ValidSyntax,
702                "valid-syntax.txt",
703            );
704            let result = validate_template_syntax(&template);
705
706            assert!(result.is_ok());
707        }
708    }
709
710    mod example_templates {
711
712        use super::*;
713
714        // Tests that all example templates return no errors.
715        #[test]
716        fn example_templates() {
717            let mut renderer = Renderer::default();
718
719            renderer
720                .build_from_directory(&crate::defaults::test::EXAMPLE_TEMPLATES)
721                .unwrap();
722        }
723    }
724}