1
//! CSS properties for fonts, such as font-family, font-size, font-weight, and font-style.
2
//!
3
//! Also contains `FontRef` (reference-counted handle to parsed font data),
4
//! `FontMetrics` (OpenType font metrics from head/hhea/os2 tables), and
5
//! `Panose` (font classification).
6

            
7
use alloc::{
8
    boxed::Box,
9
    string::{String, ToString},
10
    vec::Vec,
11
};
12
use core::{
13
    cmp::Ordering,
14
    ffi::c_void,
15
    fmt,
16
    hash::{Hash, Hasher},
17
    num::ParseIntError,
18
    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
19
};
20

            
21
#[cfg(feature = "parser")]
22
use crate::props::basic::parse::{strip_quotes, UnclosedQuotesError};
23
use crate::system::SystemFontType;
24
use crate::{
25
    corety::{AzString, U8Vec},
26
    format_rust_code::{FormatAsRustCode, GetHash},
27
    props::{
28
        basic::{
29
            error::{InvalidValueErr, InvalidValueErrOwned},
30
            pixel::{
31
                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
32
                PixelValue,
33
            },
34
        },
35
        formatter::PrintAsCssValue,
36
    },
37
};
38

            
39
// --- Font Weight ---
40

            
41
/// Represents the `font-weight` property.
42
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43
#[repr(C)]
44
#[derive(Default)]
45
pub enum StyleFontWeight {
46
    Lighter,
47
    W100,
48
    W200,
49
    W300,
50
    #[default]
51
    Normal,
52
    W500,
53
    W600,
54
    Bold,
55
    W800,
56
    W900,
57
    Bolder,
58
}
59

            
60

            
61
impl PrintAsCssValue for StyleFontWeight {
62
    fn print_as_css_value(&self) -> String {
63
        match self {
64
            StyleFontWeight::Lighter => "lighter".to_string(),
65
            StyleFontWeight::W100 => "100".to_string(),
66
            StyleFontWeight::W200 => "200".to_string(),
67
            StyleFontWeight::W300 => "300".to_string(),
68
            StyleFontWeight::Normal => "normal".to_string(),
69
            StyleFontWeight::W500 => "500".to_string(),
70
            StyleFontWeight::W600 => "600".to_string(),
71
            StyleFontWeight::Bold => "bold".to_string(),
72
            StyleFontWeight::W800 => "800".to_string(),
73
            StyleFontWeight::W900 => "900".to_string(),
74
            StyleFontWeight::Bolder => "bolder".to_string(),
75
        }
76
    }
77
}
78

            
79
impl crate::format_rust_code::FormatAsRustCode for StyleFontWeight {
80
    fn format_as_rust_code(&self, _tabs: usize) -> String {
81
        use StyleFontWeight::*;
82
        format!(
83
            "StyleFontWeight::{}",
84
            match self {
85
                Lighter => "Lighter",
86
                W100 => "W100",
87
                W200 => "W200",
88
                W300 => "W300",
89
                Normal => "Normal",
90
                W500 => "W500",
91
                W600 => "W600",
92
                Bold => "Bold",
93
                W800 => "W800",
94
                W900 => "W900",
95
                Bolder => "Bolder",
96
            }
97
        )
98
    }
99
}
100

            
101
// --- Font Style ---
102

            
103
/// Represents the `font-style` property.
104
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
105
#[repr(C)]
106
#[derive(Default)]
107
pub enum StyleFontStyle {
108
    #[default]
109
    Normal,
110
    Italic,
111
    Oblique,
112
}
113

            
114

            
115
impl PrintAsCssValue for StyleFontStyle {
116
    fn print_as_css_value(&self) -> String {
117
        match self {
118
            StyleFontStyle::Normal => "normal".to_string(),
119
            StyleFontStyle::Italic => "italic".to_string(),
120
            StyleFontStyle::Oblique => "oblique".to_string(),
121
        }
122
    }
123
}
124

            
125
impl crate::format_rust_code::FormatAsRustCode for StyleFontStyle {
126
    fn format_as_rust_code(&self, _tabs: usize) -> String {
127
        use StyleFontStyle::*;
128
        format!(
129
            "StyleFontStyle::{}",
130
            match self {
131
                Normal => "Normal",
132
                Italic => "Italic",
133
                Oblique => "Oblique",
134
            }
135
        )
136
    }
137
}
138

            
139
// --- Font Size ---
140

            
141
/// Represents a `font-size` attribute
142
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
143
#[repr(C)]
144
pub struct StyleFontSize {
145
    pub inner: PixelValue,
146
}
147

            
148
impl Default for StyleFontSize {
149
    fn default() -> Self {
150
        Self {
151
            // Default font size is 12pt, a common default for print and web.
152
            inner: PixelValue::const_pt(12),
153
        }
154
    }
155
}
156

            
157
impl_pixel_value!(StyleFontSize);
158
impl PrintAsCssValue for StyleFontSize {
159
    fn print_as_css_value(&self) -> String {
160
        format!("{}", self.inner)
161
    }
162
}
163

            
164
// --- Font Resource Management ---
165

            
166
/// Callback type for FontRef destructor - must be extern "C" for FFI safety
167
pub type FontRefDestructorCallbackType = extern "C" fn(*mut c_void);
168

            
169
/// FontRef is a reference-counted pointer to a parsed font.
170
/// It holds a *const c_void that points to the actual parsed font data
171
/// (typically a ParsedFont from the layout crate).
172
///
173
/// The parsed data is managed via atomic reference counting, allowing
174
/// safe sharing across threads without duplicating the font data.
175
#[repr(C)]
176
pub struct FontRef {
177
    /// Pointer to the parsed font data (e.g., ParsedFont)
178
    pub parsed: *const c_void,
179
    /// Reference counter for memory management
180
    pub copies: *const AtomicUsize,
181
    /// Whether to run the destructor on drop
182
    pub run_destructor: bool,
183
    /// Destructor function for the parsed data
184
    pub parsed_destructor: FontRefDestructorCallbackType,
185
}
186

            
187
impl fmt::Debug for FontRef {
188
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189
        write!(f, "FontRef(0x{:x}", self.parsed as usize)?;
190
        if let Some(c) = unsafe { self.copies.as_ref() } {
191
            write!(f, ", copies: {})", c.load(AtomicOrdering::SeqCst))?;
192
        } else {
193
            write!(f, ")")?;
194
        }
195
        Ok(())
196
    }
