Skip to main content

webfont_generator/rendering/
result.rs

1#[cfg(test)]
2mod tests;
3
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7use serde_json::{Map, Value};
8
9use super::css::{
10    SharedTemplateData, TemplateDependencies, build_css_context, make_src,
11    render_css_with_hbs_context, render_css_with_src_mutate,
12};
13use super::files::load_html_template;
14use super::html::{
15    build_html_context, render_default_html_with_styles, render_html_with_hbs_context,
16};
17use super::shared::{render_with_field_swap, to_io_err};
18use crate::result::GenerateWebfontsResult;
19use crate::types::FontType;
20
21/// Caches the last rendered CSS/HTML result for repeated calls with the same urls. Cloneable so
22/// an incremental `regenerate` can carry the still-valid entries (provided-URL renders, which
23/// don't depend on the font hash) forward into the rebuilt template data.
24#[derive(Clone, Default)]
25pub(crate) struct RenderCache {
26    /// Result of generateCss() with no urls (computed once).
27    css_no_urls: Option<Arc<String>>,
28    /// Last generateCss(urls) result.
29    css_last_urls: Option<HashMap<FontType, String>>,
30    css_last_result: Option<Arc<String>>,
31    /// Result of generateHtml() with no urls (computed once).
32    html_no_urls: Option<Arc<String>>,
33    /// Last generateHtml(urls) result.
34    html_last_urls: Option<HashMap<FontType, String>>,
35    html_last_result: Option<Arc<String>>,
36}
37
38#[derive(Clone)]
39pub(crate) struct CarriedRenderCache {
40    cache: RenderCache,
41    css_dependencies: TemplateDependencies,
42    html_dependencies: TemplateDependencies,
43    css_template_source: Option<Arc<str>>,
44    html_template_source: Option<Arc<str>>,
45}
46
47pub(crate) struct CachedTemplateData {
48    pub(crate) shared: SharedTemplateData,
49    pub(crate) css_context: Map<String, Value>,
50    pub(crate) css_hbs_context: Mutex<handlebars::Context>,
51    pub(crate) html_context: Map<String, Value>,
52    pub(crate) html_hbs_context: Mutex<handlebars::Context>,
53    pub(crate) html_template: super::html::HtmlTemplate,
54    pub(crate) render_cache: Mutex<RenderCache>,
55    #[cfg(test)]
56    pub(crate) html_render_count: std::sync::atomic::AtomicUsize,
57}
58
59impl GenerateWebfontsResult {
60    fn validate_render_urls(
61        &self,
62        urls: Option<&HashMap<FontType, String>>,
63    ) -> std::io::Result<()> {
64        if self.options.variants.is_some()
65            && urls.is_some_and(|urls| {
66                urls.contains_key(&FontType::Svg) || urls.contains_key(&FontType::Eot)
67            })
68        {
69            return Err(std::io::Error::new(
70                std::io::ErrorKind::InvalidInput,
71                "SVG and EOT URLs are unsupported for variant results.",
72            ));
73        }
74        Ok(())
75    }
76    #[cfg(test)]
77    pub(crate) fn has_carried_css_no_urls_for_test(&self) -> bool {
78        self.carried_render
79            .as_ref()
80            .is_some_and(|carried| carried.cache.css_no_urls.is_some())
81    }
82
83    #[cfg(test)]
84    pub(crate) fn has_carried_html_no_urls_for_test(&self) -> bool {
85        self.carried_render
86            .as_ref()
87            .is_some_and(|carried| carried.cache.html_no_urls.is_some())
88    }
89
90    pub(crate) fn get_cached_io(&self) -> std::io::Result<&CachedTemplateData> {
91        self.cached
92            .get_or_init(|| {
93                let shared = SharedTemplateData::from_files(&self.options, &self.source_files)
94                    .map_err(|e| e.to_string())?;
95                let css_context = match &self.css_context {
96                    Some(ctx) => ctx.clone(),
97                    None => build_css_context(&self.options, &shared),
98                };
99                let html_context = match &self.html_context {
100                    Some(ctx) => ctx.clone(),
101                    None => build_html_context(&self.options, &shared, &self.source_files, None)
102                        .map_err(|e| e.to_string())?,
103                };
104                let html_template = load_html_template(&self.options).map_err(|e| e.to_string())?;
105                let render_cache = self
106                    .carried_render
107                    .as_ref()
108                    .map(|carried| {
109                        if carried.css_template_source != shared.css_template_source {
110                            // HTML may embed CSS, so changing CSS invalidates both companions.
111                            RenderCache::default()
112                        } else if carried.html_template_source != html_template.source {
113                            RenderCache {
114                                html_no_urls: None,
115                                html_last_urls: None,
116                                html_last_result: None,
117                                ..carried.cache.clone()
118                            }
119                        } else {
120                            carried.cache.clone()
121                        }
122                    })
123                    .unwrap_or_default();
124                let css_hbs_context =
125                    handlebars::Context::wraps(&css_context).map_err(|e| e.to_string())?;
126                let html_hbs_context =
127                    handlebars::Context::wraps(&html_context).map_err(|e| e.to_string())?;
128                Ok(CachedTemplateData {
129                    #[cfg(test)]
130                    html_render_count: Default::default(),
131                    shared,
132                    css_context,
133                    css_hbs_context: Mutex::new(css_hbs_context),
134                    html_context,
135                    html_hbs_context: Mutex::new(html_hbs_context),
136                    html_template,
137                    // Seed with entries carried across a regenerate;
138                    // these are renders that don't depend on what changed, so reusing them is safe.
139                    render_cache: Mutex::new(render_cache),
140                })
141            })
142            .as_ref()
143            .map_err(to_io_err)
144    }
145
146    pub(crate) fn reusable_render_cache(
147        &self,
148        names_unchanged: bool,
149        codepoints_unchanged: bool,
150    ) -> Option<CarriedRenderCache> {
151        self.render_cache_source().map(|carried| {
152            let css_deps = carried.css_dependencies;
153            let css_no_urls_unchanged =
154                css_deps.can_reuse_css_no_urls(names_unchanged, codepoints_unchanged);
155            let css_with_urls_unchanged =
156                css_deps.can_reuse_css_with_urls(names_unchanged, codepoints_unchanged);
157            let html_no_urls_unchanged = carried.html_dependencies.can_reuse_html(
158                names_unchanged,
159                codepoints_unchanged,
160                css_no_urls_unchanged,
161            );
162            let html_with_urls_unchanged = carried.html_dependencies.can_reuse_html(
163                names_unchanged,
164                codepoints_unchanged,
165                css_with_urls_unchanged,
166            );
167
168            let rc = carried.cache;
169            CarriedRenderCache {
170                css_dependencies: carried.css_dependencies,
171                html_dependencies: carried.html_dependencies,
172                css_template_source: carried.css_template_source,
173                html_template_source: carried.html_template_source,
174                cache: RenderCache {
175                    css_no_urls: css_no_urls_unchanged
176                        .then(|| rc.css_no_urls.clone())
177                        .flatten(),
178                    html_no_urls: html_no_urls_unchanged
179                        .then(|| rc.html_no_urls.clone())
180                        .flatten(),
181                    css_last_urls: css_with_urls_unchanged
182                        .then(|| rc.css_last_urls.clone())
183                        .flatten(),
184                    css_last_result: css_with_urls_unchanged
185                        .then(|| rc.css_last_result.clone())
186                        .flatten(),
187                    html_last_urls: html_with_urls_unchanged
188                        .then(|| rc.html_last_urls.clone())
189                        .flatten(),
190                    html_last_result: html_with_urls_unchanged
191                        .then(|| rc.html_last_result.clone())
192                        .flatten(),
193                },
194            }
195        })
196    }
197
198    pub(crate) fn render_cache_source(&self) -> Option<CarriedRenderCache> {
199        self.cached
200            .get()
201            .and_then(|cached| cached.as_ref().ok())
202            .map(|cached| CarriedRenderCache {
203                cache: cached.render_cache.lock().unwrap().clone(),
204                css_dependencies: cached.shared.css_template_dependencies,
205                html_dependencies: cached.html_template.dependencies,
206                css_template_source: cached.shared.css_template_source.clone(),
207                html_template_source: cached.html_template.source.clone(),
208            })
209            .or_else(|| self.carried_render.clone())
210    }
211
212    /// Generate a CSS string for this webfont result.
213    ///
214    /// Pass `urls` to override the default font URLs in the CSS output.
215    pub fn generate_css_pure(
216        &self,
217        urls: Option<HashMap<FontType, String>>,
218    ) -> std::io::Result<String> {
219        self.generate_css_shared(urls)
220            .map(|text| text.as_ref().clone())
221    }
222
223    pub(crate) fn generate_css_shared(
224        &self,
225        urls: Option<HashMap<FontType, String>>,
226    ) -> std::io::Result<Arc<String>> {
227        self.validate_render_urls(urls.as_ref())?;
228        let cached = self.get_cached_io()?;
229        let mut rc = cached.render_cache.lock().unwrap();
230
231        match &urls {
232            None => {
233                if let Some(result) = &rc.css_no_urls {
234                    return Ok(result.clone());
235                }
236                let ctx = cached.css_hbs_context.lock().unwrap();
237                let result = Arc::new(render_css_with_hbs_context(
238                    &cached.shared,
239                    &ctx,
240                    &cached.css_context,
241                )?);
242                rc.css_no_urls = Some(result.clone());
243                Ok(result)
244            }
245            Some(urls) => {
246                // If the template doesn't reference {{src}}, URLs don't affect output
247                if !cached.shared.css_template_uses_src {
248                    drop(rc);
249                    return self.generate_css_shared(None);
250                }
251                if rc.css_last_urls.as_ref() == Some(urls)
252                    && let Some(result) = &rc.css_last_result
253                {
254                    return Ok(result.clone());
255                }
256                let src = make_src(&self.options, urls);
257                let mut ctx = cached.css_hbs_context.lock().unwrap();
258                let result = Arc::new(render_css_with_src_mutate(
259                    &cached.shared,
260                    &mut ctx,
261                    &cached.css_context,
262                    &src,
263                )?);
264                rc.css_last_urls = Some(urls.clone());
265                rc.css_last_result = Some(result.clone());
266                Ok(result)
267            }
268        }
269    }
270
271    /// Generate an HTML string for this webfont result.
272    ///
273    /// Pass `urls` to override the default font URLs in the HTML output.
274    pub fn generate_html_pure(
275        &self,
276        urls: Option<HashMap<FontType, String>>,
277    ) -> std::io::Result<String> {
278        self.generate_html_shared(urls)
279            .map(|text| text.as_ref().clone())
280    }
281
282    pub(crate) fn generate_html_shared(
283        &self,
284        urls: Option<HashMap<FontType, String>>,
285    ) -> std::io::Result<Arc<String>> {
286        self.validate_render_urls(urls.as_ref())?;
287        let cached = self.get_cached_io()?;
288        let mut rc = cached.render_cache.lock().unwrap();
289
290        match &urls {
291            None => {
292                if let Some(result) = &rc.html_no_urls {
293                    return Ok(result.clone());
294                }
295                let ctx = cached.html_hbs_context.lock().unwrap();
296                #[cfg(test)]
297                cached
298                    .html_render_count
299                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
300                let result = Arc::new(render_html_with_hbs_context(
301                    cached.html_template.registry.as_ref(),
302                    &ctx,
303                    &cached.html_context,
304                )?);
305                rc.html_no_urls = Some(result.clone());
306                Ok(result)
307            }
308            Some(urls) => {
309                // If the CSS template doesn't reference {{src}}, URLs don't affect output
310                if !cached.shared.css_template_uses_src {
311                    drop(rc);
312                    return self.generate_html_shared(None);
313                }
314                if rc.html_last_urls.as_ref() == Some(urls)
315                    && let Some(result) = &rc.html_last_result
316                {
317                    return Ok(result.clone());
318                }
319                // Render CSS with the custom URLs (in-place src mutate, no clone)
320                #[cfg(test)]
321                cached
322                    .html_render_count
323                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
324                let src = make_src(&self.options, urls);
325                let styles = {
326                    let mut css_ctx = cached.css_hbs_context.lock().unwrap();
327                    render_css_with_src_mutate(
328                        &cached.shared,
329                        &mut css_ctx,
330                        &cached.css_context,
331                        &src,
332                    )?
333                };
334                // Hot path: default HTML template -- inject styles directly, skip clone
335                if cached.html_template.registry.is_none() {
336                    let result = Arc::new(render_default_html_with_styles(
337                        &cached.html_context,
338                        &styles,
339                    ));
340                    rc.html_last_urls = Some(urls.clone());
341                    rc.html_last_result = Some(result.clone());
342                    return Ok(result);
343                }
344                // Custom HTML template: in-place styles mutate, no clone
345                let mut html_ctx = cached.html_hbs_context.lock().unwrap();
346                let registry = cached
347                    .html_template
348                    .registry
349                    .as_ref()
350                    .expect("HTML registry should exist for custom template");
351                let result = Arc::new(render_with_field_swap(
352                    &mut html_ctx,
353                    "styles",
354                    serde_json::Value::String(styles),
355                    |ctx| registry.render_with_context("html", ctx).map_err(to_io_err),
356                )?);
357                rc.html_last_urls = Some(urls.clone());
358                rc.html_last_result = Some(result.clone());
359                Ok(result)
360            }
361        }
362    }
363}