webfont_generator/memory/
updates.rs1use std::collections::{HashMap, HashSet};
2#[cfg(feature = "async")]
3use std::io::Error;
4use std::io::Result;
5use std::sync::OnceLock;
6
7use super::options::{invalid, validate_name};
8use super::{GenerateWebfontsFromSourcesResult, SvgSource, build};
9use crate::incremental::sources::{
10 MembershipChange, SourceUpdate, change_membership, ordered_sources, same_sources,
11 updated_sources,
12};
13use crate::input::{LoadedSvgSource, validate_glyph_names};
14use crate::rendering::css::{TemplateDependencies, template_dependencies};
15
16#[derive(serde::Deserialize)]
19#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
20pub enum SourceChange {
21 Added(SvgSource),
23 Changed {
25 id: String,
27 svg: String,
29 name: Option<String>,
31 },
32 Removed {
34 id: String,
36 },
37}
38
39#[derive(serde::Deserialize)]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct VariantSourceSet {
43 pub name: String,
45 pub ids: Vec<String>,
47}
48
49#[derive(serde::Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub enum SourceMembership {
54 Sources(Vec<String>),
56 Variants(Vec<VariantSourceSet>),
58}
59
60#[cfg(feature = "async")]
62pub struct SourceRegenerateError {
63 error: Error,
64 result: Option<Box<GenerateWebfontsFromSourcesResult>>,
65}
66
67#[cfg(feature = "async")]
68impl SourceRegenerateError {
69 pub(super) fn worker(error: tokio::task::JoinError) -> Self {
70 Self {
71 error: Error::other(format!("Source update task failed: {error}")),
72 result: None,
73 }
74 }
75 pub fn error(&self) -> &Error {
77 &self.error
78 }
79 pub fn into_result(self) -> Option<GenerateWebfontsFromSourcesResult> {
81 self.result.map(|result| *result)
82 }
83 pub fn into_parts(self) -> (Error, Option<GenerateWebfontsFromSourcesResult>) {
85 (self.error, self.result.map(|result| *result))
86 }
87}
88
89#[cfg(feature = "async")]
90impl std::fmt::Debug for SourceRegenerateError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("SourceRegenerateError")
93 .field("error", &self.error)
94 .field("recoverable", &self.result.is_some())
95 .finish()
96 }
97}
98#[cfg(feature = "async")]
99impl std::fmt::Display for SourceRegenerateError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 self.error.fmt(f)
102 }
103}
104#[cfg(feature = "async")]
105impl std::error::Error for SourceRegenerateError {
106 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
107 Some(&self.error)
108 }
109}
110
111impl GenerateWebfontsFromSourcesResult {
112 fn membership_for_change(
113 &self,
114 id: &str,
115 change: MembershipChange<'_>,
116 ) -> Result<SourceMembership> {
117 let names = self.options.variants.as_ref().map(|family| {
118 family
119 .variants
120 .iter()
121 .map(|variant| variant.name.clone())
122 .collect::<Vec<_>>()
123 });
124 let mut sets = self
125 .options
126 .sources
127 .iter()
128 .map(|sources| sources.iter().map(|source| source.id.clone()).collect())
129 .collect::<Vec<_>>();
130 change_membership(&mut sets, names.as_deref(), id, change)?;
131 Ok(match names {
132 Some(names) => SourceMembership::Variants(
133 names
134 .into_iter()
135 .zip(sets)
136 .map(|(name, ids)| VariantSourceSet { name, ids })
137 .collect(),
138 ),
139 None => SourceMembership::Sources(sets.remove(0)),
140 })
141 }
142
143 pub fn add_sync(&mut self, source: SvgSource, variant: Option<&str>) -> Result<()> {
146 let membership = self.membership_for_change(&source.id, MembershipChange::Add(variant))?;
147 self.regenerate_sync(membership, vec![SourceChange::Added(source)])
148 }
149
150 pub fn update_sync(&mut self, id: String, svg: String, name: Option<String>) -> Result<()> {
153 let membership = self.membership_for_change(&id, MembershipChange::Update)?;
154 self.regenerate_sync(membership, vec![SourceChange::Changed { id, svg, name }])
155 }
156
157 pub fn remove_sync(&mut self, id: String) -> Result<()> {
160 let membership = self.membership_for_change(&id, MembershipChange::Remove)?;
161 self.regenerate_sync(membership, vec![SourceChange::Removed { id }])
162 }
163
164 #[cfg(feature = "async")]
166 pub async fn add(
167 self,
168 source: SvgSource,
169 variant: Option<String>,
170 ) -> std::result::Result<Self, SourceRegenerateError> {
171 let membership = match self
172 .membership_for_change(&source.id, MembershipChange::Add(variant.as_deref()))
173 {
174 Ok(membership) => membership,
175 Err(error) => {
176 return Err(SourceRegenerateError {
177 error,
178 result: Some(Box::new(self)),
179 });
180 }
181 };
182 self.regenerate(membership, vec![SourceChange::Added(source)])
183 .await
184 }
185
186 #[cfg(feature = "async")]
188 pub async fn update(
189 self,
190 id: String,
191 svg: String,
192 name: Option<String>,
193 ) -> std::result::Result<Self, SourceRegenerateError> {
194 let membership = match self.membership_for_change(&id, MembershipChange::Update) {
195 Ok(membership) => membership,
196 Err(error) => {
197 return Err(SourceRegenerateError {
198 error,
199 result: Some(Box::new(self)),
200 });
201 }
202 };
203 self.regenerate(membership, vec![SourceChange::Changed { id, svg, name }])
204 .await
205 }
206
207 #[cfg(feature = "async")]
209 pub async fn remove(self, id: String) -> std::result::Result<Self, SourceRegenerateError> {
210 let membership = match self.membership_for_change(&id, MembershipChange::Remove) {
211 Ok(membership) => membership,
212 Err(error) => {
213 return Err(SourceRegenerateError {
214 error,
215 result: Some(Box::new(self)),
216 });
217 }
218 };
219 self.regenerate(membership, vec![SourceChange::Removed { id }])
220 .await
221 }
222
223 pub fn regenerate_sync(
227 &mut self,
228 membership: SourceMembership,
229 changes: Vec<SourceChange>,
230 ) -> Result<()> {
231 if self.caches.is_none() {
232 return Err(invalid("Source updates require incremental generation"));
233 }
234 let memberships = match (&self.options.variants, membership) {
235 (None, SourceMembership::Sources(ids)) => vec![ids],
236 (Some(variants), SourceMembership::Variants(sets)) => {
237 if sets.len() != variants.variants.len()
238 || sets
239 .iter()
240 .zip(&variants.variants)
241 .any(|(set, variant)| set.name != variant.name)
242 {
243 return Err(invalid("Supply every variant in its original order"));
244 }
245 sets.into_iter().map(|set| set.ids).collect()
246 }
247 _ => {
248 return Err(invalid(
249 "Source membership does not match the generation mode",
250 ));
251 }
252 };
253 if changes.is_empty()
254 && memberships
255 .iter()
256 .zip(&self.options.sources)
257 .all(|(ids, sources)| {
258 ids.iter()
259 .map(String::as_str)
260 .eq(sources.iter().map(|source| source.id.as_str()))
261 })
262 {
263 return Ok(());
264 }
265 let mut staged = self.options.clone();
266 if let Some(variants) = &mut staged.variants {
267 for (variant, ids) in variants.variants.iter_mut().zip(&memberships) {
268 variant.source_ids = ids.clone();
269 }
270 }
271 let previous = staged
272 .sources
273 .iter()
274 .flatten()
275 .map(|source| (source.id.clone(), source.clone()))
276 .collect::<HashMap<_, _>>();
277 let mut changed_ids = HashSet::new();
278 let mut supplied_ids = HashSet::new();
279 let mut updates = Vec::with_capacity(changes.len());
280 for change in changes {
281 let id = match &change {
282 SourceChange::Added(source) => &source.id,
283 SourceChange::Changed { id, .. } | SourceChange::Removed { id } => id,
284 };
285 if id.is_empty() || !changed_ids.insert(id.clone()) {
286 return Err(invalid("Change IDs must be nonempty and unique"));
287 }
288 match change {
289 SourceChange::Added(source) => {
290 if previous.contains_key(&source.id) {
291 return Err(invalid(format!("Source {:?} already exists", source.id)));
292 }
293 supplied_ids.insert(source.id.clone());
294 updates.push(SourceUpdate::Set(LoadedSvgSource {
295 id: source.id,
296 glyph_name: source.name,
297 contents: source.svg.into(),
298 }));
299 }
300 SourceChange::Changed { id, svg, name } => {
301 let source = previous
302 .get(&id)
303 .ok_or_else(|| invalid(format!("Unknown source {id:?}")))?;
304 supplied_ids.insert(id.clone());
305 updates.push(SourceUpdate::Set(LoadedSvgSource {
306 id,
307 contents: svg.into(),
308 glyph_name: name.unwrap_or_else(|| source.glyph_name.clone()),
309 }));
310 }
311 SourceChange::Removed { id } => {
312 if !previous.contains_key(&id) {
313 return Err(invalid(format!("Unknown source {id:?}")));
314 }
315 updates.push(SourceUpdate::Remove(id));
316 }
317 }
318 }
319 let previous = previous.into_values().collect::<Vec<_>>();
320 let definitions = updated_sources(&previous, &updates);
321 let mut used = HashSet::new();
322 staged.sources = memberships
323 .into_iter()
324 .map(|ids| {
325 if ids.is_empty() {
326 return Err(invalid("Each design must contain at least one source"));
327 }
328 let mut seen = HashSet::new();
329 for id in &ids {
330 if !seen.insert(id.clone()) {
331 return Err(invalid(format!("Duplicate source ID {id:?}")));
332 }
333 used.insert(id.clone());
334 }
335 let sources = ordered_sources(&definitions, &ids)
336 .map_err(|id| invalid(format!("Unknown membership ID {id:?}")))?;
337 for source in &sources {
338 validate_name(&source.glyph_name)?;
339 }
340 validate_glyph_names(&sources)?;
341 Ok(sources)
342 })
343 .collect::<Result<Vec<_>>>()?;
344 if supplied_ids.iter().any(|id| !used.contains(id)) {
345 return Err(invalid(
346 "Added or changed sources must appear in final membership",
347 ));
348 }
349 let same = staged
350 .sources
351 .iter()
352 .zip(&self.options.sources)
353 .all(|(new, old)| same_sources(old, new));
354 if same {
355 return Ok(());
356 }
357
358 let fonts = build(&mut staged, self.caches.as_mut(), &updates)?;
359 let names_unchanged = self
360 .options
361 .sources
362 .iter()
363 .flatten()
364 .map(|source| &source.glyph_name)
365 .eq(staged
366 .sources
367 .iter()
368 .flatten()
369 .map(|source| &source.glyph_name));
370 let codepoints_unchanged = self.options.common.codepoints == staged.common.codepoints;
371 let keep_css = matches!(self.css.get(), Some(Ok(_)))
372 && self
373 .options
374 .css_template
375 .as_deref()
376 .map(template_dependencies)
377 .unwrap_or_else(TemplateDependencies::css_default)
378 .can_reuse_css_no_urls(names_unchanged, codepoints_unchanged);
379 let html_dependencies = self
380 .options
381 .html_template
382 .as_deref()
383 .map(template_dependencies)
384 .unwrap_or_else(TemplateDependencies::html_default);
385 let keep_html =
386 html_dependencies.can_reuse_html(names_unchanged, codepoints_unchanged, keep_css)
387 && matches!(self.html.get(), Some(Ok(_)));
388 self.options = staged;
389 self.fonts = fonts;
390 self.shared = OnceLock::new();
391 if !keep_css {
392 self.css = OnceLock::new();
393 }
394 if !keep_html {
395 self.html = OnceLock::new();
396 }
397 Ok(())
398 }
399
400 #[cfg(feature = "async")]
403 pub async fn regenerate(
404 mut self,
405 membership: SourceMembership,
406 changes: Vec<SourceChange>,
407 ) -> std::result::Result<Self, SourceRegenerateError> {
408 let runtime = match tokio::runtime::Handle::try_current() {
409 Ok(runtime) => runtime,
410 Err(error) => {
411 return Err(SourceRegenerateError {
412 error: Error::other(format!("Source update requires a Tokio runtime: {error}")),
413 result: Some(Box::new(self)),
414 });
415 }
416 };
417 runtime
418 .spawn_blocking(move || match self.regenerate_sync(membership, changes) {
419 Ok(()) => Ok(self),
420 Err(error) => Err(SourceRegenerateError {
421 error,
422 result: Some(Box::new(self)),
423 }),
424 })
425 .await
426 .map_err(SourceRegenerateError::worker)?
427 }
428}