197
}
198

            
199
impl FontRef {
200
    /// Create a new FontRef from parsed font data
201
    ///
202
    /// # Arguments
203
    /// * `parsed` - Pointer to parsed font data (e.g., Arc::into_raw(Arc::new(ParsedFont)))
204
    /// * `destructor` - Function to clean up the parsed data
205
25410
    pub fn new(parsed: *const c_void, destructor: FontRefDestructorCallbackType) -> Self {
206
25410
        Self {
207
25410
            parsed,
208
25410
            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
209
25410
            run_destructor: true,
210
25410
            parsed_destructor: destructor,
211
25410
        }
212
25410
    }
213

            
214
    /// Get a raw pointer to the parsed font data
215
    #[inline]
216
123585
    pub fn get_parsed(&self) -> *const c_void {
217
123585
        self.parsed
218
123585
    }
219
}
220
impl_option!(
221
    FontRef,
222
    OptionFontRef,
223
    copy = false,
224
    [Debug, Clone, PartialEq, Eq, Hash]
225
);
226
unsafe impl Send for FontRef {}
227
unsafe impl Sync for FontRef {}
228
impl PartialEq for FontRef {
229
    fn eq(&self, rhs: &Self) -> bool {
230
        std::ptr::eq(self.parsed, rhs.parsed)
231
    }
232
}
233
impl PartialOrd for FontRef {
234
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
235
        Some((self.parsed as usize).cmp(&(other.parsed as usize)))
236
    }
