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}