Skip to main content

webfont_generator/rendering/
result.rs

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