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