237
}
238
impl Ord for FontRef {
239
    fn cmp(&self, other: &Self) -> Ordering {
240
        (self.parsed as usize).cmp(&(other.parsed as usize))
241
    }
242
}
243
impl Eq for FontRef {}
244
impl Hash for FontRef {
245
    fn hash<H: Hasher>(&self, state: &mut H) {
246
        (self.parsed as usize).hash(state);
247
    }
248
}
249
impl Clone for FontRef {
250
131460
    fn clone(&self) -> Self {
251
131460
        if !self.copies.is_null() {
252
131460
            unsafe {
253
131460
                (*self.copies).fetch_add(1, AtomicOrdering::SeqCst);
254
131460
            }
255
        }
256
131460
        Self {
257
131460
            parsed: self.parsed,
258
131460
            copies: self.copies,
259
131460
            run_destructor: self.run_destructor,
260
131460
            parsed_destructor: self.parsed_destructor,
261
131460
        }
262
131460
    }
263
}
264
impl Drop for FontRef {
265
156870
    fn drop(&mut self) {
266
156870
        if self.run_destructor && !self.copies.is_null()
267
156870
            && unsafe { (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst) } == 1 {
268
25410
                unsafe {
269
25410
                    (self.parsed_destructor)(self.parsed as *mut c_void);
270
25410
                    let _ = Box::from_raw(self.copies as *mut AtomicUsize);
271
25410
                }
272
131460
            }
273
156870
    }
274
}
275

            
276
// --- Font Family ---
277

            
278
/// Represents a `font-family` attribute.
279
/// 
280
/// Can be:
281
/// - `System(AzString)`: A named font family (e.g., "Arial", "Times New Roman")
282
/// - `SystemType(SystemFontType)`: A semantic system font type (e.g., `system:ui`, `system:monospace`)
283
/// - `File(AzString)`: A font loaded from a file URL
284
/// - `Ref(FontRef)`: A reference to a pre-loaded font
285
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
286
#[repr(C, u8)]
287
pub enum StyleFontFamily {
288
    /// Named font family (e.g., "Arial", "Times New Roman", "monospace")
289
    System(AzString),
290
    /// Semantic system font type (e.g., `system:ui`, `system:monospace:bold`)
291
    /// Resolved at runtime based on platform and accessibility settings
292
    SystemType(SystemFontType),
293
    /// Font loaded from a file URL
294
    File(AzString),
295
    /// Reference to a pre-loaded font
296
    Ref(FontRef),
297
}
298

            
299
impl_option!(
300
    StyleFontFamily,
301
    OptionStyleFontFamily,
302
    copy = false,
303
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
304
);
305

            
306
impl StyleFontFamily {
307
43970
    pub fn as_string(&self) -> String {
308
43970
        match &self {
309
43970
            StyleFontFamily::System(s) => {
310
43970
                let owned = s.clone().into_library_owned_string();
311
43970
                if owned.contains(char::is_whitespace) {
312
                    format!("\"{}\"", owned)
313
                } else {
314
43970
                    owned
315
                }
316
            }
317
            StyleFontFamily::SystemType(st) => st.as_css_str().to_string(),
318
            StyleFontFamily::File(s) => format!("url({})", s.clone().into_library_owned_string()),
319
            StyleFontFamily::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
320
        }
321
43970
    }
322
}
323

            
324
impl_vec!(StyleFontFamily, StyleFontFamilyVec, StyleFontFamilyVecDestructor, StyleFontFamilyVecDestructorType, StyleFontFamilyVecSlice, OptionStyleFontFamily);
325
impl_vec_clone!(
326
    StyleFontFamily,
327
    StyleFontFamilyVec,
328
    StyleFontFamilyVecDestructor
329
);
330
impl_vec_debug!(StyleFontFamily, StyleFontFamilyVec);
331
impl_vec_eq!(StyleFontFamily, StyleFontFamilyVec);
332
impl_vec_ord!(StyleFontFamily, StyleFontFamilyVec);
333
impl_vec_hash!(StyleFontFamily, StyleFontFamilyVec);
334
impl_vec_partialeq!(StyleFontFamily, StyleFontFamilyVec);
335
impl_vec_partialord!(StyleFontFamily, StyleFontFamilyVec);
336

            
337
impl PrintAsCssValue for StyleFontFamilyVec {
338
    fn print_as_css_value(&self) -> String {
339
        self.iter()
340
            .map(|f| f.as_string())
341
            .collect::<Vec<_>>()
342
            .join(", ")
343
    }
344
}
345

            
346
// Formatting to Rust code for StyleFontFamilyVec
347
impl crate::format_rust_code::FormatAsRustCode for StyleFontFamilyVec {
348
    fn format_as_rust_code(&self, _tabs: usize) -> String {
349
        format!(
350
            "StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_{}_ITEMS)",
351
            self.get_hash()
352
        )
353
    }
354
}
355

            
356
// --- PARSERS ---
357

            
358
// -- Font Weight Parser --
359

            
360
#[derive(Clone, PartialEq)]
361
pub enum CssFontWeightParseError<'a> {
362
    InvalidValue(InvalidValueErr<'a>),
363
    InvalidNumber(ParseIntError),
364
}
365

            
366
// Formatting to Rust code for StyleFontFamily
367
impl crate::format_rust_code::FormatAsRustCode for StyleFontFamily {
368
    fn format_as_rust_code(&self, _tabs: usize) -> String {
369
        match self {
370
            StyleFontFamily::System(id) => {
371
                format!("StyleFontFamily::System(STRING_{})", id.get_hash())
372
            }
373
            StyleFontFamily::SystemType(st) => {
374
                format!("StyleFontFamily::SystemType(SystemFontType::{:?})", st)
375
            }
376
            StyleFontFamily::File(path) => {
377
                format!("StyleFontFamily::File(STRING_{})", path.get_hash())
378
            }
379
            StyleFontFamily::Ref(font_ref) => {
380
                format!("StyleFontFamily::Ref({:0x})", font_ref.parsed as usize)
381
            }
382
        }
383
    }
384
}
385
impl_debug_as_display!(CssFontWeightParseError<'a>);
386
impl_display! { CssFontWeightParseError<'a>, {
387
    InvalidValue(e) => format!("Invalid font-weight keyword: \"{}\"", e.0),
388
    InvalidNumber(e) => format!("Invalid font-weight number: {}", e),
389
}}
390
impl<'a> From<InvalidValueErr<'a>> for CssFontWeightParseError<'a> {
391
4
    fn from(e: InvalidValueErr<'a>) -> Self {
392
4
        CssFontWeightParseError::InvalidValue(e)
393
4
    }
394
}
395
impl<'a> From<ParseIntError> for CssFontWeightParseError<'a> {
396
    fn from(e: ParseIntError) -> Self {
397
        CssFontWeightParseError::InvalidNumber(e)
398
    }
399
}
400

            
401
#[derive(Debug, Clone, PartialEq)]
402
#[repr(C, u8)]
403
pub enum CssFontWeightParseErrorOwned {
404
    InvalidValue(InvalidValueErrOwned),
405
    InvalidNumber(crate::props::basic::error::ParseIntError),
406
}
407

            
408
impl<'a> CssFontWeightParseError<'a> {
409
    pub fn to_contained(&self) -> CssFontWeightParseErrorOwned {
410
        match self {
411
            Self::InvalidValue(e) => CssFontWeightParseErrorOwned::InvalidValue(e.to_contained()),
412
            Self::InvalidNumber(e) => CssFontWeightParseErrorOwned::InvalidNumber(e.clone().into()),
413
        }
414
    }
415
}
416

            
417
impl CssFontWeightParseErrorOwned {
418
    pub fn to_shared<'a>(&'a self) -> CssFontWeightParseError<'a> {
419
        match self {
420
            Self::InvalidValue(e) => CssFontWeightParseError::InvalidValue(e.to_shared()),
421
            Self::InvalidNumber(e) => CssFontWeightParseError::InvalidNumber(e.to_std()),
422
        }
423
    }
