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}