lib/render/
names.rs

1//! Defines types to represent the output file/directory names of rendered
2//! templates.
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::contexts::annotation::AnnotationContext;
9use crate::contexts::book::BookContext;
10use crate::contexts::entry::EntryContext;
11use crate::models::datetime::DateTimeUtc;
12use crate::render::template::Template;
13use crate::result::Result;
14use crate::strings;
15use crate::utils;
16
17/// A struct representing the raw template strings for generating output file and directory names.
18#[derive(Debug, Clone, Deserialize)]
19pub struct Names {
20    /// The default template used when generating an output filename for the template when its
21    /// context mode is [`ContextMode::Book`][book].
22    ///
23    /// [book]: crate::render::template::ContextMode::Book
24    #[serde(default = "Names::default_book")]
25    pub book: String,
26
27    /// The default template used when generating an output filename for the template when its
28    /// context mode is [`ContextMode::Annotation`][annotation].
29    ///
30    /// [annotation]: crate::render::template::ContextMode::Annotation
31    #[serde(default = "Names::default_annotation")]
32    pub annotation: String,
33
34    /// The default template used when generating a nested output directory for the
35    /// template when its structure mode is either [`StructureMode::Nested`][nested] or
36    /// [`StructureMode::NestedGrouped`][nested-grouped].
37    ///
38    /// [nested]: crate::render::template::StructureMode::Nested
39    /// [nested-grouped]: crate::render::template::StructureMode::NestedGrouped
40    #[serde(default = "Names::default_directory")]
41    pub directory: String,
42}
43
44impl Default for Names {
45    fn default() -> Self {
46        Self {
47            book: Self::default_book(),
48            annotation: Self::default_annotation(),
49            directory: Self::default_directory(),
50        }
51    }
52}
53
54impl Names {
55    /// Returns the default template for a book's filename.
56    fn default_book() -> String {
57        super::defaults::FILENAME_TEMPLATE_BOOK.to_owned()
58    }
59
60    /// Returns the default template for an annotation's filename.
61    fn default_annotation() -> String {
62        super::defaults::FILENAME_TEMPLATE_ANNOTATION.to_owned()
63    }
64
65    /// Returns the default template for a directory.
66    fn default_directory() -> String {
67        super::defaults::DIRECTORY_TEMPLATE.to_owned()
68    }
69}
70
71/// A struct representing the rendered template strings for all the output file and directory names
72/// for a given template.
73///
74/// This is used to (1) name files and directories when rendering templates to disk and (2) is
75/// included in the template's context so that files/direcories related to the template can be
76/// references within the tenplate.
77///
78/// See [`Renderer::render()`][renderer] for more information.
79///
80/// [renderer]: crate::render::renderer::Renderer::render()
81#[derive(Debug, Default, Clone, Serialize)]
82pub struct NamesRender {
83    /// The output filename for a template with [`ContextMode::Book`][book].
84    ///
85    /// [book]: crate::render::template::ContextMode::Book
86    pub book: String,
87
88    /// The output filenames for a template with [`ContextMode::Annotation`][annotation].
89    ///
90    /// Internally this field is stored as a `HashMap` but is converted into a `Vec` before it's
91    /// injected into a template.
92    ///
93    /// [annotation]: crate::render::template::ContextMode::Annotation
94    #[serde(serialize_with = "utils::serialize_hashmap_to_vec")]
95    pub annotations: HashMap<String, AnnotationNameAttributes>,
96
97    /// The directory name for a template with [`StructureMode::Nested`][nested] or
98    /// [`StructureMode::NestedGrouped`][nested-grouped].
99    ///
100    /// [nested]: crate::render::template::StructureMode::Nested
101    /// [nested-grouped]: crate::render::template::StructureMode::NestedGrouped
102    pub directory: String,
103}
104
105impl NamesRender {
106    /// Creates a new instance of [`NamesRender`].
107    ///
108    /// Note that all names are generated regardless of the template's [`ContextMode`][context-mode].
109    /// For example, when a separate template is used to render a [`Book`][book] and another for its
110    /// [`Annotation`][annotation]s, it's important that both templates have access to the other's
111    /// filenames so they can link to one another if the user desires.
112    ///
113    /// # Arguments
114    ///
115    /// * `entry` - The context injected into the filename templates.
116    /// * `template` - The template containing the filename templates.
117    ///
118    /// # Errors
119    ///
120    /// Will return `Err` if any templates have syntax errors or are referencing non-existent fields
121    /// in their respective contexts.
122    ///
123    /// [annotation]: crate::models::annotation::Annotation
124    /// [book]: crate::models::book::Book
125    /// [context-mode]: crate::render::template::ContextMode
126    pub fn new(entry: &EntryContext<'_>, template: &Template) -> Result<Self> {
127        Ok(Self {
128            book: Self::render_book_filename(entry, template)?,
129            annotations: Self::render_annotation_filenames(entry, template)?,
130            directory: Self::render_directory_name(entry, template)?,
131        })
132    }
133
134    /// Returns the rendered annotation filename based on its id.
135    ///
136    /// # Arguments
137    ///
138    /// * `annotation_id` - The annotation's id.
139    #[must_use]
140    #[allow(clippy::missing_panics_doc)]
141    pub fn get_annotation_filename(&self, annotation_id: &str) -> String {
142        self.annotations
143            .get(annotation_id)
144            // This should theoretically never fail as the `NamesRender` instance is created from
145            // the `Entry`. This means they contain the same exact keys and it should therefore be
146            // safe to unwrap. An error here would be critical and should fail.
147            .expect("`NamesRender` instance missing `Annotation` present in `Entry`")
148            .filename
149            .clone()
150    }
151
152    /// Renders the filename for a template with [`ContextMode::Book`][context-mode].
153    ///
154    /// # Arguments
155    ///
156    /// * `entry` - The context to inject into the template.
157    /// * `template` - The template to render.
158    ///
159    /// [context-mode]: crate::render::template::ContextMode::Book
160    fn render_book_filename(entry: &EntryContext<'_>, template: &Template) -> Result<String> {
161        let context = NamesContext::book(&entry.book, &entry.annotations);
162
163        let filename = strings::render_and_sanitize(&template.names.book, context)?;
164        let filename = strings::build_filename_and_sanitize(&filename, &template.extension);
165
166        Ok(filename)
167    }
168
169    /// Renders the filename for a template with [`ContextMode::Annotation`][context-mode].
170    ///
171    /// # Arguments
172    ///
173    /// * `entry` - The context to inject into the template.
174    /// * `template` - The template to render.
175    ///
176    /// [context-mode]: crate::render::template::ContextMode::Annotation
177    fn render_annotation_filenames(
178        entry: &EntryContext<'_>,
179        template: &Template,
180    ) -> Result<HashMap<String, AnnotationNameAttributes>> {
181        let mut annotations = HashMap::new();
182
183        for annotation in &entry.annotations {
184            let context = NamesContext::annotation(&entry.book, annotation);
185
186            let filename = strings::render_and_sanitize(&template.names.annotation, context)?;
187            let filename = strings::build_filename_and_sanitize(&filename, &template.extension);
188
189            annotations.insert(
190                annotation.metadata.id.clone(),
191                AnnotationNameAttributes::new(annotation, filename),
192            );
193        }
194
195        Ok(annotations)
196    }
197
198    /// Renders the directory name for a template with [`StructureMode::Nested`][nested] or
199    /// [`StructureMode::NestedGouped`][nested-grouped].
200    ///
201    /// # Arguments
202    ///
203    /// * `entry` - The context to inject into the template.
204    /// * `template` - The template to render.
205    ///
206    /// [nested]: crate::render::template::StructureMode::Nested
207    /// [nested-grouped]: crate::render::template::StructureMode::NestedGrouped
208    fn render_directory_name(entry: &EntryContext<'_>, template: &Template) -> Result<String> {
209        let context = NamesContext::directory(&entry.book);
210
211        strings::render_and_sanitize(&template.names.directory, context)
212    }
213}
214
215/// A struct representing the rendered filename for a template with
216/// [`ContextMode::Annotation`][context-mode] along with a set of attributes used for sorting within
217/// a template.
218///
219/// For example:
220///
221/// ```jinja
222/// {% for name in names.annotations | sort(attribute="location") -%}
223/// ![[{{ name.filename }}]]
224/// {% endfor %}
225/// ```
226/// See [`AnnotationMetadata`][annotation-metadata] for undocumented fields.
227///
228/// [annotation-metadata]: crate::models::annotation::AnnotationMetadata
229/// [context-mode]: crate::render::template::ContextMode::Annotation
230#[derive(Debug, Default, Clone, Serialize)]
231pub struct AnnotationNameAttributes {
232    /// The rendered filename for a template with
233    /// [`ContextMode::Annotation`][context-mode].
234    ///
235    /// [context-mode]: crate::render::template::ContextMode
236    pub filename: String,
237    #[allow(missing_docs)]
238    pub created: DateTimeUtc,
239    #[allow(missing_docs)]
240    pub modified: DateTimeUtc,
241    #[allow(missing_docs)]
242    pub location: String,
243}
244
245impl AnnotationNameAttributes {
246    /// Creates a new instance of [`AnnotationNameAttributes`].
247    fn new(annotation: &AnnotationContext<'_>, filename: String) -> Self {
248        Self {
249            filename,
250            created: annotation.metadata.created,
251            modified: annotation.metadata.modified,
252            location: annotation.metadata.location.clone(),
253        }
254    }
255}
256
257/// An enum representing the different template contexts for rendering file and directory names.
258#[derive(Debug, Serialize)]
259#[serde(untagged)]
260enum NamesContext<'a> {
261    /// The context when rendering a filename for a template with [`ContextMode::Book`][context-mode].
262    ///
263    /// [context-mode]: crate::render::template::ContextMode::Book
264    Book {
265        book: &'a BookContext<'a>,
266        annotations: &'a [AnnotationContext<'a>],
267    },
268    /// The context when rendering a filename for a template with [`ContextMode::Annotation`][context-mode].
269    ///
270    /// [context-mode]: crate::render::template::ContextMode::Annotation
271    Annotation {
272        book: &'a BookContext<'a>,
273        annotation: &'a AnnotationContext<'a>,
274    },
275    /// The context when rendering the directory name for a template with
276    /// [`StructureMode::Nested`][nested] or [`StructureMode::NestedGouped`][nested-grouped].
277    ///
278    /// [nested]: crate::render::template::StructureMode::Nested
279    /// [nested-grouped]: crate::render::template::StructureMode::NestedGrouped
280    Directory { book: &'a BookContext<'a> },
281}
282
283impl<'a> NamesContext<'a> {
284    fn book(book: &'a BookContext<'a>, annotations: &'a [AnnotationContext<'a>]) -> Self {
285        Self::Book { book, annotations }
286    }
287
288    fn annotation(book: &'a BookContext<'a>, annotation: &'a AnnotationContext<'a>) -> Self {
289        Self::Annotation { book, annotation }
290    }
291
292    fn directory(book: &'a BookContext<'a>) -> Self {
293        Self::Directory { book }
294    }
295}