use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use crate::input::{
GenerationOptions, LoadedSvgSource, ResolvedGenerationOptions, ResolvedVariants,
VariantMetadata, resolve_variant_metadata, validate_format_options, validate_glyph_names,
validate_variants,
};
use crate::{ColorGlyphSelection, FontType, FormatOptions, MissingGlyphOptions};
#[derive(Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SvgSource {
pub id: String,
pub name: String,
pub svg: String,
}
#[derive(Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SourceFontVariant {
pub name: String,
pub sources: Vec<SvgSource>,
pub weight: Option<u16>,
pub default: Option<bool>,
}
#[derive(Clone, Default, serde::Deserialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct GenerateWebfontsFromSourcesOptions {
pub sources: Vec<SvgSource>,
pub variants: Option<Vec<SourceFontVariant>>,
pub css_template: Option<String>,
pub html_template: Option<String>,
#[serde(
default,
deserialize_with = "crate::types::color::deserialize_selection"
)]
pub color_glyphs: Option<ColorGlyphSelection>,
pub color_slots: Option<HashMap<String, String>>,
pub ascent: Option<f64>,
pub center_horizontally: Option<bool>,
pub center_vertically: Option<bool>,
pub css: Option<bool>,
pub codepoints: Option<HashMap<String, u32>>,
pub css_fonts_url: Option<String>,
pub descent: Option<f64>,
pub fixed_width: Option<bool>,
pub format_options: Option<FormatOptions>,
pub html: Option<bool>,
pub incremental: Option<bool>,
pub font_height: Option<f64>,
pub font_name: Option<String>,
pub font_style: Option<String>,
pub font_weight: Option<String>,
pub ligature: Option<bool>,
pub missing_glyphs: Option<MissingGlyphOptions>,
pub normalize: Option<bool>,
pub order: Option<Vec<FontType>>,
pub optimize_output: Option<bool>,
pub preserve_aspect_ratio: Option<bool>,
pub round: Option<f64>,
pub start_codepoint: Option<u32>,
pub template_options: Option<serde_json::Map<String, serde_json::Value>>,
pub types: Option<Vec<FontType>>,
pub variant_class_prefix: Option<String>,
}
#[derive(Clone)]
pub(super) struct ResolvedSourceOptions {
pub common: ResolvedGenerationOptions,
pub variants: Option<ResolvedVariants>,
pub sources: Vec<Vec<LoadedSvgSource>>,
pub css_template: Option<Arc<str>>,
pub html_template: Option<Arc<str>>,
}
pub(super) fn resolve(
options: GenerateWebfontsFromSourcesOptions,
) -> std::io::Result<ResolvedSourceOptions> {
let settings = GenerationOptions {
color_glyphs: options.color_glyphs,
color_slots: options.color_slots,
ascent: options.ascent,
center_horizontally: options.center_horizontally,
center_vertically: options.center_vertically,
css: options.css,
codepoints: options.codepoints,
css_fonts_url: options.css_fonts_url,
descent: options.descent,
fixed_width: options.fixed_width,
format_options: options.format_options,
html: options.html,
incremental: options.incremental,
font_height: options.font_height,
font_name: options.font_name,
font_style: options.font_style,
font_weight: options.font_weight,
ligature: options.ligature,
missing_glyphs: options.missing_glyphs,
normalize: options.normalize,
order: options.order,
optimize_output: options.optimize_output,
preserve_aspect_ratio: options.preserve_aspect_ratio,
round: options.round,
start_codepoint: options.start_codepoint,
template_options: options.template_options,
types: options.types,
variant_class_prefix: options.variant_class_prefix,
};
let (variants, designs) = if let Some(variants) = options.variants {
if !options.sources.is_empty() {
return Err(invalid(
"options.sources must be empty when variants are supplied",
));
}
let metadata = variants
.iter()
.map(|variant| VariantMetadata {
name: &variant.name,
weight: variant.weight,
default: variant.default,
source_count: variant.sources.len(),
})
.collect::<Vec<_>>();
validate_variants(&settings, &metadata, "sources")?;
validate_format_options(settings.format_options.as_ref())?;
let resolved = resolve_variant_metadata(
&metadata,
variants
.iter()
.map(|variant| {
variant
.sources
.iter()
.map(|source| source.id.clone())
.collect()
})
.collect(),
settings.variant_class_prefix.as_deref().unwrap_or("icon--"),
)?;
(
Some(resolved),
variants
.into_iter()
.map(|variant| variant.sources)
.collect(),
)
} else {
if options.sources.is_empty() {
return Err(invalid("options.sources is empty"));
}
settings.validate_without_variants()?;
validate_format_options(settings.format_options.as_ref())?;
(None, vec![options.sources])
};
let mut common = settings.resolve(variants.is_some())?;
common.filesystem_free = true;
for name in common.explicit_codepoints.keys() {
validate_name(name)?;
}
let sources = normalize_sources(designs)?;
Ok(ResolvedSourceOptions {
common,
variants,
sources,
css_template: options.css_template.map(Arc::from),
html_template: options.html_template.map(Arc::from),
})
}
fn normalize_sources(designs: Vec<Vec<SvgSource>>) -> std::io::Result<Vec<Vec<LoadedSvgSource>>> {
let mut definitions = HashMap::<String, LoadedSvgSource>::new();
designs
.into_iter()
.map(|sources| {
let mut ids = HashSet::new();
let sources = sources
.into_iter()
.map(|source| {
if source.id.is_empty() || !ids.insert(source.id.clone()) {
return Err(invalid(format!(
"Source ID {:?} must be nonempty and unique within a design",
source.id
)));
}
validate_name(&source.name)?;
if let Some(existing) = definitions.get(&source.id) {
if existing.glyph_name != source.name
|| existing.contents.as_ref() != source.svg
{
return Err(invalid(format!(
"Conflicting definitions for source ID {:?}",
source.id
)));
}
return Ok(existing.clone());
}
let loaded = LoadedSvgSource {
id: source.id,
glyph_name: source.name,
contents: source.svg.into(),
};
definitions.insert(loaded.id.clone(), loaded.clone());
Ok(loaded)
})
.collect::<std::io::Result<Vec<_>>>()?;
validate_glyph_names(&sources)?;
Ok(sources)
})
.collect()
}
pub(super) fn invalid(message: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Error {
Error::new(ErrorKind::InvalidInput, message)
}
pub(super) fn validate_name(name: &str) -> std::io::Result<()> {
if name.is_empty()
|| !name
.chars()
.all(|character| character.is_alphanumeric() || matches!(character, '-' | '_'))
{
return Err(invalid(format!(
"Glyph name {name:?} must contain only Unicode letters/numbers, hyphens or underscores"
)));
}
Ok(())
}