Skip to main content

cdx_core/asset/
font.rs

1//! Font asset types.
2
3use serde::{Deserialize, Serialize};
4
5use crate::DocumentId;
6
7/// Font format enumeration.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum FontFormat {
11    /// WOFF2 format (required, preferred).
12    Woff2,
13    /// WOFF format (required).
14    Woff,
15    /// TrueType format (optional).
16    Ttf,
17    /// OpenType format (optional).
18    Otf,
19}
20
21impl FontFormat {
22    /// Get the file extension for this format.
23    #[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    /// Get the MIME type for this format.
34    #[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/// Font weight values.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum FontWeight {
55    /// Thin (100).
56    Thin,
57    /// Extra Light (200).
58    ExtraLight,
59    /// Light (300).
60    Light,
61    /// Normal/Regular (400).
62    #[default]
63    Normal,
64    /// Medium (500).
65    Medium,
66    /// Semi Bold (600).
67    SemiBold,
68    /// Bold (700).
69    Bold,
70    /// Extra Bold (800).
71    ExtraBold,
72    /// Black (900).
73    Black,
74    /// Custom numeric weight.
75    #[serde(untagged)]
76    Custom(u16),
77}
78
79impl FontWeight {
80    /// Get the numeric weight value.
81    #[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/// Font style.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum FontStyle {
102    /// Normal (upright) style.
103    #[default]
104    Normal,
105    /// Italic style.
106    Italic,
107    /// Oblique style.
108    Oblique,
109}
110
111/// A font asset embedded in a Codex document.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct FontAsset {
115    /// Unique identifier for the font.
116    pub id: String,
117
118    /// Path within the archive (e.g., "assets/fonts/roboto-regular.woff2").
119    pub path: String,
120
121    /// Content hash for verification.
122    pub hash: DocumentId,
123
124    /// Font format.
125    pub format: FontFormat,
126
127    /// File size in bytes.
128    pub size: u64,
129
130    /// Font family name.
131    pub family: String,
132
133    /// Font weight.
134    #[serde(default)]
135    pub weight: FontWeight,
136
137    /// Font style.
138    #[serde(default)]
139    pub style: FontStyle,
140
141    /// Unicode range covered (CSS unicode-range format).
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub unicode_range: Option<String>,
144
145    /// Font feature settings.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub feature_settings: Option<String>,
148
149    /// Font variation settings (for variable fonts).
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub variation_settings: Option<String>,
152
153    /// License information.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub license: Option<String>,
156}
157
158impl FontAsset {
159    /// Create a new font asset.
160    #[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    /// Set the content hash.
182    #[must_use]
183    pub fn with_hash(mut self, hash: DocumentId) -> Self {
184        self.hash = hash;
185        self
186    }
187
188    /// Set the file size.
189    #[must_use]
190    pub const fn with_size(mut self, size: u64) -> Self {
191        self.size = size;
192        self
193    }
194
195    /// Set the font weight.
196    #[must_use]
197    pub const fn with_weight(mut self, weight: FontWeight) -> Self {
198        self.weight = weight;
199        self
200    }
201
202    /// Set the font style.
203    #[must_use]
204    pub const fn with_style(mut self, style: FontStyle) -> Self {
205        self.style = style;
206        self
207    }
208
209    /// Set the unicode range.
210    #[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    /// Set the license.
217    #[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    /// Set a custom path.
224    #[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}