1mod eot;
63mod svg;
64mod templates;
65#[cfg(test)]
66mod test_helpers;
67mod ttf;
68mod types;
69mod util;
70mod woff;
71
72#[cfg(feature = "napi")]
73use napi::threadsafe_function::ThreadsafeFunction;
74#[cfg(feature = "napi")]
75use napi::{Error as NapiError, Status};
76#[cfg(feature = "napi")]
77use napi_derive::napi;
78use rayon::join;
79use std::collections::HashSet;
80use std::io::ErrorKind;
81use std::path::Path;
82use std::sync::Arc;
83#[cfg(feature = "napi")]
84use std::sync::Mutex;
85use tokio::task::JoinSet;
86
87use svg::{build_svg_font, prepare_svg_font, svg_options_from_options};
88#[cfg(feature = "napi")]
89use templates::{
90 SharedTemplateData, apply_context_function, build_css_context, build_html_context,
91 build_html_registry,
92};
93use templates::{render_css_with_hbs_context, render_html_with_hbs_context};
94#[cfg(feature = "napi")]
95use util::to_napi_err;
96
97pub use types::{
98 CssContext, FontType, FormatOptions, GenerateWebfontsOptions, GenerateWebfontsResult,
99 HtmlContext, SvgFormatOptions, TtfFormatOptions, WoffFormatOptions,
100};
101use types::{
102 DEFAULT_FONT_ORDER, LoadedSvgFile, ResolvedGenerateWebfontsOptions, resolved_font_types,
103};
104
105#[cfg(all(test, feature = "napi"))]
106#[unsafe(no_mangle)]
107extern "C" fn napi_call_threadsafe_function(
108 _: napi::sys::napi_threadsafe_function,
109 _: *mut std::ffi::c_void,
110 _: napi::sys::napi_threadsafe_function_call_mode,
111) -> napi::sys::napi_status {
112 0
113}
114
115#[cfg(feature = "napi")]
128#[napi]
129#[allow(clippy::type_complexity)] pub async fn generate_webfonts(
131 options: GenerateWebfontsOptions,
132 rename: Option<ThreadsafeFunction<String, String, String, Status, false>>,
133 css_context: Option<
134 ThreadsafeFunction<
135 serde_json::Map<String, serde_json::Value>,
136 serde_json::Map<String, serde_json::Value>,
137 serde_json::Map<String, serde_json::Value>,
138 Status,
139 false,
140 >,
141 >,
142 html_context: Option<
143 ThreadsafeFunction<
144 serde_json::Map<String, serde_json::Value>,
145 serde_json::Map<String, serde_json::Value>,
146 serde_json::Map<String, serde_json::Value>,
147 Status,
148 false,
149 >,
150 >,
151) -> napi::Result<GenerateWebfontsResult> {
152 validate_generate_webfonts_options(&options)?;
153 let source_files = load_svg_files_napi(&options.files, rename.as_ref()).await?;
154 let mut resolved_options = resolve_generate_webfonts_options(options)?;
155 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
156
157 let mut result =
158 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
159 .await
160 .map_err(|error| {
161 NapiError::new(
162 Status::GenericFailure,
163 format!("Native webfont generation task failed: {error}"),
164 )
165 })??;
166
167 if css_context.is_some() || html_context.is_some() {
171 let shared =
172 SharedTemplateData::new(&result.options, &result.source_files).map_err(to_napi_err)?;
173
174 let mut css_ctx = build_css_context(&result.options, &shared);
175 if css_context.is_some() {
176 css_ctx = apply_context_function(css_ctx, css_context.as_ref())
177 .await
178 .map_err(to_napi_err)?;
179 result.css_context = Some(css_ctx.clone());
180 }
181
182 let mut html_ctx = if result.options.html || html_context.is_some() {
183 build_html_context(&result.options, &shared, &result.source_files, None)
184 .map_err(to_napi_err)?
185 } else {
186 serde_json::Map::new()
187 };
188 if html_context.is_some() {
189 html_ctx = apply_context_function(html_ctx, html_context.as_ref())
190 .await
191 .map_err(to_napi_err)?;
192 result.html_context = Some(html_ctx.clone());
193 }
194
195 let html_registry = build_html_registry(&result.options).map_err(to_napi_err)?;
197 let css_hbs_context = handlebars::Context::wraps(&css_ctx).map_err(to_napi_err)?;
198 let html_hbs_context = handlebars::Context::wraps(&html_ctx).map_err(to_napi_err)?;
199 let _ = result.cached.set(Ok(types::CachedTemplateData {
200 shared,
201 css_context: css_ctx,
202 css_hbs_context: Mutex::new(css_hbs_context),
203 html_context: html_ctx,
204 html_hbs_context: Mutex::new(html_hbs_context),
205 html_registry,
206 render_cache: Mutex::new(Default::default()),
207 }));
208 }
209
210 if result.options.write_files {
211 write_generate_webfonts_result(&result).await?;
212 }
213
214 Ok(result)
215}
216
217pub type RenameFn = Box<dyn Fn(&str) -> String + Send + Sync>;
219
220pub async fn generate(
224 options: GenerateWebfontsOptions,
225 rename: Option<RenameFn>,
226) -> std::io::Result<GenerateWebfontsResult> {
227 validate_generate_webfonts_options(&options)?;
228 let source_files = load_svg_files(&options.files, rename.as_deref()).await?;
229 let mut resolved_options = resolve_generate_webfonts_options(options)?;
230 finalize_generate_webfonts_options(&mut resolved_options, &source_files)?;
231
232 let result =
233 tokio::task::spawn_blocking(move || generate_webfonts_sync(resolved_options, source_files))
234 .await
235 .map_err(std::io::Error::other)??;
236
237 if result.options.write_files {
238 write_generate_webfonts_result(&result).await?;
239 }
240
241 Ok(result)
242}
243
244pub fn generate_sync(
246 options: GenerateWebfontsOptions,
247 rename: Option<RenameFn>,
248) -> std::io::Result<GenerateWebfontsResult> {
249 tokio::runtime::Runtime::new()?.block_on(generate(options, rename))
250}
251
252fn validate_generate_webfonts_options(options: &GenerateWebfontsOptions) -> std::io::Result<()> {
253 if options.dest.is_empty() {
254 return Err(std::io::Error::new(
255 ErrorKind::InvalidInput,
256 "\"options.dest\" is empty.".to_owned(),
257 ));
258 }
259
260 if options.files.is_empty() {
261 return Err(std::io::Error::new(
262 ErrorKind::InvalidInput,
263 "\"options.files\" is empty.".to_owned(),
264 ));
265 }
266
267 if options.css.unwrap_or(true)
268 && let Some(ref path) = options.css_template
269 && !Path::new(path).exists()
270 {
271 return Err(std::io::Error::new(
272 ErrorKind::InvalidInput,
273 format!("\"options.cssTemplate\" file not found: {path}"),
274 ));
275 }
276
277 if options.html.unwrap_or(false)
278 && let Some(ref path) = options.html_template
279 && !Path::new(path).exists()
280 {
281 return Err(std::io::Error::new(
282 ErrorKind::InvalidInput,
283 format!("\"options.htmlTemplate\" file not found: {path}"),
284 ));
285 }
286
287 Ok(())
288}
289
290pub(crate) fn resolve_generate_webfonts_options(
291 options: GenerateWebfontsOptions,
292) -> std::io::Result<ResolvedGenerateWebfontsOptions> {
293 let types = resolved_font_types(&options);
294 validate_font_type_order(&options, &types)?;
295 let order = resolve_font_type_order(&options, &types);
296 let css = options.css.unwrap_or(true);
297 let html = options.html.unwrap_or(false);
298 let font_name = options.font_name.unwrap_or_else(|| "iconfont".to_owned());
299 let css_dest = options
300 .css_dest
301 .unwrap_or_else(|| default_output_dest(&options.dest, &font_name, "css"));
302 let html_dest = options
303 .html_dest
304 .unwrap_or_else(|| default_output_dest(&options.dest, &font_name, "html"));
305 let write_files = options.write_files.unwrap_or(true);
306
307 let svg_format = options
308 .format_options
309 .as_ref()
310 .and_then(|fo| fo.svg.as_ref());
311 let center_vertically = svg_format
312 .and_then(|s| s.center_vertically)
313 .or(options.center_vertically);
314 let optimize_output = svg_format
315 .and_then(|s| s.optimize_output)
316 .or(options.optimize_output);
317 let preserve_aspect_ratio = svg_format
318 .and_then(|s| s.preserve_aspect_ratio)
319 .or(options.preserve_aspect_ratio);
320
321 Ok(ResolvedGenerateWebfontsOptions {
322 ascent: options.ascent,
323 center_horizontally: options.center_horizontally,
324 center_vertically,
325 css,
326 css_dest,
327 css_template: match options.css_template {
328 Some(ref t) if t.is_empty() => {
329 return Err(std::io::Error::new(
330 ErrorKind::InvalidInput,
331 "\"options.cssTemplate\" must not be empty.".to_owned(),
332 ));
333 }
334 other => other,
335 },
336 codepoints: options.codepoints.unwrap_or_default().into_iter().collect(),
337 css_fonts_url: options.css_fonts_url,
338 descent: options.descent,
339 dest: options.dest,
340 files: options.files,
341 fixed_width: options.fixed_width,
342 format_options: options.format_options,
343 html,
344 html_dest,
345 html_template: match options.html_template {
346 Some(ref t) if t.is_empty() => {
347 return Err(std::io::Error::new(
348 ErrorKind::InvalidInput,
349 "\"options.htmlTemplate\" must not be empty.".to_owned(),
350 ));
351 }
352 other => other,
353 },
354 font_height: options.font_height,
355 font_name,
356 font_style: options.font_style,
357 font_weight: options.font_weight,
358 ligature: options.ligature.unwrap_or(true),
359 normalize: options.normalize.unwrap_or(true),
360 order,
361 optimize_output,
362 preserve_aspect_ratio,
363 round: options.round,
364 start_codepoint: options.start_codepoint.unwrap_or(0xF101),
365 template_options: options.template_options,
366 types,
367 write_files,
368 })
369}
370
371pub(crate) fn finalize_generate_webfonts_options(
372 options: &mut ResolvedGenerateWebfontsOptions,
373 source_files: &[LoadedSvgFile],
374) -> std::io::Result<()> {
375 options.codepoints =
376 resolve_codepoints(source_files, &options.codepoints, options.start_codepoint)?;
377
378 Ok(())
379}
380
381fn resolve_font_type_order(options: &GenerateWebfontsOptions, types: &[FontType]) -> Vec<FontType> {
382 match &options.order {
383 Some(order) => order.clone(),
384 None => DEFAULT_FONT_ORDER
385 .iter()
386 .copied()
387 .filter(|font_type| types.contains(font_type))
388 .collect(),
389 }
390}
391
392fn default_output_dest(dest: &str, font_name: &str, extension: &str) -> String {
393 Path::new(dest)
394 .join(format!("{font_name}.{extension}"))
395 .to_string_lossy()
396 .into_owned()
397}
398
399fn generate_webfonts_sync(
400 options: ResolvedGenerateWebfontsOptions,
401 source_files: Vec<LoadedSvgFile>,
402) -> std::io::Result<GenerateWebfontsResult> {
403 let wants_svg = options.types.contains(&FontType::Svg);
404 let wants_ttf = options.types.contains(&FontType::Ttf);
405 let wants_woff = options.types.contains(&FontType::Woff);
406 let wants_woff2 = options.types.contains(&FontType::Woff2);
407 let wants_eot = options.types.contains(&FontType::Eot);
408
409 let svg_options = svg_options_from_options(&options);
410 let prepared = prepare_svg_font(&svg_options, &source_files)?;
411
412 let (svg_font, raw_ttf) = join(
413 || -> std::io::Result<Option<String>> {
414 if wants_svg {
415 Ok(Some(build_svg_font(&svg_options, &prepared)))
416 } else {
417 Ok(None)
418 }
419 },
420 || -> std::io::Result<Option<Vec<u8>>> {
421 if wants_ttf || wants_woff || wants_woff2 || wants_eot {
422 let ttf_options = ttf::ttf_options_from_options(&options);
423 ttf::generate_ttf_font_bytes_from_glyphs(ttf_options, &prepared.processed_glyphs)
424 .map(Some)
425 } else {
426 Ok(None)
427 }
428 },
429 );
430
431 let svg_font = svg_font?.map(Arc::new);
432 let raw_ttf = raw_ttf?;
433
434 let (ttf_font, woff_font, woff2_font, eot_font) = if let Some(raw_ttf) = raw_ttf {
435 let raw_ttf = Arc::new(raw_ttf);
436 let ttf_font = wants_ttf.then(|| Arc::clone(&raw_ttf));
437 let woff_metadata = options
438 .format_options
439 .as_ref()
440 .and_then(|value| value.woff.as_ref())
441 .and_then(|value| value.metadata.as_deref());
442
443 let (woff_font, (woff2_font, eot_font)) = join(
444 || -> std::io::Result<Option<Vec<u8>>> {
445 if wants_woff {
446 woff::ttf_to_woff1(&raw_ttf, woff_metadata).map(Some)
447 } else {
448 Ok(None)
449 }
450 },
451 || {
452 join(
453 || -> std::io::Result<Option<Vec<u8>>> {
454 if wants_woff2 {
455 woff::ttf_to_woff2(&raw_ttf).map(Some)
456 } else {
457 Ok(None)
458 }
459 },
460 || -> std::io::Result<Option<Vec<u8>>> {
461 if wants_eot {
462 eot::ttf_to_eot(&raw_ttf).map(Some)
463 } else {
464 Ok(None)
465 }
466 },
467 )
468 },
469 );
470
471 (
472 ttf_font,
473 woff_font?.map(Arc::new),
474 woff2_font?.map(Arc::new),
475 eot_font?.map(Arc::new),
476 )
477 } else {
478 (None, None, None, None)
479 };
480
481 Ok(GenerateWebfontsResult {
482 cached: std::sync::OnceLock::new(),
483 css_context: None,
484 eot_font,
485 html_context: None,
486 options,
487 source_files,
488 svg_font,
489 ttf_font,
490 woff2_font,
491 woff_font,
492 })
493}
494
495async fn write_generate_webfonts_result(result: &GenerateWebfontsResult) -> std::io::Result<()> {
496 let mut tasks = JoinSet::new();
497 let font_name = result.options.font_name.clone();
498 let dest = result.options.dest.clone();
499
500 if let Some(svg_font) = &result.svg_font {
501 let path = default_output_dest(&dest, &font_name, "svg");
502 let contents = Arc::clone(svg_font);
503 tasks.spawn(async move { write_output_file(path, contents.as_bytes()).await });
504 }
505
506 if let Some(ttf_font) = &result.ttf_font {
507 let path = default_output_dest(&dest, &font_name, "ttf");
508 let contents = Arc::clone(ttf_font);
509 tasks.spawn(async move { write_output_file(path, &*contents).await });
510 }
511
512 if let Some(woff_font) = &result.woff_font {
513 let path = default_output_dest(&dest, &font_name, "woff");
514 let contents = Arc::clone(woff_font);
515 tasks.spawn(async move { write_output_file(path, &*contents).await });
516 }
517
518 if let Some(woff2_font) = &result.woff2_font {
519 let path = default_output_dest(&dest, &font_name, "woff2");
520 let contents = Arc::clone(woff2_font);
521 tasks.spawn(async move { write_output_file(path, &*contents).await });
522 }
523
524 if let Some(eot_font) = &result.eot_font {
525 let path = default_output_dest(&dest, &font_name, "eot");
526 let contents = Arc::clone(eot_font);
527 tasks.spawn(async move { write_output_file(path, &*contents).await });
528 }
529
530 if result.options.css || result.options.html {
532 let cached = result.get_cached_io()?;
533
534 if result.options.css {
535 let ctx = cached.css_hbs_context.lock().unwrap();
536 let css = render_css_with_hbs_context(&cached.shared, &ctx, &cached.css_context)?;
537 drop(ctx);
538 let css_dest = result.options.css_dest.clone();
539 let css = Arc::new(css);
540 tasks.spawn(async move { write_output_file(css_dest, css.as_bytes()).await });
541 }
542
543 if result.options.html {
544 let ctx = cached.html_hbs_context.lock().unwrap();
545 let html = render_html_with_hbs_context(
546 cached.html_registry.as_ref(),
547 &ctx,
548 &cached.html_context,
549 )?;
550 let html_dest = result.options.html_dest.clone();
551 tasks.spawn(async move { write_output_file(html_dest, html.into_bytes()).await });
552 }
553 }
554
555 while let Some(result) = tasks.join_next().await {
556 result.map_err(|error| {
557 std::io::Error::other(format!("Native write task failed: {error}"))
558 })??;
559 }
560
561 Ok(())
562}
563
564async fn write_output_file(path: String, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
565 if let Some(parent) = Path::new(&path).parent() {
566 tokio::fs::create_dir_all(parent).await?;
567 }
568
569 tokio::fs::write(path, contents).await
570}
571
572fn validate_font_type_order(
573 options: &GenerateWebfontsOptions,
574 requested_types: &[FontType],
575) -> std::io::Result<()> {
576 if let Some(order) = &options.order
577 && let Some(invalid_type) = order
578 .iter()
579 .copied()
580 .find(|font_type| !requested_types.contains(font_type))
581 {
582 return Err(std::io::Error::new(
583 ErrorKind::InvalidInput,
584 format!(
585 "Invalid font type order: '{}' is not present in 'types'.",
586 invalid_type.as_extension()
587 ),
588 ));
589 }
590
591 Ok(())
592}
593
594async fn load_svg_contents(paths: &[String]) -> std::io::Result<Vec<(String, String)>> {
596 let mut tasks = JoinSet::new();
597
598 for (index, path) in paths.iter().cloned().enumerate() {
599 tasks.spawn(async move {
600 tokio::fs::read_to_string(&path)
601 .await
602 .map(|contents| (index, (path, contents)))
603 });
604 }
605
606 let mut results = Vec::with_capacity(paths.len());
607 while let Some(result) = tasks.join_next().await {
608 let (index, pair) = result
609 .map_err(|error| std::io::Error::other(format!("SVG loading task failed: {error}")))?
610 .map_err(|error| {
611 std::io::Error::other(format!("Failed to read source SVG file: {error}"))
612 })?;
613 results.push((index, pair));
614 }
615
616 results.sort_by_key(|(index, _)| *index);
617 Ok(results.into_iter().map(|(_, pair)| pair).collect())
618}
619
620async fn load_svg_files(
622 paths: &[String],
623 rename: Option<&(dyn Fn(&str) -> String + Send + Sync)>,
624) -> std::io::Result<Vec<LoadedSvgFile>> {
625 let raw = load_svg_contents(paths).await?;
626 let source_files: Vec<LoadedSvgFile> = raw
627 .into_iter()
628 .map(|(path, contents)| {
629 let glyph_name = util::glyph_name_from_path(&path, rename)?;
630 Ok(LoadedSvgFile {
631 contents,
632 glyph_name,
633 path,
634 })
635 })
636 .collect::<std::io::Result<_>>()?;
637
638 validate_glyph_names(&source_files)?;
639 Ok(source_files)
640}
641
642#[cfg(feature = "napi")]
644async fn load_svg_files_napi(
645 paths: &[String],
646 rename: Option<
647 &napi::threadsafe_function::ThreadsafeFunction<String, String, String, Status, false>,
648 >,
649) -> napi::Result<Vec<LoadedSvgFile>> {
650 let raw = load_svg_contents(paths).await.map_err(to_napi_err)?;
651 let mut source_files = Vec::with_capacity(raw.len());
652
653 for (path, contents) in raw {
654 let glyph_name = if let Some(rename) = rename {
655 rename.call_async(path.clone()).await?
656 } else {
657 util::default_glyph_name_from_path(&path).map_err(to_napi_err)?
658 };
659 source_files.push(LoadedSvgFile {
660 contents,
661 glyph_name,
662 path,
663 });
664 }
665
666 validate_glyph_names(&source_files).map_err(to_napi_err)?;
667 Ok(source_files)
668}
669
670fn validate_glyph_names(source_files: &[LoadedSvgFile]) -> std::io::Result<()> {
671 let mut seen_names = HashSet::with_capacity(source_files.len());
672
673 for source_file in source_files {
674 if !seen_names.insert(source_file.glyph_name.clone()) {
675 return Err(std::io::Error::new(
676 ErrorKind::InvalidInput,
677 format!(
678 "The glyph name \"{}\" must be unique.",
679 source_file.glyph_name
680 ),
681 ));
682 }
683 }
684
685 Ok(())
686}
687
688use util::resolve_codepoints;
690
691#[cfg(test)]
692mod tests {
693 use super::{
694 resolve_generate_webfonts_options, resolved_font_types, validate_font_type_order,
695 validate_generate_webfonts_options, woff,
696 };
697 use crate::{FontType, GenerateWebfontsOptions, ttf::generate_ttf_font_bytes};
698
699 #[test]
700 fn generates_woff2_font_with_expected_header() {
701 let ttf_result = generate_ttf_font_bytes(GenerateWebfontsOptions {
702 css: Some(false),
703 dest: "artifacts".to_owned(),
704 files: vec![format!(
705 "{}/../vite-svg-2-webfont/src/fixtures/webfont-test/svg/add.svg",
706 env!("CARGO_MANIFEST_DIR")
707 )],
708 html: Some(false),
709 font_name: Some("iconfont".to_owned()),
710 ligature: Some(false),
711 ..Default::default()
712 })
713 .expect("expected ttf generation to succeed");
714
715 let result = woff::ttf_to_woff2(&ttf_result).expect("woff2 generation should succeed");
716
717 assert_eq!(&result[..4], b"wOF2");
718 }
719
720 #[test]
721 fn rejects_order_entries_that_are_not_present_in_types() {
722 let options = GenerateWebfontsOptions {
723 dest: "artifacts".to_owned(),
724 files: vec![],
725 font_name: Some("iconfont".to_owned()),
726 ligature: Some(false),
727 order: Some(vec![FontType::Svg, FontType::Woff]),
728 types: Some(vec![FontType::Svg]),
729 ..Default::default()
730 };
731
732 let error = validate_font_type_order(&options, &resolved_font_types(&options)).unwrap_err();
733
734 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
735 assert!(
736 error
737 .to_string()
738 .contains("Invalid font type order: 'woff' is not present in 'types'.")
739 );
740 }
741
742 #[test]
743 fn rejects_an_empty_dest() {
744 let options = GenerateWebfontsOptions {
745 dest: String::new(),
746 files: vec!["icon.svg".to_owned()],
747 font_name: Some("iconfont".to_owned()),
748 ligature: Some(false),
749 types: Some(vec![FontType::Svg]),
750 ..Default::default()
751 };
752
753 let error = validate_generate_webfonts_options(&options).unwrap_err();
754
755 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
756 assert!(error.to_string().contains("\"options.dest\" is empty."));
757 }
758
759 #[test]
760 fn rejects_empty_files() {
761 let options = GenerateWebfontsOptions {
762 dest: "artifacts".to_owned(),
763 files: vec![],
764 font_name: Some("iconfont".to_owned()),
765 ligature: Some(false),
766 types: Some(vec![FontType::Svg]),
767 ..Default::default()
768 };
769
770 let error = validate_generate_webfonts_options(&options).unwrap_err();
771
772 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
773 assert!(error.to_string().contains("\"options.files\" is empty."));
774 }
775
776 #[test]
777 fn rejects_empty_css_template() {
778 let options = GenerateWebfontsOptions {
779 css: Some(true),
780 css_template: Some(String::new()),
781 dest: "artifacts".to_owned(),
782 files: vec!["icon.svg".to_owned()],
783 html: Some(false),
784 font_name: Some("iconfont".to_owned()),
785 ligature: Some(false),
786 types: Some(vec![FontType::Svg]),
787 ..Default::default()
788 };
789
790 let error = resolve_generate_webfonts_options(options)
791 .err()
792 .expect("expected empty css template to fail");
793
794 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
795 assert!(
796 error
797 .to_string()
798 .contains("\"options.cssTemplate\" must not be empty.")
799 );
800 }
801
802 #[test]
803 fn rejects_empty_html_template() {
804 let options = GenerateWebfontsOptions {
805 css: Some(false),
806 dest: "artifacts".to_owned(),
807 files: vec!["icon.svg".to_owned()],
808 html: Some(true),
809 html_template: Some(String::new()),
810 font_name: Some("iconfont".to_owned()),
811 ligature: Some(false),
812 types: Some(vec![FontType::Svg]),
813 ..Default::default()
814 };
815
816 let error = resolve_generate_webfonts_options(options)
817 .err()
818 .expect("expected empty html template to fail");
819
820 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
821 assert!(
822 error
823 .to_string()
824 .contains("\"options.htmlTemplate\" must not be empty.")
825 );
826 }
827
828 #[test]
829 fn resolves_write_defaults_from_dest_and_font_name() {
830 let options = GenerateWebfontsOptions {
831 css: Some(false),
832 dest: "artifacts".to_owned(),
833 files: vec!["icon.svg".to_owned()],
834 html: Some(false),
835 font_name: Some("iconfont".to_owned()),
836 ligature: Some(false),
837 types: Some(vec![FontType::Svg]),
838 ..Default::default()
839 };
840
841 let resolved = resolve_generate_webfonts_options(options)
842 .expect("expected defaults to resolve successfully");
843
844 assert!(resolved.write_files);
845 assert_eq!(resolved.css_dest, "artifacts/iconfont.css");
846 assert_eq!(resolved.html_dest, "artifacts/iconfont.html");
847 }
848
849 #[test]
850 fn rejects_nonexistent_css_template_when_css_is_true() {
851 let error = validate_generate_webfonts_options(&GenerateWebfontsOptions {
852 css: Some(true),
853 css_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
854 dest: "artifacts".to_owned(),
855 files: vec!["icon.svg".to_owned()],
856 html: Some(false),
857 ..Default::default()
858 })
859 .unwrap_err();
860
861 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
862 assert!(error.to_string().contains("cssTemplate"));
863 }
864
865 #[test]
866 fn allows_nonexistent_css_template_when_css_is_false() {
867 validate_generate_webfonts_options(&GenerateWebfontsOptions {
868 css: Some(false),
869 css_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
870 dest: "artifacts".to_owned(),
871 files: vec!["icon.svg".to_owned()],
872 html: Some(false),
873 ..Default::default()
874 })
875 .expect("should allow nonexistent css template when css is false");
876 }
877
878 #[test]
879 fn rejects_nonexistent_html_template_when_html_is_true() {
880 let error = validate_generate_webfonts_options(&GenerateWebfontsOptions {
881 css: Some(false),
882 dest: "artifacts".to_owned(),
883 files: vec!["icon.svg".to_owned()],
884 html: Some(true),
885 html_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
886 ..Default::default()
887 })
888 .unwrap_err();
889
890 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
891 assert!(error.to_string().contains("htmlTemplate"));
892 }
893
894 #[test]
895 fn allows_nonexistent_html_template_when_html_is_false() {
896 validate_generate_webfonts_options(&GenerateWebfontsOptions {
897 css: Some(false),
898 dest: "artifacts".to_owned(),
899 files: vec!["icon.svg".to_owned()],
900 html: Some(false),
901 html_template: Some("/tmp/__nonexistent_template__.hbs".to_owned()),
902 ..Default::default()
903 })
904 .expect("should allow nonexistent html template when html is false");
905 }
906}