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