424
}
425

            
426
#[cfg(feature = "parser")]
427
61
pub fn parse_font_weight<'a>(
428
61
    input: &'a str,
429
61
) -> Result<StyleFontWeight, CssFontWeightParseError<'a>> {
430
61
    let input = input.trim();
431
61
    match input {
432
61
        "lighter" => Ok(StyleFontWeight::Lighter),
433
60
        "normal" => Ok(StyleFontWeight::Normal),
434
59
        "bold" => Ok(StyleFontWeight::Bold),
435
9
        "bolder" => Ok(StyleFontWeight::Bolder),
436
8
        "100" => Ok(StyleFontWeight::W100),
437
7
        "200" => Ok(StyleFontWeight::W200),
438
7
        "300" => Ok(StyleFontWeight::W300),
439
7
        "400" => Ok(StyleFontWeight::Normal),
440
6
        "500" => Ok(StyleFontWeight::W500),
441
6
        "600" => Ok(StyleFontWeight::W600),
442
6
        "700" => Ok(StyleFontWeight::Bold),
443
5
        "800" => Ok(StyleFontWeight::W800),
444
5
        "900" => Ok(StyleFontWeight::W900),
445
4
        _ => Err(InvalidValueErr(input).into()),
446
    }
447
61
}
448

            
449
// -- Font Style Parser --
450

            
451
#[derive(Clone, PartialEq)]
452
pub enum CssFontStyleParseError<'a> {
453
    InvalidValue(InvalidValueErr<'a>),
454
}
455
impl_debug_as_display!(CssFontStyleParseError<'a>);
456
impl_display! { CssFontStyleParseError<'a>, {
457
    InvalidValue(e) => format!("Invalid font-style: \"{}\"", e.0),
458
}}
459
impl_from! { InvalidValueErr<'a>, CssFontStyleParseError::InvalidValue }
460

            
461
#[derive(Debug, Clone, PartialEq)]
462
#[repr(C, u8)]
463
pub enum CssFontStyleParseErrorOwned {
464
    InvalidValue(InvalidValueErrOwned),
465
}
466
impl<'a> CssFontStyleParseError<'a> {
467
    pub fn to_contained(&self) -> CssFontStyleParseErrorOwned {
468
        match self {
469
            Self::InvalidValue(e) => CssFontStyleParseErrorOwned::InvalidValue(e.to_contained()),
470
        }
471
    }
472
}
473
impl CssFontStyleParseErrorOwned {
474
    pub fn to_shared<'a>(&'a self) -> CssFontStyleParseError<'a> {
475
        match self {
476
            Self::InvalidValue(e) => CssFontStyleParseError::InvalidValue(e.to_shared()),
477
        }
478
    }
479
}
480

            
481
#[cfg(feature = "parser")]
482
5
pub fn parse_font_style<'a>(input: &'a str) -> Result<StyleFontStyle, CssFontStyleParseError<'a>> {
483
5
    match input.trim() {
484
5
        "normal" => Ok(StyleFontStyle::Normal),
485
4
        "italic" => Ok(StyleFontStyle::Italic),
486
2
        "oblique" => Ok(StyleFontStyle::Oblique),
487
1
        other => Err(InvalidValueErr(other).into()),
488
    }
489
5
}
490

            
491
// -- Font Size Parser --
492

            
493
#[derive(Clone, PartialEq)]
494
pub enum CssStyleFontSizeParseError<'a> {
495
    PixelValue(CssPixelValueParseError<'a>),
496
}
497
impl_debug_as_display!(CssStyleFontSizeParseError<'a>);
498
impl_display! { CssStyleFontSizeParseError<'a>, {
499
    PixelValue(e) => format!("Invalid font-size: {}", e),
500
}}
501
impl_from! { CssPixelValueParseError<'a>, CssStyleFontSizeParseError::PixelValue }
502

            
503
#[derive(Debug, Clone, PartialEq)]
504
#[repr(C, u8)]
505
pub enum CssStyleFontSizeParseErrorOwned {
506
    PixelValue(CssPixelValueParseErrorOwned),
507
}
508
impl<'a> CssStyleFontSizeParseError<'a> {
509
    pub fn to_contained(&self) -> CssStyleFontSizeParseErrorOwned {
510
        match self {
511
            Self::PixelValue(e) => CssStyleFontSizeParseErrorOwned::PixelValue(e.to_contained()),
512
        }
513
    }
514
}
515
impl CssStyleFontSizeParseErrorOwned {
516
    pub fn to_shared<'a>(&'a self) -> CssStyleFontSizeParseError<'a> {
517
        match self {
518
            Self::PixelValue(e) => CssStyleFontSizeParseError::PixelValue(e.to_shared()),
519
        }
520
    }
521
}
522

            
523
#[cfg(feature = "parser")]
524
1479
pub fn parse_style_font_size<'a>(
525
1479
    input: &'a str,
526
1479
) -> Result<StyleFontSize, CssStyleFontSizeParseError<'a>> {
527
    Ok(StyleFontSize {
528
1479
        inner: parse_pixel_value(input)?,
529
    })
