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    /// Render a fixed TypeScript source module with `icons`, `IconName`,
61    /// `iconBaseSelector`, and `iconClassPrefix`. Repeated calls share the output.
62    /// Uses effective CSS context settings, independent of CSS/HTML templates or output flags.
63    pub fn generate_ts(&self) -> std::io::Result<Arc<String>> {
64        Ok(self
65            .ts
66            .get_or_init(|| {
67                Arc::new(super::ts::render_ts(
68                    &self.options,
69                    self.source_files
70                        .iter()
71                        .map(|source| source.glyph_name.as_str()),
72                    self.options.variants.is_some(),
73                    self.css_context.as_ref(),
74                ))
75            })
76            .clone())
77    }
78
79    fn validate_render_urls(
80        &self,
81        urls: Option<&HashMap<FontType, String>>,
82    ) -> std::io::Result<()> {
83        if self.options.variants.is_some()
84            && urls.is_some_and(|urls| {
85                urls.contains_key(&FontType::Svg) || urls.contains_key(&FontType::Eot)
86            })
87        {
88            return Err(std::io::Error::new(
89                std::io::ErrorKind::InvalidInput,
90                "SVG and EOT URLs are unsupported for variant results.",
91            ));
92        }
93        Ok(())
94    }
95    #[cfg(test)]
96    pub(crate) fn has_carried_css_no_urls_for_test(&self) -> bool {
97        self.carried_render
98            .as_ref()
99            .is_some_and(|carried| carried.cache.css_no_urls.is_some())
100    }
101
102    #[cfg(test)]
103    pub(crate) fn has_carried_html_no_urls_for_test(&self) -> bool {
104        self.carried_render
105            .as_ref()
106            .is_some_and(|carried| carried.cache.html_no_urls.is_some())
107    }
108
109    pub(crate) fn get_cached_io(&self) -> std::io::Result<&CachedTemplateData> {
110        self.cached
111            .get_or_init(|| {
112                let shared = SharedTemplateData::from_files(&self.options, &self.source_files)
113                    .map_err(|e| e.to_string())?;
114                let css_context = match &self.css_context {
115                    Some(ctx) => ctx.clone(),
116                    None => build_css_context(&self.options, &shared),
117                };
118                let html_context = match &self.html_context {
119                    Some(ctx) => ctx.clone(),
120                    None => build_html_context(&self.options, &shared, &self.source_files, None)
121                        .map_err(|e| e.to_string())?,
122                };
123                let html_template = load_html_template(&self.options).map_err(|e| e.to_string())?;
124                let render_cache = self
125                    .carried_render
126                    .as_ref()
127                    .map(|carried| {
128                        if carried.css_template_source != shared.css_template_source {
129                            // HTML may embed CSS, so changing CSS invalidates both companions.
130                            RenderCache::default()
131                        } else if carried.html_template_source != html_template.source {
132                            RenderCache {
133                                html_no_urls: None,
134                                html_last_urls: None,
135                                html_last_result: None,
136                                ..carried.cache.clone()
137                            }
138                        } else {
139                            carried.cache.clone()
140                        }
141                    })
142                    .unwrap_or_default();
143                let css_hbs_context =
144                    handlebars::Context::wraps(&css_context).map_err(|e| e.to_string())?;
145                let html_hbs_context =
146                    handlebars::Context::wraps(&html_context).map_err(|e| e.to_string())?;
147                Ok(CachedTemplateData {
148                    #[cfg(test)]
149                    html_render_count: Default::default(),
150                    shared,
151                    css_context,
152                    css_hbs_context: Mutex::new(css_hbs_context),
153                    html_context,
154                    html_hbs_context: Mutex::new(html_hbs_context),
155                    html_template,
156                    // Seed with entries carried across a regenerate;
157                    // these are renders that don't depend on what changed, so reusing them is safe.
158                    render_cache: Mutex::new(render_cache),
159                })
160            })
161            .as_ref()
162            .map_err(to_io_err)
163    }
164
165    pub(crate) fn reusable_render_cache(
166        &self,
167        names_unchanged: bool,
168        codepoints_unchanged: bool,
169    ) -> Option<CarriedRenderCache> {
170        self.render_cache_source().map(|carried| {
171            let css_deps = carried.css_dependencies;
172            let css_no_urls_unchanged =
173                css_deps.can_reuse_css_no_urls(names_unchanged, codepoints_unchanged);
174            let css_with_urls_unchanged =
175                css_deps.can_reuse_css_with_urls(names_unchanged, codepoints_unchanged);
176            let html_no_urls_unchanged = carried.html_dependencies.can_reuse_html(
177                names_unchanged,
178                codepoints_unchanged,
179                css_no_urls_unchanged,
180            );
181            let html_with_urls_unchanged = carried.html_dependencies.can_reuse_html(
182                names_unchanged,
183                codepoints_unchanged,
184                css_with_urls_unchanged,
185            );
186
187            let rc = carried.cache;
188            CarriedRenderCache {
189                css_dependencies: carried.css_dependencies,
190                html_dependencies: carried.html_dependencies,
191                css_template_source: carried.css_template_source,
192                html_template_source: carried.html_template_source,
193                cache: RenderCache {
194                    css_no_urls: css_no_urls_unchanged
195                        .then(|| rc.css_no_urls.clone())
196                        .flatten(),
197                    html_no_urls: html_no_urls_unchanged
198                        .then(|| rc.html_no_urls.clone())
199                        .flatten(),
200                    css_last_urls: css_with_urls_unchanged
201                        .then(|| rc.css_last_urls.clone())
202                        .flatten(),
203                    css_last_result: css_with_urls_unchanged
204                        .then(|| rc.css_last_result.clone())
205                        .flatten(),
206                    html_last_urls: html_with_urls_unchanged
207                        .then(|| rc.html_last_urls.clone())
208                        .flatten(),
209                    html_last_result: html_with_urls_unchanged
210                        .then(|| rc.html_last_result.clone())
211                        .flatten(),
212                },
213            }
214        })
215    }
216
217    pub(crate) fn render_cache_source(&self) -> Option<CarriedRenderCache> {
218        self.cached
219            .get()
220            .and_then(|cached| cached.as_ref().ok())
221            .map(|cached| CarriedRenderCache {
222                cache: cached.render_cache.lock().unwrap().clone(),
223                css_dependencies: cached.shared.css_template_dependencies,
224                html_dependencies: cached.html_template.dependencies,
225                css_template_source: cached.shared.css_template_source.clone(),
226                html_template_source: cached.html_template.source.clone(),
227            })
228            .or_else(|| self.carried_render.clone())
229    }
230
231    /// Generate a CSS string for this webfont result.
232    ///
233    /// Pass `urls` to override the default font URLs in the CSS output.
234    pub fn generate_css_pure(
235        &self,
236        urls: Option<HashMap<FontType, String>>,
237    ) -> std::io::Result<String> {
238        self.generate_css_shared(urls)
239            .map(|text| text.as_ref().clone())
240    }
241
242    pub(crate) fn generate_css_shared(
243        &self,
244        urls: Option<HashMap<FontType, String>>,
245    ) -> std::io::Result<Arc<String>> {
246        self.validate_render_urls(urls.as_ref())?;
247        let cached = self.get_cached_io()?;
248        let mut rc = cached.render_cache.lock().unwrap();
249
250        match &urls {
251            None => {
252                if let Some(result) = &rc.css_no_urls {
253                    return Ok(result.clone());
254                }
255                let ctx = cached.css_hbs_context.lock().unwrap();
256                let result = Arc::new(render_css_with_hbs_context(
257                    &cached.shared,
258                    &ctx,
259                    &cached.css_context,
260                )?);
261                rc.css_no_urls = Some(result.clone());
262                Ok(result)
263            }
264            Some(urls) => {
265                // If the template doesn't reference {{src}}, URLs don't affect output
266                if !cached.shared.css_template_uses_src {
267                    drop(rc);
268                    return self.generate_css_shared(None);
269                }
270                if rc.css_last_urls.as_ref() == Some(urls)
271                    && let Some(result) = &rc.css_last_result
272                {
273                    return Ok(result.clone());
274                }
275                let src = make_src(&self.options, urls);
276                let mut ctx = cached.css_hbs_context.lock().unwrap();
277                let result = Arc::new(render_css_with_src_mutate(
278                    &cached.shared,
279                    &mut ctx,
280                    &cached.css_context,
281                    &src,
282                )?);
283                rc.css_last_urls = Some(urls.clone());
284                rc.css_last_result = Some(result.clone());
285                Ok(result)
286            }
287        }
288    }
289
290    /// Generate an HTML string for this webfont result.
291    ///
292    /// Pass `urls` to override the default font URLs in the HTML output.
293    pub fn generate_html_pure(
294        &self,
295        urls: Option<HashMap<FontType, String>>,
296    ) -> std::io::Result<String> {
297        self.generate_html_shared(urls)
298            .map(|text| text.as_ref().clone())
299    }
300
301    pub(crate) fn generate_html_shared(
302        &self,
303        urls: Option<HashMap<FontType, String>>,
304    ) -> std::io::Result<Arc<String>> {
305        self.validate_render_urls(urls.as_ref())?;
306        let cached = self.get_cached_io()?;
307        let mut rc = cached.render_cache.lock().unwrap();
308
309        match &urls {
310            None => {
311                if let Some(result) = &rc.html_no_urls {
312                    return Ok(result.clone());
313                }
314                let ctx = cached.html_hbs_context.lock().unwrap();
315                #[cfg(test)]
316                cached
317                    .html_render_count
318                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
319                let result = Arc::new(render_html_with_hbs_context(
320                    cached.html_template.registry.as_ref(),
321                    &ctx,
322                    &cached.html_context,
323                )?);
324                rc.html_no_urls = Some(result.clone());
325                Ok(result)
326            }
327            Some(urls) => {
328                // If the CSS template doesn't reference {{src}}, URLs don't affect output
329                if !cached.shared.css_template_uses_src {
330                    drop(rc);
331                    return self.generate_html_shared(None);
332                }
333                if rc.html_last_urls.as_ref() == Some(urls)
334                    && let Some(result) = &rc.html_last_result
335                {
336                    return Ok(result.clone());
337                }
338                // Render CSS with the custom URLs (in-place src mutate, no clone)
339                #[cfg(test)]
340                cached
341                    .html_render_count
342                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
343                let src = make_src(&self.options, urls);
344                let styles = {
345                    let mut css_ctx = cached.css_hbs_context.lock().unwrap();
346                    render_css_with_src_mutate(
347                        &cached.shared,
348                        &mut css_ctx,
349                        &cached.css_context,
350                        &src,
351                    )?
352                };
353                // Hot path: default HTML template -- inject styles directly, skip clone
354                if cached.html_template.registry.is_none() {
355                    let result = Arc::new(render_default_html_with_styles(
356                        &cached.html_context,
357                        &styles,
358                    ));
359                    rc.html_last_urls = Some(urls.clone());
360                    rc.html_last_result = Some(result.clone());
361                    return Ok(result);
362                }
363                // Custom HTML template: in-place styles mutate, no clone
364                let mut html_ctx = cached.html_hbs_context.lock().unwrap();
365                let registry = cached
366                    .html_template
367                    .registry
368                    .as_ref()
369                    .expect("HTML registry should exist for custom template");
370                let result = Arc::new(render_with_field_swap(
371                    &mut html_ctx,
372                    "styles",
373                    serde_json::Value::String(styles),
374                    |ctx| registry.render_with_context("html", ctx).map_err(to_io_err),
375                )?);
376                rc.html_last_urls = Some(urls.clone());
377                rc.html_last_result = Some(result.clone());
378                Ok(result)
379            }
380        }
381    }
382}