1use serde::{Deserialize, Serialize};
4
5use crate::DocumentId;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum FontFormat {
11 Woff2,
13 Woff,
15 Ttf,
17 Otf,
19}
20
21impl FontFormat {
22 #[must_use]
24 pub const fn extension(&self) -> &'static str {
25 match self {
26 Self::Woff2 => "woff2",
27 Self::Woff => "woff",
28 Self::Ttf => "ttf",
29 Self::Otf => "otf",
30 }
31 }
32
33 #[must_use]
35 pub const fn mime_type(&self) -> &'static str {
36 match self {
37 Self::Woff2 => "font/woff2",
38 Self::Woff => "font/woff",
39 Self::Ttf => "font/ttf",
40 Self::Otf => "font/otf",
41 }
42 }
43}
44
45impl std::fmt::Display for FontFormat {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.extension())
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum FontWeight {
55 Thin,
57 ExtraLight,
59 Light,
61 #[default]
63 Normal,
64 Medium,
66 SemiBold,
68 Bold,
70 ExtraBold,
72 Black,
74 #[serde(untagged)]
76 Custom(u16),
77}
78
79impl FontWeight {
80 #[must_use]
82 pub const fn value(&self) -> u16 {
83 match self {
84 Self::Thin => 100,
85 Self::ExtraLight => 200,
86 Self::Light => 300,
87 Self::Normal => 400,
88 Self::Medium => 500,
89 Self::SemiBold => 600,
90 Self::Bold => 700,
91 Self::ExtraBold => 800,
92 Self::Black => 900,
93 Self::Custom(v) => *v,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum FontStyle {
102 #[default]
104 Normal,
105 Italic,
107 Oblique,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct FontAsset {
115 pub id: String,
117
118 pub path: String,
120
121 pub hash: DocumentId,
123
124 pub format: FontFormat,
126
127 pub size: u64,
129
130 pub family: String,
132
133 #[serde(default)]
135 pub weight: FontWeight,
136
137 #[serde(default)]
139 pub style: FontStyle,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub unicode_range: Option<String>,
144
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub feature_settings: Option<String>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub variation_settings: Option<String>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub license: Option<String>,
156}
157
158impl FontAsset {
159 #[must_use]
161 pub fn new(id: impl Into<String>, family: impl Into<String>, format: FontFormat) -> Self {
162 let id = id.into();
163 let family = family.into();
164 let path = format!("assets/fonts/{}.{}", id, format.extension());
165 Self {
166 id,
167 path,
168 hash: DocumentId::pending(),
169 format,
170 size: 0,
171 family,
172 weight: FontWeight::Normal,
173 style: FontStyle::Normal,
174 unicode_range: None,
175 feature_settings: None,
176 variation_settings: None,
177 license: None,
178 }
179 }
180
181 #[must_use]
183 pub fn with_hash(mut self, hash: DocumentId) -> Self {
184 self.hash = hash;
185 self
186 }
187
188 #[must_use]
190 pub const fn with_size(mut self, size: u64) -> Self {
191 self.size = size;
192 self
193 }
194
195 #[must_use]
197 pub const fn with_weight(mut self, weight: FontWeight) -> Self {
198 self.weight = weight;
199 self
200 }
201
202 #[must_use]
204 pub const fn with_style(mut self, style: FontStyle) -> Self {
205 self.style = style;
206 self
207 }
208
209 #[must_use]
211 pub fn with_unicode_range(mut self, range: impl Into<String>) -> Self {
212 self.unicode_range = Some(range.into());
213 self
214 }
215
216 #[must_use]
218 pub fn with_license(mut self, license: impl Into<String>) -> Self {
219 self.license = Some(license.into());
220 self
221 }
222
223 #[must_use]
225 pub fn with_path(mut self, path: impl Into<String>) -> Self {
226 self.path = path.into();
227 self
228 }
229}
230
231impl super::Asset for FontAsset {
232 fn id(&self) -> &str {
233 &self.id
234 }
235
236 fn path(&self) -> &str {
237 &self.path
238 }
239
240 fn hash(&self) -> &DocumentId {
241 &self.hash
242 }
243
244 fn size(&self) -> u64 {
245 self.size
246 }
247
248 fn mime_type(&self) -> &str {
249 self.format.mime_type()
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_font_format_extension() {
259 assert_eq!(FontFormat::Woff2.extension(), "woff2");
260 assert_eq!(FontFormat::Woff.extension(), "woff");
261 assert_eq!(FontFormat::Ttf.extension(), "ttf");
262 assert_eq!(FontFormat::Otf.extension(), "otf");
263 }
264
265 #[test]
266 fn test_font_format_mime_type() {
267 assert_eq!(FontFormat::Woff2.mime_type(), "font/woff2");
268 assert_eq!(FontFormat::Ttf.mime_type(), "font/ttf");
269 }
270
271 #[test]
272 fn test_font_weight_value() {
273 assert_eq!(FontWeight::Normal.value(), 400);
274 assert_eq!(FontWeight::Bold.value(), 700);
275 assert_eq!(FontWeight::Custom(450).value(), 450);
276 }
277
278 #[test]
279 fn test_font_asset_new() {
280 let font = FontAsset::new("roboto-regular", "Roboto", FontFormat::Woff2);
281 assert_eq!(font.id, "roboto-regular");
282 assert_eq!(font.family, "Roboto");
283 assert_eq!(font.path, "assets/fonts/roboto-regular.woff2");
284 assert_eq!(font.format, FontFormat::Woff2);
285 }
286
287 #[test]
288 fn test_font_asset_builder() {
289 let font = FontAsset::new("roboto-bold", "Roboto", FontFormat::Woff2)
290 .with_weight(FontWeight::Bold)
291 .with_style(FontStyle::Normal)
292 .with_size(32768)
293 .with_license("Apache-2.0");
294
295 assert_eq!(font.weight, FontWeight::Bold);
296 assert_eq!(font.style, FontStyle::Normal);
297 assert_eq!(font.size, 32768);
298 assert_eq!(font.license, Some("Apache-2.0".to_string()));
299 }
300
301 #[test]
302 fn test_font_asset_serialization() {
303 let font = FontAsset::new("test-font", "Test Family", FontFormat::Woff2)
304 .with_weight(FontWeight::Bold);
305
306 let json = serde_json::to_string_pretty(&font).unwrap();
307 assert!(json.contains(r#""family": "Test Family""#));
308 assert!(json.contains(r#""format": "woff2""#));
309
310 let deserialized: FontAsset = serde_json::from_str(&json).unwrap();
311 assert_eq!(deserialized.family, font.family);
312 assert_eq!(deserialized.weight, font.weight);
313 }
314}