530
1479
}
531

            
532
// -- Font Family Parser --
533

            
534
#[derive(PartialEq, Clone)]
535
pub enum CssStyleFontFamilyParseError<'a> {
536
    InvalidStyleFontFamily(&'a str),
537
    UnclosedQuotes(UnclosedQuotesError<'a>),
538
}
539
impl_debug_as_display!(CssStyleFontFamilyParseError<'a>);
540
impl_display! { CssStyleFontFamilyParseError<'a>, {
541
    InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val),
542
    UnclosedQuotes(val) => format!("Unclosed quotes in font-family: \"{}\"", val.0),
543
}}
544
impl<'a> From<UnclosedQuotesError<'a>> for CssStyleFontFamilyParseError<'a> {
545
    fn from(err: UnclosedQuotesError<'a>) -> Self {
546
        CssStyleFontFamilyParseError::UnclosedQuotes(err)
547
    }
548
}
549

            
550
#[derive(Debug, Clone, PartialEq)]
551
#[repr(C, u8)]
552
pub enum CssStyleFontFamilyParseErrorOwned {
553
    InvalidStyleFontFamily(AzString),
554
    UnclosedQuotes(AzString),
555
}
556
impl<'a> CssStyleFontFamilyParseError<'a> {
557
    pub fn to_contained(&self) -> CssStyleFontFamilyParseErrorOwned {
558
        match self {
559
            CssStyleFontFamilyParseError::InvalidStyleFontFamily(s) => {
560
                CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily(s.to_string().into())
561
            }
562
            CssStyleFontFamilyParseError::UnclosedQuotes(e) => {
563
                CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(e.0.to_string().into())
564
            }
565
        }
566
    }
567
}
568
impl CssStyleFontFamilyParseErrorOwned {
569
    pub fn to_shared<'a>(&'a self) -> CssStyleFontFamilyParseError<'a> {
570
        match self {
571
            CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily(s) => {
572
                CssStyleFontFamilyParseError::InvalidStyleFontFamily(s)
573
            }
574
            CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(s) => {
575
                CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError(s))
576
            }
577
        }
578
    }
579
}
580

            
581
#[cfg(feature = "parser")]
582
1141
pub fn parse_style_font_family<'a>(
583
1141
    input: &'a str,
584
1141
) -> Result<StyleFontFamilyVec, CssStyleFontFamilyParseError<'a>> {
585
1141
    let multiple_fonts = input.split(',');
586
1141
    let mut fonts = Vec::with_capacity(1);
587

            
588
2552
    for font in multiple_fonts {
589
1411
        let font = font.trim();
590
        
591
        // Check for system font type syntax: system:ui, system:monospace:bold, etc.
592
1411
        if font.starts_with("system:") {
593
16
            if let Some(system_type) = SystemFontType::from_css_str(font) {
594
15
                fonts.push(StyleFontFamily::SystemType(system_type));
595
15
                continue;
596
1
            }
597
            // Invalid system font type, fall through to treat as regular font name
598
1395
        }
599
        
600
1396
        if let Ok(stripped) = strip_quotes(font) {
601
142
            fonts.push(StyleFontFamily::System(stripped.0.to_string().into()));
602
1254
        } else {
603
1254
            // It could be an unquoted font name like `Times New Roman`.
604
1254
            fonts.push(StyleFontFamily::System(font.to_string().into()));
605
1254
        }
606
    }
607

            
608
1141
    Ok(fonts.into())
609
1141
}
610

            
611
// --- Font Metrics ---
612

            
613
use crate::corety::{OptionI16, OptionU16, OptionU32};
614

            
615
/// PANOSE classification values for font identification (10 bytes).
616
/// See https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose
617
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
618
#[repr(C)]
619
#[derive(Default)]
620
pub struct Panose {
621
    pub family_type: u8,
622
    pub serif_style: u8,
623
    pub weight: u8,
624
    pub proportion: u8,
625
    pub contrast: u8,
626
    pub stroke_variation: u8,
627
    pub arm_style: u8,
628
    pub letterform: u8,
629
    pub midline: u8,
630
    pub x_height: u8,
631
}
632

            
633

            
634
impl Panose {
635
    pub const fn zero() -> Self {
636
        Panose {
637
            family_type: 0,
638
            serif_style: 0,
639
            weight: 0,
640
            proportion: 0,
641
            contrast: 0,
642
            stroke_variation: 0,
643
            arm_style: 0,
644
            letterform: 0,
645
            midline: 0,
646
            x_height: 0,
647
        }
648
    }
649
}
650

            
651
/// Font metrics structure containing all font-related measurements from
652
/// the font file tables (head, hhea, and os/2 tables).
653
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
654
#[repr(C)]
655
pub struct FontMetrics {
656
    // os/2 version 1 table (u32 fields - align 4, placed first)
657
    pub ul_code_page_range1: OptionU32,
658
    pub ul_code_page_range2: OptionU32,
659

            
660
    // os/2 table (u32 fields)
661
    pub ul_unicode_range1: u32,
662
    pub ul_unicode_range2: u32,
663
    pub ul_unicode_range3: u32,
664
    pub ul_unicode_range4: u32,
665
    pub ach_vend_id: u32,
666

            
667
    // os/2 version 0 table (Option<i16>/Option<u16> - align 2)
668
    pub s_typo_ascender: OptionI16,
669
    pub s_typo_descender: OptionI16,
670
    pub s_typo_line_gap: OptionI16,
671
    pub us_win_ascent: OptionU16,
672
    pub us_win_descent: OptionU16,
673

            
674
    // +spec:font-metrics:d3b654 - cap-height and x-height metrics for visual text centering (leading-trim)
675
    // os/2 version 2 table
676
    pub sx_height: OptionI16,
677
    pub s_cap_height: OptionI16,
678
    pub us_default_char: OptionU16,
679
    pub us_break_char: OptionU16,
680
    pub us_max_context: OptionU16,
681

            
682
    // os/2 version 3 table
683
    pub us_lower_optical_point_size: OptionU16,
684
    pub us_upper_optical_point_size: OptionU16,
685

            
686
    // head table (u16/i16 - align 2)
687
    pub units_per_em: u16,
688
    pub font_flags: u16,
689
    pub x_min: i16,
690
    pub y_min: i16,
691
    pub x_max: i16,
692
    pub y_max: i16,
693

            
694
    // hhea table
695
    pub ascender: i16,
696
    pub descender: i16,
697
    pub line_gap: i16,
698
    pub advance_width_max: u16,
699
    pub min_left_side_bearing: i16,
700
    pub min_right_side_bearing: i16,
701
    pub x_max_extent: i16,
702
    pub caret_slope_rise: i16,
703
    pub caret_slope_run: i16,
704
    pub caret_offset: i16,
705
    pub num_h_metrics: u16,
706

            
707
    // os/2 table (u16/i16 fields)
708
    pub x_avg_char_width: i16,
709
    pub us_weight_class: u16,
710
    pub us_width_class: u16,
711
    pub fs_type: u16,
712
    pub y_subscript_x_size: i16,
713
    pub y_subscript_y_size: i16,
714
    pub y_subscript_x_offset: i16,
715
    pub y_subscript_y_offset: i16,
716
    pub y_superscript_x_size: i16,
717
    pub y_superscript_y_size: i16,
718
    pub y_superscript_x_offset: i16,
719
    pub y_superscript_y_offset: i16,
720
    pub y_strikeout_size: i16,
721
    pub y_strikeout_position: i16,
722
    pub s_family_class: i16,
723
    pub fs_selection: u16,
724
    pub us_first_char_index: u16,
725
    pub us_last_char_index: u16,
726

            
727
    // panose (align 1 - last)
728
    pub panose: Panose,
729
}
730

            
731
impl Default for FontMetrics {
732
    fn default() -> Self {
733
        FontMetrics::zero()
734
    }
735
}
736

            
737
impl FontMetrics {
738
    /// Only for testing, zero-sized font, will always return 0 for every metric
739
    /// (`units_per_em = 1000`)
740
    pub const fn zero() -> Self {
741
        FontMetrics {
742
            ul_code_page_range1: OptionU32::None,
743
            ul_code_page_range2: OptionU32::None,
744
            ul_unicode_range1: 0,
745
            ul_unicode_range2: 0,
746
            ul_unicode_range3: 0,
747
            ul_unicode_range4: 0,
748
            ach_vend_id: 0,
749
            s_typo_ascender: OptionI16::None,
750
            s_typo_descender: OptionI16::None,
751
            s_typo_line_gap: OptionI16::None,
752
            us_win_ascent: OptionU16::None,
753
            us_win_descent: OptionU16::None,
754
            sx_height: OptionI16::None,
755
            s_cap_height: OptionI16::None,
756
            us_default_char: OptionU16::None,
757
            us_break_char: OptionU16::None,
758
            us_max_context: OptionU16::None,
759
            us_lower_optical_point_size: OptionU16::None,
760
            us_upper_optical_point_size: OptionU16::None,
761
            units_per_em: 1000,
762
            font_flags: 0,
763
            x_min: 0,
764
            y_min: 0,
765
            x_max: 0,
766
            y_max: 0,
767
            ascender: 0,
768
            descender: 0,
769
            line_gap: 0,
770
            advance_width_max: 0,
771
            min_left_side_bearing: 0,
772
            min_right_side_bearing: 0,
773
            x_max_extent: 0,
774
            caret_slope_rise: 0,
775
            caret_slope_run: 0,
776
            caret_offset: 0,
777
            num_h_metrics: 0,
778
            x_avg_char_width: 0,
779
            us_weight_class: 400,
780
            us_width_class: 5,
781
            fs_type: 0,
782
            y_subscript_x_size: 0,
783
            y_subscript_y_size: 0,
784
            y_subscript_x_offset: 0,
785
            y_subscript_y_offset: 0,
786
            y_superscript_x_size: 0,
787
            y_superscript_y_size: 0,
788
            y_superscript_x_offset: 0,
789
            y_superscript_y_offset: 0,
790
            y_strikeout_size: 0,
791
            y_strikeout_position: 0,
792
            s_family_class: 0,
793
            fs_selection: 0,
794
            us_first_char_index: 0,
795
            us_last_char_index: 0,
796
            panose: Panose::zero(),
797
        }
798
    }
799

            
800
    /// Returns the ascender value from the hhea table
801
    pub fn get_ascender(&self) -> i16 {
802
        self.ascender
803
    }
804

            
805
    /// Returns the descender value from the hhea table
806
    pub fn get_descender(&self) -> i16 {
807
        self.descender
808
    }
809

            
810
    /// Returns the line gap value from the hhea table
811
    pub fn get_line_gap(&self) -> i16 {
812
        self.line_gap
813
    }
814

            
815
    /// Returns the maximum advance width from the hhea table
816
    pub fn get_advance_width_max(&self) -> u16 {
817
        self.advance_width_max
818
    }
819

            
820
    /// Returns the minimum left side bearing from the hhea table
821
    pub fn get_min_left_side_bearing(&self) -> i16 {
822
        self.min_left_side_bearing
823
    }
824

            
825
    /// Returns the minimum right side bearing from the hhea table
826
    pub fn get_min_right_side_bearing(&self) -> i16 {
827
        self.min_right_side_bearing
828
    }
829

            
830
    /// Returns the x_min value from the head table
831
    pub fn get_x_min(&self) -> i16 {
832
        self.x_min
833
    }
834

            
835
    /// Returns the y_min value from the head table
836
    pub fn get_y_min(&self) -> i16 {
837
        self.y_min
838
    }
839

            
840
    /// Returns the x_max value from the head table
841
    pub fn get_x_max(&self) -> i16 {
842
        self.x_max
843
    }
844

            
845
    /// Returns the y_max value from the head table
846
    pub fn get_y_max(&self) -> i16 {
847
        self.y_max
848
    }
849

            
850
    /// Returns the maximum extent in the x direction from the hhea table
851
    pub fn get_x_max_extent(&self) -> i16 {
852
        self.x_max_extent
853
    }
854

            
855
    /// Returns the average character width from the os/2 table
856
    pub fn get_x_avg_char_width(&self) -> i16 {
857
        self.x_avg_char_width
858
    }
859

            
860
    /// Returns the subscript x size from the os/2 table
861
    pub fn get_y_subscript_x_size(&self) -> i16 {
862
        self.y_subscript_x_size
863
    }
864

            
865
    /// Returns the subscript y size from the os/2 table
866
    pub fn get_y_subscript_y_size(&self) -> i16 {
867
        self.y_subscript_y_size
868
    }
869

            
870
    /// Returns the subscript x offset from the os/2 table
871
    pub fn get_y_subscript_x_offset(&self) -> i16 {
872
        self.y_subscript_x_offset
873
    }
874

            
875
    /// Returns the subscript y offset from the os/2 table
876
    pub fn get_y_subscript_y_offset(&self) -> i16 {
877
        self.y_subscript_y_offset
878
    }
879

            
880
    /// Returns the superscript x size from the os/2 table
881
    pub fn get_y_superscript_x_size(&self) -> i16 {
882
        self.y_superscript_x_size
883
    }
884

            
885
    /// Returns the superscript y size from the os/2 table
886
    pub fn get_y_superscript_y_size(&self) -> i16 {
887
        self.y_superscript_y_size
888
    }
889

            
890
    /// Returns the superscript x offset from the os/2 table
891
    pub fn get_y_superscript_x_offset(&self) -> i16 {
892
        self.y_superscript_x_offset
893
    }
894

            
895
    /// Returns the superscript y offset from the os/2 table
896
    pub fn get_y_superscript_y_offset(&self) -> i16 {
897
        self.y_superscript_y_offset
898
    }
899

            
900
    /// Returns the strikeout size from the os/2 table
901
    pub fn get_y_strikeout_size(&self) -> i16 {
902
        self.y_strikeout_size
903
    }
904

            
905
    /// Returns the strikeout position from the os/2 table
906
    pub fn get_y_strikeout_position(&self) -> i16 {
907
        self.y_strikeout_position
908
    }
909

            
910
    /// Returns whether typographic metrics should be used (from fs_selection flag)
911
    pub fn use_typo_metrics(&self) -> bool {
912
        // Bit 7 of fs_selection indicates USE_TYPO_METRICS
913
        (self.fs_selection & 0x0080) != 0
914
    }
915
}
916

            
917
#[cfg(all(test, feature = "parser"))]
918
mod tests {
919
    use super::*;
920

            
921
    #[test]
922
1
    fn test_parse_font_weight_keywords() {
923
1
        assert_eq!(
924
1
            parse_font_weight("normal").unwrap(),
925
            StyleFontWeight::Normal
926
        );
927
1
        assert_eq!(parse_font_weight("bold").unwrap(), StyleFontWeight::Bold);
928
1
        assert_eq!(
929
1
            parse_font_weight("lighter").unwrap(),
930
            StyleFontWeight::Lighter
931
        );
932
1
        assert_eq!(
933
1
            parse_font_weight("bolder").unwrap(),
934
            StyleFontWeight::Bolder
935
        );
936
1
    }
937

            
938
    #[test]
939
1
    fn test_parse_font_weight_numbers() {
940
1
        assert_eq!(parse_font_weight("100").unwrap(), StyleFontWeight::W100);
941
1
        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
942
1
        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
943
1
        assert_eq!(parse_font_weight("900").unwrap(), StyleFontWeight::W900);
944
1
    }
945

            
946
    #[test]
947
1
    fn test_parse_font_weight_invalid() {
948
1
        assert!(parse_font_weight("thin").is_err());
949
1
        assert!(parse_font_weight("").is_err());
950
1
        assert!(parse_font_weight("450").is_err());
951
1
        assert!(parse_font_weight("boldest").is_err());
952
1
    }
953

            
954
    #[test]
955
1
    fn test_parse_font_style() {
956
1
        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
957
1
        assert_eq!(parse_font_style("italic").unwrap(), StyleFontStyle::Italic);
958
1
        assert_eq!(
959
1
            parse_font_style("oblique").unwrap(),
960
            StyleFontStyle::Oblique
961
        );
962
1
        assert_eq!(
963
1
            parse_font_style("  italic  ").unwrap(),
964
            StyleFontStyle::Italic
965
        );
966
1
        assert!(parse_font_style("slanted").is_err());
967
1
    }
968

            
969
    #[test]
970
1
    fn test_parse_font_size() {
971
1
        assert_eq!(
972
1
            parse_style_font_size("16px").unwrap().inner,
973
1
            PixelValue::px(16.0)
974
        );
975
1
        assert_eq!(
976
1
            parse_style_font_size("1.2em").unwrap().inner,
977
1
            PixelValue::em(1.2)
978
        );
979
1
        assert_eq!(
980
1
            parse_style_font_size("12pt").unwrap().inner,
981
1
            PixelValue::pt(12.0)
982
        );
983
1
        assert_eq!(
984
1
            parse_style_font_size("120%").unwrap().inner,
985
1
            PixelValue::percent(120.0)
986
        );
987
1
        assert!(parse_style_font_size("medium").is_err());
988
1
    }
989

            
990
    #[test]
991
1
    fn test_parse_font_family() {
992
        // Single unquoted
993
1
        let result = parse_style_font_family("Arial").unwrap();
994
1
        assert_eq!(result.len(), 1);
995
1
        assert_eq!(
996
1
            result.as_slice()[0],
997
1
            StyleFontFamily::System("Arial".into())
998
        );
999

            
        // Single quoted
1
        let result = parse_style_font_family("\"Times New Roman\"").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(
1
            result.as_slice()[0],
1
            StyleFontFamily::System("Times New Roman".into())
        );
        // Multiple
1
        let result = parse_style_font_family("Georgia, serif").unwrap();
1
        assert_eq!(result.len(), 2);
1
        assert_eq!(
1
            result.as_slice()[0],
1
            StyleFontFamily::System("Georgia".into())
        );
1
        assert_eq!(
1
            result.as_slice()[1],
1
            StyleFontFamily::System("serif".into())
        );
        // Multiple with quotes and extra whitespace
1
        let result = parse_style_font_family("  'Courier New'  , monospace  ").unwrap();
1
        assert_eq!(result.len(), 2);
1
        assert_eq!(
1
            result.as_slice()[0],
1
            StyleFontFamily::System("Courier New".into())
        );
1
        assert_eq!(
1
            result.as_slice()[1],
1
            StyleFontFamily::System("monospace".into())
        );
1
    }
    #[test]
1
    fn test_parse_system_font_type() {
        use crate::system::SystemFontType;
        // Single system font type
1
        let result = parse_style_font_family("system:ui").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::Ui));
        // System font type with bold variant
1
        let result = parse_style_font_family("system:monospace:bold").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::MonospaceBold));
        // System font type with italic variant
1
        let result = parse_style_font_family("system:monospace:italic").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::MonospaceItalic));
        // System font type with fallback
1
        let result = parse_style_font_family("system:ui, Arial, sans-serif").unwrap();
1
        assert_eq!(result.len(), 3);
1
        assert_eq!(result.as_slice()[0], StyleFontFamily::SystemType(SystemFontType::Ui));
1
        assert_eq!(result.as_slice()[1], StyleFontFamily::System("Arial".into()));
1
        assert_eq!(result.as_slice()[2], StyleFontFamily::System("sans-serif".into()));
        // All system font types
1
        assert!(parse_style_font_family("system:ui").is_ok());
1
        assert!(parse_style_font_family("system:ui:bold").is_ok());
1
        assert!(parse_style_font_family("system:monospace").is_ok());
1
        assert!(parse_style_font_family("system:monospace:bold").is_ok());
1
        assert!(parse_style_font_family("system:monospace:italic").is_ok());
1
        assert!(parse_style_font_family("system:title").is_ok());
1
        assert!(parse_style_font_family("system:title:bold").is_ok());
1
        assert!(parse_style_font_family("system:menu").is_ok());
1
        assert!(parse_style_font_family("system:small").is_ok());
1
        assert!(parse_style_font_family("system:serif").is_ok());
1
        assert!(parse_style_font_family("system:serif:bold").is_ok());
        // Invalid system font type should be parsed as regular font name
1
        let result = parse_style_font_family("system:invalid").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(result.as_slice()[0], StyleFontFamily::System("system:invalid".into()));
1
    }
    #[test]
1
    fn test_system_font_type_css_roundtrip() {
        use crate::system::SystemFontType;
        // Test that as_css_str() and from_css_str() are inverses
1
        let types = [
1
            SystemFontType::Ui,
1
            SystemFontType::UiBold,
1
            SystemFontType::Monospace,
1
            SystemFontType::MonospaceBold,
1
            SystemFontType::MonospaceItalic,
1
            SystemFontType::Title,
1
            SystemFontType::TitleBold,
1
            SystemFontType::Menu,
1
            SystemFontType::Small,
1
            SystemFontType::Serif,
1
            SystemFontType::SerifBold,
1
        ];
12
        for ft in &types {
11
            let css = ft.as_css_str();
11
            let parsed = SystemFontType::from_css_str(css).unwrap();
11
            assert_eq!(*ft, parsed, "Roundtrip failed for {:?}", ft);
        }
1
    }
}