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::{AtomicU64, AtomicUsize, Ordering as AtomicOrdering},
19
};
20

            
21
#[cfg(feature = "parser")]
22
use crate::props::basic::parse::{strip_quotes, UnclosedQuotesError};
23
use crate::{
24
    codegen::format::{FormatAsRustCode, GetHash},
25
    corety::{AzString, U8Vec},
26
    props::{
27
        basic::{
28
            error::{InvalidValueErr, InvalidValueErrOwned},
29
            pixel::{
30
                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
31
                PixelValue,
32
            },
33
        },
34
        formatter::PrintAsCssValue,
35
    },
36
    system::SystemFontType,
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
impl PrintAsCssValue for StyleFontWeight {
61
13
    fn print_as_css_value(&self) -> String {
62
13
        match self {
63
1
            Self::Lighter => "lighter".to_string(),
64
1
            Self::W100 => "100".to_string(),
65
1
            Self::W200 => "200".to_string(),
66
1
            Self::W300 => "300".to_string(),
67
2
            Self::Normal => "normal".to_string(),
68
1
            Self::W500 => "500".to_string(),
69
1
            Self::W600 => "600".to_string(),
70
2
            Self::Bold => "bold".to_string(),
71
1
            Self::W800 => "800".to_string(),
72
1
            Self::W900 => "900".to_string(),
73
1
            Self::Bolder => "bolder".to_string(),
74
        }
75
13
    }
76
}
77

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

            
102
// --- Font Style ---
103

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

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

            
125
impl FormatAsRustCode for StyleFontStyle {
126
3
    fn format_as_rust_code(&self, _tabs: usize) -> String {
127
        use StyleFontStyle::{Italic, Normal, Oblique};
128
3
        format!(
129
3
            "StyleFontStyle::{}",
130
3
            match self {
131
1
                Normal => "Normal",
132
1
                Italic => "Italic",
133
1
                Oblique => "Oblique",
134
            }
135
        )
136
3
    }
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
7
    fn default() -> Self {
150
7
        Self {
151
7
            // Default font size is 12pt, a common default for print and web.
152
7
            inner: PixelValue::const_pt(12),
153
7
        }
154
7
    }
155
}
156

            
157
impl_pixel_value!(StyleFontSize);
158
impl PrintAsCssValue for StyleFontSize {
159
1498
    fn print_as_css_value(&self) -> String {
160
1498
        format!("{}", self.inner)
161
1498
    }
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
    /// Process-unique, monotonically-assigned identity of this parsed font.
182
    /// Shared by shallow clones (same font), fresh for each `new`. Used for
183
    /// `Eq`/`Ord`/`Hash` instead of the `parsed` pointer so that freeing a
184
    /// font and reusing its heap address can't forge identity — the same
185
    /// aliasing fix applied to `ImageRef`. (Content-level dedup still uses the
186
    /// separate content hash via `font_ref_get_hash`.)
187
    pub id: u64,
188
    /// Whether to run the destructor on drop
189
    pub run_destructor: bool,
190
    /// Destructor function for the parsed data
191
    pub parsed_destructor: FontRefDestructorCallbackType,
192
}
193

            
194
/// Never-reused source of [`FontRef::id`]. Starts at 1 so `id == 0` can flag
195
/// an un-initialised / raw-reconstructed handle.
196
static FONT_REF_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
197

            
198
#[must_use]
199
34518
fn next_font_ref_id() -> u64 {
200
34518
    FONT_REF_ID_COUNTER.fetch_add(1, AtomicOrdering::SeqCst)
201
34518
}
202

            
203
impl fmt::Debug for FontRef {
204
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205
2
        write!(f, "FontRef(0x{:x}", self.parsed as usize)?;
206
2
        if let Some(c) = unsafe { self.copies.as_ref() } {
207
1
            write!(f, ", copies: {})", c.load(AtomicOrdering::SeqCst))?;
208
        } else {
209
1
            write!(f, ")")?;
210
        }
211
2
        Ok(())
212
2
    }
213
}
214

            
215
impl FontRef {
216
    /// Create a new `FontRef` from parsed font data
217
    ///
218
    /// # Arguments
219
    /// * `parsed` - Pointer to parsed font data (e.g., `Arc::into_raw(Arc::new(ParsedFont))`)
220
    /// * `destructor` - Function to clean up the parsed data
221
34516
    pub fn new(parsed: *const c_void, destructor: FontRefDestructorCallbackType) -> Self {
222
34516
        Self {
223
34516
            parsed,
224
34516
            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
225
34516
            id: next_font_ref_id(),
226
34516
            run_destructor: true,
227
34516
            parsed_destructor: destructor,
228
34516
        }
229
34516
    }
230

            
231
    /// Get a raw pointer to the parsed font data
232
    #[inline]
233
    #[must_use]
234
9
    pub const fn get_parsed(&self) -> *const c_void {
235
9
        self.parsed
236
9
    }
237
}
238
impl_option!(
239
    FontRef,
240
    OptionFontRef,
241
    copy = false,
242
    [Debug, Clone, PartialEq, Eq, Hash]
243
);
244
unsafe impl Send for FontRef {}
245
unsafe impl Sync for FontRef {}
246
// Identity is the never-reused `id`, NOT the `parsed` pointer (which is freed
247
// when the last ref drops and whose address may be reused by a later font).
248
impl PartialEq for FontRef {
249
1185
    fn eq(&self, rhs: &Self) -> bool {
250
1185
        self.id == rhs.id
251
1185
    }
252
}
253
impl PartialOrd for FontRef {
254
53
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255
53
        Some(self.id.cmp(&other.id))
256
53
    }
257
}
258
impl Ord for FontRef {
259
2
    fn cmp(&self, other: &Self) -> Ordering {
260
2
        self.id.cmp(&other.id)
261
2
    }
262
}
263
impl Eq for FontRef {}
264
impl Hash for FontRef {
265
121
    fn hash<H: Hasher>(&self, state: &mut H) {
266
121
        self.id.hash(state);
267
121
    }
268
}
269
impl Clone for FontRef {
270
2197866
    fn clone(&self) -> Self {
271
2197866
        if !self.copies.is_null() {
272
2197865
            unsafe {
273
2197865
                (*self.copies).fetch_add(1, AtomicOrdering::SeqCst);
274
2197865
            }
275
1
        }
276
2197866
        Self {
277
2197866
            parsed: self.parsed,
278
2197866
            copies: self.copies,
279
2197866
            id: self.id, // same font → same identity
280
2197866
            run_destructor: self.run_destructor,
281
2197866
            parsed_destructor: self.parsed_destructor,
282
2197866
        }
283
2197866
    }
284
}
285
impl Drop for FontRef {
286
2232384
    fn drop(&mut self) {
287
2232384
        if self.run_destructor
288
2232381
            && !self.copies.is_null()
289
2232381
            && unsafe { (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst) } == 1
290
        {
291
34516
            unsafe {
292
34516
                (self.parsed_destructor)(self.parsed.cast_mut());
293
34516
                drop(Box::from_raw(self.copies.cast_mut()));
294
34516
            }
295
2197868
        }
296
2232384
    }
297
}
298

            
299
// --- Font Family ---
300

            
301
/// Represents a `font-family` attribute.
302
///
303
/// Can be:
304
/// - `System(AzString)`: A named font family (e.g., "Arial", "Times New Roman")
305
/// - `SystemType(SystemFontType)`: A semantic system font type (e.g., `system:ui`,
306
///   `system:monospace`)
307
/// - `File(AzString)`: A font loaded from a file URL
308
/// - `Ref(FontRef)`: A reference to a pre-loaded font
309
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
310
#[repr(C, u8)]
311
pub enum StyleFontFamily {
312
    /// Named font family (e.g., "Arial", "Times New Roman", "monospace")
313
    System(AzString),
314
    /// Semantic system font type (e.g., `system:ui`, `system:monospace:bold`)
315
    /// Resolved at runtime based on platform and accessibility settings
316
    SystemType(SystemFontType),
317
    /// Font loaded from a file URL
318
    File(AzString),
319
    /// Reference to a pre-loaded font
320
    Ref(FontRef),
321
}
322

            
323
impl_option!(
324
    StyleFontFamily,
325
    OptionStyleFontFamily,
326
    copy = false,
327
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
328
);
329

            
330
impl StyleFontFamily {
331
122
    pub fn as_string(&self) -> String {
332
122
        match &self {
333
104
            Self::System(s) => {
334
104
                let owned = s.clone().into_library_owned_string();
335
104
                if owned.contains(char::is_whitespace) {
336
8
                    format!("\"{owned}\"")
337
                } else {
338
96
                    owned
339
                }
340
            }
341
14
            Self::SystemType(st) => st.as_css_str().to_string(),
342
2
            Self::File(s) => format!("url({})", s.clone().into_library_owned_string()),
343
2
            Self::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
344
        }
345
122
    }
346

            
347
    /// The RAW family name, for querying the font backend (fontconfig). Unlike
348
    /// `as_string()` this does NOT apply CSS serialization — a multi-word name comes
349
    /// back as `Times New Roman`, not `"Times New Roman"`, since the backend matches on
350
    /// the bare name and the quotes would corrupt the query.
351
    #[must_use]
352
15782
    pub fn as_query_string(&self) -> String {
353
15782
        match &self {
354
15782
            Self::System(s) | Self::File(s) => s.clone().into_library_owned_string(),
355
            Self::SystemType(st) => st.as_css_str().to_string(),
356
            Self::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
357
        }
358
15782
    }
359
}
360

            
361
impl_vec!(
362
    StyleFontFamily,
363
    StyleFontFamilyVec,
364
    StyleFontFamilyVecDestructor,
365
    StyleFontFamilyVecDestructorType,
366
    StyleFontFamilyVecSlice,
367
    OptionStyleFontFamily
368
);
369
impl_vec_clone!(
370
    StyleFontFamily,
371
    StyleFontFamilyVec,
372
    StyleFontFamilyVecDestructor
373
);
374
impl_vec_debug!(StyleFontFamily, StyleFontFamilyVec);
375
impl_vec_eq!(StyleFontFamily, StyleFontFamilyVec);
376
impl_vec_ord!(StyleFontFamily, StyleFontFamilyVec);
377
impl_vec_hash!(StyleFontFamily, StyleFontFamilyVec);
378
impl_vec_partialeq!(StyleFontFamily, StyleFontFamilyVec);
379
impl_vec_partialord!(StyleFontFamily, StyleFontFamilyVec);
380

            
381
impl PrintAsCssValue for StyleFontFamilyVec {
382
2
    fn print_as_css_value(&self) -> String {
383
2
        self.iter()
384
2
            .map(StyleFontFamily::as_string)
385
2
            .collect::<Vec<_>>()
386
2
            .join(", ")
387
2
    }
388
}
389

            
390
// Formatting to Rust code for StyleFontFamilyVec
391
impl FormatAsRustCode for StyleFontFamilyVec {
392
    fn format_as_rust_code(&self, _tabs: usize) -> String {
393
        format!(
394
            "StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_{}_ITEMS)",
395
            self.get_hash()
396
        )
397
    }
398
}
399

            
400
// --- PARSERS ---
401

            
402
// -- Font Weight Parser --
403

            
404
#[derive(Clone, PartialEq, Eq)]
405
pub enum CssFontWeightParseError<'a> {
406
    InvalidValue(InvalidValueErr<'a>),
407
    InvalidNumber(ParseIntError),
408
}
409

            
410
// Formatting to Rust code for StyleFontFamily
411
impl FormatAsRustCode for StyleFontFamily {
412
3
    fn format_as_rust_code(&self, _tabs: usize) -> String {
413
3
        match self {
414
1
            Self::System(id) => {
415
1
                format!("StyleFontFamily::System(STRING_{})", id.get_hash())
416
            }
417
1
            Self::SystemType(st) => {
418
1
                format!("StyleFontFamily::SystemType(SystemFontType::{st:?})")
419
            }
420
1
            Self::File(path) => {
421
1
                format!("StyleFontFamily::File(STRING_{})", path.get_hash())
422
            }
423
            Self::Ref(font_ref) => {
424
                format!("StyleFontFamily::Ref({:0x})", font_ref.parsed as usize)
425
            }
426
        }
427
3
    }
428
}
429
impl_debug_as_display!(CssFontWeightParseError<'a>);
430
impl_display! { CssFontWeightParseError<'a>, {
431
    InvalidValue(e) => format!("Invalid font-weight keyword: \"{}\"", e.0),
432
    InvalidNumber(e) => format!("Invalid font-weight number: {}", e),
433
}}
434
impl<'a> From<InvalidValueErr<'a>> for CssFontWeightParseError<'a> {
435
36
    fn from(e: InvalidValueErr<'a>) -> Self {
436
36
        CssFontWeightParseError::InvalidValue(e)
437
36
    }
438
}
439
impl From<ParseIntError> for CssFontWeightParseError<'_> {
440
    fn from(e: ParseIntError) -> Self {
441
        CssFontWeightParseError::InvalidNumber(e)
442
    }
443
}
444
#[allow(variant_size_differences)]
445
// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size
446
// disparity accepted
447
#[derive(Debug, Clone, PartialEq, Eq)]
448
#[repr(C, u8)]
449
pub enum CssFontWeightParseErrorOwned {
450
    InvalidValue(InvalidValueErrOwned),
451
    InvalidNumber(crate::props::basic::error::ParseIntError),
452
}
453

            
454
impl CssFontWeightParseError<'_> {
455
    #[must_use]
456
9
    pub fn to_contained(&self) -> CssFontWeightParseErrorOwned {
457
9
        match self {
458
4
            Self::InvalidValue(e) => CssFontWeightParseErrorOwned::InvalidValue(e.to_contained()),
459
5
            Self::InvalidNumber(e) => CssFontWeightParseErrorOwned::InvalidNumber(e.clone().into()),
460
        }
461
9
    }
462
}
463

            
464
impl CssFontWeightParseErrorOwned {
465
    #[must_use]
466
9
    pub fn to_shared(&self) -> CssFontWeightParseError<'_> {
467
9
        match self {
468
4
            Self::InvalidValue(e) => CssFontWeightParseError::InvalidValue(e.to_shared()),
469
5
            Self::InvalidNumber(e) => CssFontWeightParseError::InvalidNumber(e.to_std()),
470
        }
471
9
    }
472
}
473

            
474
#[cfg(feature = "parser")]
475
/// # Errors
476
///
477
/// Returns an error if `input` is not a valid CSS `font-weight` value.
478
106
pub fn parse_font_weight(input: &str) -> Result<StyleFontWeight, CssFontWeightParseError<'_>> {
479
106
    let input = input.trim();
480
106
    match input {
481
106
        "lighter" => Ok(StyleFontWeight::Lighter),
482
104
        "normal" | "400" => Ok(StyleFontWeight::Normal),
483
98
        "bold" | "700" => Ok(StyleFontWeight::Bold),
484
47
        "bolder" => Ok(StyleFontWeight::Bolder),
485
45
        "100" => Ok(StyleFontWeight::W100),
486
43
        "200" => Ok(StyleFontWeight::W200),
487
42
        "300" => Ok(StyleFontWeight::W300),
488
41
        "500" => Ok(StyleFontWeight::W500),
489
40
        "600" => Ok(StyleFontWeight::W600),
490
39
        "800" => Ok(StyleFontWeight::W800),
491
38
        "900" => Ok(StyleFontWeight::W900),
492
36
        _ => Err(InvalidValueErr(input).into()),
493
    }
494
106
}
495

            
496
// -- Font Style Parser --
497

            
498
#[derive(Clone, PartialEq, Eq)]
499
pub enum CssFontStyleParseError<'a> {
500
    InvalidValue(InvalidValueErr<'a>),
501
}
502
impl_debug_as_display!(CssFontStyleParseError<'a>);
503
impl_display! { CssFontStyleParseError<'a>, {
504
    InvalidValue(e) => format!("Invalid font-style: \"{}\"", e.0),
505
}}
506
impl_from! { InvalidValueErr<'a>, CssFontStyleParseError::InvalidValue }
507

            
508
#[derive(Debug, Clone, PartialEq, Eq)]
509
#[repr(C, u8)]
510
pub enum CssFontStyleParseErrorOwned {
511
    InvalidValue(InvalidValueErrOwned),
512
}
513
impl CssFontStyleParseError<'_> {
514
    #[must_use]
515
3
    pub fn to_contained(&self) -> CssFontStyleParseErrorOwned {
516
3
        match self {
517
3
            Self::InvalidValue(e) => CssFontStyleParseErrorOwned::InvalidValue(e.to_contained()),
518
        }
519
3
    }
520
}
521
impl CssFontStyleParseErrorOwned {
522
    #[must_use]
523
3
    pub fn to_shared(&self) -> CssFontStyleParseError<'_> {
524
3
        match self {
525
3
            Self::InvalidValue(e) => CssFontStyleParseError::InvalidValue(e.to_shared()),
526
        }
527
3
    }
528
}
529

            
530
#[cfg(feature = "parser")]
531
/// # Errors
532
///
533
/// Returns an error if `input` is not a valid CSS `font-style` value.
534
53
pub fn parse_font_style(input: &str) -> Result<StyleFontStyle, CssFontStyleParseError<'_>> {
535
53
    match input.trim() {
536
53
        "normal" => Ok(StyleFontStyle::Normal),
537
50
        "italic" => Ok(StyleFontStyle::Italic),
538
20
        "oblique" => Ok(StyleFontStyle::Oblique),
539
18
        other => Err(InvalidValueErr(other).into()),
540
    }
541
53
}
542

            
543
// -- Font Size Parser --
544

            
545
#[derive(Clone, PartialEq, Eq)]
546
pub enum CssStyleFontSizeParseError<'a> {
547
    PixelValue(CssPixelValueParseError<'a>),
548
}
549
impl_debug_as_display!(CssStyleFontSizeParseError<'a>);
550
impl_display! { CssStyleFontSizeParseError<'a>, {
551
    PixelValue(e) => format!("Invalid font-size: {}", e),
552
}}
553
impl_from! { CssPixelValueParseError<'a>, CssStyleFontSizeParseError::PixelValue }
554

            
555
#[derive(Debug, Clone, PartialEq, Eq)]
556
#[repr(C, u8)]
557
pub enum CssStyleFontSizeParseErrorOwned {
558
    PixelValue(CssPixelValueParseErrorOwned),
559
}
560
impl CssStyleFontSizeParseError<'_> {
561
    #[must_use]
562
8
    pub fn to_contained(&self) -> CssStyleFontSizeParseErrorOwned {
563
8
        match self {
564
8
            Self::PixelValue(e) => CssStyleFontSizeParseErrorOwned::PixelValue(e.to_contained()),
565
        }
566
8
    }
567
}
568
impl CssStyleFontSizeParseErrorOwned {
569
    #[must_use]
570
8
    pub fn to_shared(&self) -> CssStyleFontSizeParseError<'_> {
571
8
        match self {
572
8
            Self::PixelValue(e) => CssStyleFontSizeParseError::PixelValue(e.to_shared()),
573
        }
574
8
    }
575
}
576

            
577
#[cfg(feature = "parser")]
578
/// # Errors
579
///
580
/// Returns an error if `input` is not a valid CSS `font-size` value.
581
21219
pub fn parse_style_font_size(input: &str) -> Result<StyleFontSize, CssStyleFontSizeParseError<'_>> {
582
    Ok(StyleFontSize {
583
21219
        inner: parse_pixel_value(input)?,
584
    })
585
21219
}
586

            
587
// -- Font Family Parser --
588

            
589
#[derive(PartialEq, Eq, Clone)]
590
pub enum CssStyleFontFamilyParseError<'a> {
591
    InvalidStyleFontFamily(&'a str),
592
    UnclosedQuotes(UnclosedQuotesError<'a>),
593
}
594
impl_debug_as_display!(CssStyleFontFamilyParseError<'a>);
595
impl_display! { CssStyleFontFamilyParseError<'a>, {
596
    InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val),
597
    UnclosedQuotes(val) => format!("Unclosed quotes in font-family: \"{}\"", val.0),
598
}}
599
impl<'a> From<UnclosedQuotesError<'a>> for CssStyleFontFamilyParseError<'a> {
600
    fn from(err: UnclosedQuotesError<'a>) -> Self {
601
        CssStyleFontFamilyParseError::UnclosedQuotes(err)
602
    }
603
}
604

            
605
#[derive(Debug, Clone, PartialEq, Eq)]
606
#[repr(C, u8)]
607
pub enum CssStyleFontFamilyParseErrorOwned {
608
    InvalidStyleFontFamily(AzString),
609
    UnclosedQuotes(AzString),
610
}
611
impl CssStyleFontFamilyParseError<'_> {
612
    #[must_use]
613
4
    pub fn to_contained(&self) -> CssStyleFontFamilyParseErrorOwned {
614
4
        match self {
615
2
            CssStyleFontFamilyParseError::InvalidStyleFontFamily(s) => {
616
2
                CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily((*s).to_string().into())
617
            }
618
2
            CssStyleFontFamilyParseError::UnclosedQuotes(e) => {
619
2
                CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(e.0.to_string().into())
620
            }
621
        }
622
4
    }
623
}
624
impl CssStyleFontFamilyParseErrorOwned {
625
    #[must_use]
626
4
    pub fn to_shared(&self) -> CssStyleFontFamilyParseError<'_> {
627
4
        match self {
628
2
            Self::InvalidStyleFontFamily(s) => {
629
2
                CssStyleFontFamilyParseError::InvalidStyleFontFamily(s)
630
            }
631
2
            Self::UnclosedQuotes(s) => {
632
2
                CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError(s))
633
            }
634
        }
635
4
    }
636
}
637

            
638
#[cfg(feature = "parser")]
639
/// # Errors
640
///
641
/// Returns an error if `input` is not a valid CSS `font-family` value.
642
3433
pub fn parse_style_font_family(
643
3433
    input: &str,
644
3433
) -> Result<StyleFontFamilyVec, CssStyleFontFamilyParseError<'_>> {
645
3433
    let multiple_fonts = input.split(',');
646
3433
    let mut fonts = Vec::with_capacity(1);
647

            
648
17847
    for font in multiple_fonts {
649
14414
        let font = font.trim();
650

            
651
        // Check for system font type syntax: system:ui, system:monospace:bold, etc.
652
14414
        if font.starts_with("system:") {
653
594
            if let Some(system_type) = SystemFontType::from_css_str(font) {
654
588
                fonts.push(StyleFontFamily::SystemType(system_type));
655
588
                continue;
656
6
            }
657
            // Invalid system font type, fall through to treat as regular font name
658
13820
        }
659

            
660
13826
        if let Ok(stripped) = strip_quotes(font) {
661
1949
            fonts.push(StyleFontFamily::System(stripped.0.to_string().into()));
662
12111
        } else {
663
11877
            // It could be an unquoted font name like `Times New Roman`.
664
11877
            fonts.push(StyleFontFamily::System(font.to_string().into()));
665
11877
        }
666
    }
667

            
668
3433
    Ok(fonts.into())
669
3433
}
670

            
671
// --- Font Metrics ---
672

            
673
use crate::corety::{OptionI16, OptionU16, OptionU32};
674

            
675
/// PANOSE classification values for font identification (10 bytes).
676
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose>
677
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
678
#[repr(C)]
679
#[derive(Default)]
680
pub struct Panose {
681
    pub family_type: u8,
682
    pub serif_style: u8,
683
    pub weight: u8,
684
    pub proportion: u8,
685
    pub contrast: u8,
686
    pub stroke_variation: u8,
687
    pub arm_style: u8,
688
    pub letterform: u8,
689
    pub midline: u8,
690
    pub x_height: u8,
691
}
692

            
693
impl Panose {
694
    #[must_use]
695
5
    pub const fn zero() -> Self {
696
5
        Self {
697
5
            family_type: 0,
698
5
            serif_style: 0,
699
5
            weight: 0,
700
5
            proportion: 0,
701
5
            contrast: 0,
702
5
            stroke_variation: 0,
703
5
            arm_style: 0,
704
5
            letterform: 0,
705
5
            midline: 0,
706
5
            x_height: 0,
707
5
        }
708
5
    }
709
}
710

            
711
/// Font metrics structure containing all font-related measurements from
712
/// the font file tables (head, hhea, and os/2 tables).
713
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
714
#[repr(C)]
715
pub struct FontMetrics {
716
    // os/2 version 1 table (u32 fields - align 4, placed first)
717
    pub ul_code_page_range1: OptionU32,
718
    pub ul_code_page_range2: OptionU32,
719

            
720
    // os/2 table (u32 fields)
721
    pub ul_unicode_range1: u32,
722
    pub ul_unicode_range2: u32,
723
    pub ul_unicode_range3: u32,
724
    pub ul_unicode_range4: u32,
725
    pub ach_vend_id: u32,
726

            
727
    // os/2 version 0 table (Option<i16>/Option<u16> - align 2)
728
    pub s_typo_ascender: OptionI16,
729
    pub s_typo_descender: OptionI16,
730
    pub s_typo_line_gap: OptionI16,
731
    pub us_win_ascent: OptionU16,
732
    pub us_win_descent: OptionU16,
733

            
734
    // +spec:font-metrics:d3b654 - cap-height and x-height metrics for visual text centering
735
    // (leading-trim) os/2 version 2 table
736
    pub sx_height: OptionI16,
737
    pub s_cap_height: OptionI16,
738
    pub us_default_char: OptionU16,
739
    pub us_break_char: OptionU16,
740
    pub us_max_context: OptionU16,
741

            
742
    // os/2 version 3 table
743
    pub us_lower_optical_point_size: OptionU16,
744
    pub us_upper_optical_point_size: OptionU16,
745

            
746
    // head table (u16/i16 - align 2)
747
    pub units_per_em: u16,
748
    pub font_flags: u16,
749
    pub x_min: i16,
750
    pub y_min: i16,
751
    pub x_max: i16,
752
    pub y_max: i16,
753

            
754
    // hhea table
755
    pub ascender: i16,
756
    pub descender: i16,
757
    pub line_gap: i16,
758
    pub advance_width_max: u16,
759
    pub min_left_side_bearing: i16,
760
    pub min_right_side_bearing: i16,
761
    pub x_max_extent: i16,
762
    pub caret_slope_rise: i16,
763
    pub caret_slope_run: i16,
764
    pub caret_offset: i16,
765
    pub num_h_metrics: u16,
766

            
767
    // os/2 table (u16/i16 fields)
768
    pub x_avg_char_width: i16,
769
    pub us_weight_class: u16,
770
    pub us_width_class: u16,
771
    pub fs_type: u16,
772
    pub y_subscript_x_size: i16,
773
    pub y_subscript_y_size: i16,
774
    pub y_subscript_x_offset: i16,
775
    pub y_subscript_y_offset: i16,
776
    pub y_superscript_x_size: i16,
777
    pub y_superscript_y_size: i16,
778
    pub y_superscript_x_offset: i16,
779
    pub y_superscript_y_offset: i16,
780
    pub y_strikeout_size: i16,
781
    pub y_strikeout_position: i16,
782
    pub s_family_class: i16,
783
    pub fs_selection: u16,
784
    pub us_first_char_index: u16,
785
    pub us_last_char_index: u16,
786

            
787
    // panose (align 1 - last)
788
    pub panose: Panose,
789
}
790

            
791
impl Default for FontMetrics {
792
1
    fn default() -> Self {
793
1
        Self::zero()
794
1
    }
795
}
796

            
797
impl FontMetrics {
798
    /// Only for testing, zero-sized font, will always return 0 for every metric
799
    /// (`units_per_em = 1000`)
800
    #[must_use]
801
3
    pub const fn zero() -> Self {
802
3
        Self {
803
3
            ul_code_page_range1: OptionU32::None,
804
3
            ul_code_page_range2: OptionU32::None,
805
3
            ul_unicode_range1: 0,
806
3
            ul_unicode_range2: 0,
807
3
            ul_unicode_range3: 0,
808
3
            ul_unicode_range4: 0,
809
3
            ach_vend_id: 0,
810
3
            s_typo_ascender: OptionI16::None,
811
3
            s_typo_descender: OptionI16::None,
812
3
            s_typo_line_gap: OptionI16::None,
813
3
            us_win_ascent: OptionU16::None,
814
3
            us_win_descent: OptionU16::None,
815
3
            sx_height: OptionI16::None,
816
3
            s_cap_height: OptionI16::None,
817
3
            us_default_char: OptionU16::None,
818
3
            us_break_char: OptionU16::None,
819
3
            us_max_context: OptionU16::None,
820
3
            us_lower_optical_point_size: OptionU16::None,
821
3
            us_upper_optical_point_size: OptionU16::None,
822
3
            units_per_em: 1000,
823
3
            font_flags: 0,
824
3
            x_min: 0,
825
3
            y_min: 0,
826
3
            x_max: 0,
827
3
            y_max: 0,
828
3
            ascender: 0,
829
3
            descender: 0,
830
3
            line_gap: 0,
831
3
            advance_width_max: 0,
832
3
            min_left_side_bearing: 0,
833
3
            min_right_side_bearing: 0,
834
3
            x_max_extent: 0,
835
3
            caret_slope_rise: 0,
836
3
            caret_slope_run: 0,
837
3
            caret_offset: 0,
838
3
            num_h_metrics: 0,
839
3
            x_avg_char_width: 0,
840
3
            us_weight_class: 400,
841
3
            us_width_class: 5,
842
3
            fs_type: 0,
843
3
            y_subscript_x_size: 0,
844
3
            y_subscript_y_size: 0,
845
3
            y_subscript_x_offset: 0,
846
3
            y_subscript_y_offset: 0,
847
3
            y_superscript_x_size: 0,
848
3
            y_superscript_y_size: 0,
849
3
            y_superscript_x_offset: 0,
850
3
            y_superscript_y_offset: 0,
851
3
            y_strikeout_size: 0,
852
3
            y_strikeout_position: 0,
853
3
            s_family_class: 0,
854
3
            fs_selection: 0,
855
3
            us_first_char_index: 0,
856
3
            us_last_char_index: 0,
857
3
            panose: Panose::zero(),
858
3
        }
859
3
    }
860

            
861
    /// Returns the ascender value from the hhea table
862
    #[must_use]
863
3
    pub const fn get_ascender(&self) -> i16 {
864
3
        self.ascender
865
3
    }
866

            
867
    /// Returns the descender value from the hhea table
868
    #[must_use]
869
3
    pub const fn get_descender(&self) -> i16 {
870
3
        self.descender
871
3
    }
872

            
873
    /// Returns the line gap value from the hhea table
874
    #[must_use]
875
2
    pub const fn get_line_gap(&self) -> i16 {
876
2
        self.line_gap
877
2
    }
878

            
879
    /// Returns the maximum advance width from the hhea table
880
    #[must_use]
881
2
    pub const fn get_advance_width_max(&self) -> u16 {
882
2
        self.advance_width_max
883
2
    }
884

            
885
    /// Returns the minimum left side bearing from the hhea table
886
    #[must_use]
887
2
    pub const fn get_min_left_side_bearing(&self) -> i16 {
888
2
        self.min_left_side_bearing
889
2
    }
890

            
891
    /// Returns the minimum right side bearing from the hhea table
892
    #[must_use]
893
2
    pub const fn get_min_right_side_bearing(&self) -> i16 {
894
2
        self.min_right_side_bearing
895
2
    }
896

            
897
    /// Returns the `x_min` value from the head table
898
    #[must_use]
899
2
    pub const fn get_x_min(&self) -> i16 {
900
2
        self.x_min
901
2
    }
902

            
903
    /// Returns the `y_min` value from the head table
904
    #[must_use]
905
2
    pub const fn get_y_min(&self) -> i16 {
906
2
        self.y_min
907
2
    }
908

            
909
    /// Returns the `x_max` value from the head table
910
    #[must_use]
911
2
    pub const fn get_x_max(&self) -> i16 {
912
2
        self.x_max
913
2
    }
914

            
915
    /// Returns the `y_max` value from the head table
916
    #[must_use]
917
2
    pub const fn get_y_max(&self) -> i16 {
918
2
        self.y_max
919
2
    }
920

            
921
    /// Returns the maximum extent in the x direction from the hhea table
922
    #[must_use]
923
2
    pub const fn get_x_max_extent(&self) -> i16 {
924
2
        self.x_max_extent
925
2
    }
926

            
927
    /// Returns the average character width from the os/2 table
928
    #[must_use]
929
2
    pub const fn get_x_avg_char_width(&self) -> i16 {
930
2
        self.x_avg_char_width
931
2
    }
932

            
933
    /// Returns the subscript x size from the os/2 table
934
    #[must_use]
935
2
    pub const fn get_y_subscript_x_size(&self) -> i16 {
936
2
        self.y_subscript_x_size
937
2
    }
938

            
939
    /// Returns the subscript y size from the os/2 table
940
    #[must_use]
941
2
    pub const fn get_y_subscript_y_size(&self) -> i16 {
942
2
        self.y_subscript_y_size
943
2
    }
944

            
945
    /// Returns the subscript x offset from the os/2 table
946
    #[must_use]
947
2
    pub const fn get_y_subscript_x_offset(&self) -> i16 {
948
2
        self.y_subscript_x_offset
949
2
    }
950

            
951
    /// Returns the subscript y offset from the os/2 table
952
    #[must_use]
953
2
    pub const fn get_y_subscript_y_offset(&self) -> i16 {
954
2
        self.y_subscript_y_offset
955
2
    }
956

            
957
    /// Returns the superscript x size from the os/2 table
958
    #[must_use]
959
2
    pub const fn get_y_superscript_x_size(&self) -> i16 {
960
2
        self.y_superscript_x_size
961
2
    }
962

            
963
    /// Returns the superscript y size from the os/2 table
964
    #[must_use]
965
2
    pub const fn get_y_superscript_y_size(&self) -> i16 {
966
2
        self.y_superscript_y_size
967
2
    }
968

            
969
    /// Returns the superscript x offset from the os/2 table
970
    #[must_use]
971
2
    pub const fn get_y_superscript_x_offset(&self) -> i16 {
972
2
        self.y_superscript_x_offset
973
2
    }
974

            
975
    /// Returns the superscript y offset from the os/2 table
976
    #[must_use]
977
2
    pub const fn get_y_superscript_y_offset(&self) -> i16 {
978
2
        self.y_superscript_y_offset
979
2
    }
980

            
981
    /// Returns the strikeout size from the os/2 table
982
    #[must_use]
983
2
    pub const fn get_y_strikeout_size(&self) -> i16 {
984
2
        self.y_strikeout_size
985
2
    }
986

            
987
    /// Returns the strikeout position from the os/2 table
988
    #[must_use]
989
2
    pub const fn get_y_strikeout_position(&self) -> i16 {
990
2
        self.y_strikeout_position
991
2
    }
992

            
993
    /// Returns whether typographic metrics should be used (from `fs_selection` flag)
994
    #[must_use]
995
20
    pub const fn use_typo_metrics(&self) -> bool {
996
        // Bit 7 of fs_selection indicates USE_TYPO_METRICS
997
20
        (self.fs_selection & 0x0080) != 0
998
20
    }
999
}
#[cfg(all(test, feature = "parser"))]
mod tests {
    use super::*;
    #[test]
1
    fn test_parse_font_weight_keywords() {
1
        assert_eq!(
1
            parse_font_weight("normal").unwrap(),
            StyleFontWeight::Normal
        );
1
        assert_eq!(parse_font_weight("bold").unwrap(), StyleFontWeight::Bold);
1
        assert_eq!(
1
            parse_font_weight("lighter").unwrap(),
            StyleFontWeight::Lighter
        );
1
        assert_eq!(
1
            parse_font_weight("bolder").unwrap(),
            StyleFontWeight::Bolder
        );
1
    }
    #[test]
1
    fn test_parse_font_weight_numbers() {
1
        assert_eq!(parse_font_weight("100").unwrap(), StyleFontWeight::W100);
1
        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
1
        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
1
        assert_eq!(parse_font_weight("900").unwrap(), StyleFontWeight::W900);
1
    }
    #[test]
1
    fn test_parse_font_weight_invalid() {
1
        assert!(parse_font_weight("thin").is_err());
1
        assert!(parse_font_weight("").is_err());
1
        assert!(parse_font_weight("450").is_err());
1
        assert!(parse_font_weight("boldest").is_err());
1
    }
    #[test]
1
    fn test_parse_font_style() {
1
        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
1
        assert_eq!(parse_font_style("italic").unwrap(), StyleFontStyle::Italic);
1
        assert_eq!(
1
            parse_font_style("oblique").unwrap(),
            StyleFontStyle::Oblique
        );
1
        assert_eq!(
1
            parse_font_style("  italic  ").unwrap(),
            StyleFontStyle::Italic
        );
1
        assert!(parse_font_style("slanted").is_err());
1
    }
    #[test]
1
    fn test_parse_font_size() {
1
        assert_eq!(
1
            parse_style_font_size("16px").unwrap().inner,
1
            PixelValue::px(16.0)
        );
1
        assert_eq!(
1
            parse_style_font_size("1.2em").unwrap().inner,
1
            PixelValue::em(1.2)
        );
1
        assert_eq!(
1
            parse_style_font_size("12pt").unwrap().inner,
1
            PixelValue::pt(12.0)
        );
1
        assert_eq!(
1
            parse_style_font_size("120%").unwrap().inner,
1
            PixelValue::percent(120.0)
        );
1
        assert!(parse_style_font_size("medium").is_err());
1
    }
    #[test]
1
    fn test_parse_font_family() {
        // Single unquoted
1
        let result = parse_style_font_family("Arial").unwrap();
1
        assert_eq!(result.len(), 1);
1
        assert_eq!(
1
            result.as_slice()[0],
1
            StyleFontFamily::System("Arial".into())
        );
        // 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!(
1
            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!(
1
            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!(
1
            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!(
1
            result.as_slice()[0],
            StyleFontFamily::SystemType(SystemFontType::Ui)
        );
1
        assert_eq!(
1
            result.as_slice()[1],
1
            StyleFontFamily::System("Arial".into())
        );
1
        assert_eq!(
1
            result.as_slice()[2],
1
            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!(
1
            result.as_slice()[0],
1
            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
    }
}
#[cfg(test)]
#[allow(clippy::too_many_lines, clippy::float_cmp)]
mod autotest_generated {
    use std::collections::hash_map::DefaultHasher;
    use super::*;
    use crate::props::basic::{error::ParseIntError as CParseIntError, length::SizeMetric};
    fn hash_of<T: Hash>(value: &T) -> u64 {
        let mut hasher = DefaultHasher::new();
        value.hash(&mut hasher);
        hasher.finish()
    }
    /// Leaks nothing: the matching destructor below reconstructs the `Box`.
    fn boxed_font_data(value: u64) -> *const c_void {
        Box::into_raw(Box::new(value)).cast::<c_void>().cast_const()
    }
    extern "C" fn noop_destructor(_ptr: *mut c_void) {}
    // One counter per test: `cargo test` runs tests in parallel within a single
    // process, so a shared counter would race.
    static SINGLE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
    extern "C" fn single_counting_destructor(ptr: *mut c_void) {
        SINGLE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
        if !ptr.is_null() {
            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
        }
    }
    static CLONE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
    extern "C" fn clone_counting_destructor(ptr: *mut c_void) {
        CLONE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
        if !ptr.is_null() {
            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
        }
    }
    static MANY_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
    extern "C" fn many_counting_destructor(ptr: *mut c_void) {
        MANY_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
        if !ptr.is_null() {
            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
        }
    }
    // ---------------------------------------------------------------------
    // next_font_ref_id (private)
    // ---------------------------------------------------------------------
    #[test]
    fn next_font_ref_id_is_monotonic_and_never_zero() {
        let a = next_font_ref_id();
        let b = next_font_ref_id();
        // `id == 0` is the "un-initialised / raw-reconstructed" sentinel, so the
        // counter must never hand it out.
        assert!(a >= 1, "id 0 is reserved as the null-handle sentinel");
        assert!(b > a, "ids must be strictly increasing ({a} -> {b})");
    }
    // ---------------------------------------------------------------------
    // FontRef::new / FontRef::get_parsed
    // ---------------------------------------------------------------------
    #[test]
    fn font_ref_new_post_construction_invariants() {
        let ptr = boxed_font_data(0xDEAD);
        let font = FontRef::new(ptr, single_counting_destructor);
        assert_eq!(
            font.get_parsed(),
            ptr,
            "get_parsed must return the pointer passed to new()"
        );
        assert!(font.run_destructor);
        assert!(font.id >= 1);
        assert!(!font.copies.is_null());
        assert_eq!(unsafe { (*font.copies).load(AtomicOrdering::SeqCst) }, 1);
        assert_eq!(SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
        drop(font);
        assert_eq!(
            SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
            1,
            "the destructor must run exactly once when the last handle drops"
        );
    }
    #[test]
    fn font_ref_new_accepts_null_pointer_without_panicking() {
        let font = FontRef::new(core::ptr::null(), noop_destructor);
        assert!(font.get_parsed().is_null());
        assert!(font.id >= 1);
        // Debug must not choke on a null `parsed`.
        let dbg = format!("{font:?}");
        assert!(
            dbg.starts_with("FontRef(0x0"),
            "unexpected Debug output: {dbg}"
        );
        assert!(dbg.contains("copies: 1"), "unexpected Debug output: {dbg}");
    }
    #[test]
    fn font_ref_clone_shares_identity_and_defers_the_destructor() {
        let ptr = boxed_font_data(42);
        let original = FontRef::new(ptr, clone_counting_destructor);
        let copy = original.clone();
        assert_eq!(original, copy, "shallow clones are the same font");
        assert_eq!(original.id, copy.id);
        assert_eq!(hash_of(&original), hash_of(&copy));
        assert_eq!(original.cmp(&copy), Ordering::Equal);
        assert_eq!(original.get_parsed(), copy.get_parsed());
        assert_eq!(
            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
            2
        );
        drop(copy);
        assert_eq!(
            CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
            0,
            "dropping one of two handles must not free the parsed data"
        );
        drop(original);
        assert_eq!(CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
    }
    #[test]
    fn font_ref_many_clones_run_the_destructor_exactly_once() {
        let ptr = boxed_font_data(7);
        let original = FontRef::new(ptr, many_counting_destructor);
        let clones: Vec<FontRef> = (0..1000).map(|_| original.clone()).collect();
        assert_eq!(
            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
            1001
        );
        assert!(clones.iter().all(|c| *c == original));
        drop(clones);
        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
        drop(original);
        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
    }
    #[test]
    fn font_ref_identity_is_the_id_not_the_pointer() {
        // Two independently-constructed handles over the *same* pointer value must
        // NOT compare equal — that is the whole point of the `id` field (a freed
        // font's heap address can be reused by a later font).
        let a = FontRef::new(core::ptr::null(), noop_destructor);
        let b = FontRef::new(core::ptr::null(), noop_destructor);
        assert_eq!(a.get_parsed(), b.get_parsed(), "same (null) pointer");
        assert_ne!(a, b, "same pointer must not forge identity");
        assert_ne!(a.id, b.id);
        assert_ne!(hash_of(&a), hash_of(&b));
        assert_eq!(
            a.cmp(&b),
            Ordering::Less,
            "ids are handed out in increasing order"
        );
        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
    }
    #[test]
    fn font_ref_raw_zero_handle_is_drop_safe() {
        // A handle reconstructed from raw parts (id == 0, no refcount) must not be
        // dereferenced by Debug/Drop.
        let make = || FontRef {
            parsed: core::ptr::null(),
            copies: core::ptr::null(),
            id: 0,
            run_destructor: false,
            parsed_destructor: noop_destructor,
        };
        let raw = make();
        let raw2 = make();
        assert!(raw.get_parsed().is_null());
        assert_eq!(format!("{raw:?}"), "FontRef(0x0)");
        assert_eq!(raw, raw2, "both carry the id==0 sentinel");
        let cloned = raw.clone();
        assert_eq!(cloned.id, 0);
        assert!(
            cloned.copies.is_null(),
            "cloning must not allocate a refcount for a raw handle"
        );
        drop(cloned);
        drop(raw2);
        drop(raw); // must not double-free / deref null
    }
    // ---------------------------------------------------------------------
    // StyleFontFamily::as_string
    // ---------------------------------------------------------------------
    #[test]
    fn style_font_family_as_string_quotes_only_when_whitespace_is_present() {
        assert_eq!(StyleFontFamily::System("Arial".into()).as_string(), "Arial");
        assert_eq!(
            StyleFontFamily::System("Times New Roman".into()).as_string(),
            "\"Times New Roman\""
        );
        // An empty family name is not quoted (it has no whitespace).
        assert_eq!(StyleFontFamily::System("".into()).as_string(), "");
        // Tabs / newlines count as whitespace.
        assert_eq!(
            StyleFontFamily::System("a\tb".into()).as_string(),
            "\"a\tb\""
        );
        assert_eq!(
            StyleFontFamily::System("a\nb".into()).as_string(),
            "\"a\nb\""
        );
    }
    #[test]
    fn style_font_family_as_string_handles_unicode() {
        // No ASCII whitespace -> unquoted, bytes preserved.
        assert_eq!(
            StyleFontFamily::System("日本語".into()).as_string(),
            "日本語"
        );
        assert_eq!(
            StyleFontFamily::System("\u{1F600}".into()).as_string(),
            "\u{1F600}"
        );
        // Combining marks are not whitespace.
        assert_eq!(
            StyleFontFamily::System("e\u{0301}".into()).as_string(),
            "e\u{0301}"
        );
        // U+00A0 NO-BREAK SPACE *is* `char::is_whitespace`, so it gets quoted.
        assert_eq!(
            StyleFontFamily::System("a\u{00A0}b".into()).as_string(),
            "\"a\u{00A0}b\""
        );
    }
    #[test]
    fn style_font_family_as_string_file_and_systemtype_and_ref() {
        // `File` is never quoted, even when it contains whitespace.
        assert_eq!(
            StyleFontFamily::File("my font.ttf".into()).as_string(),
            "url(my font.ttf)"
        );
        assert_eq!(
            StyleFontFamily::SystemType(SystemFontType::MonospaceBold).as_string(),
            "system:monospace:bold"
        );
        let ptr = 0xdead_beef_usize as *const c_void;
        let fam = StyleFontFamily::Ref(FontRef::new(ptr, noop_destructor));
        assert_eq!(fam.as_string(), "font-ref(0xdeadbeef)");
    }
    #[test]
    fn style_font_family_as_string_on_huge_name_does_not_panic() {
        let huge = "x".repeat(1_000_000);
        let fam = StyleFontFamily::System(huge.as_str().into());
        assert_eq!(fam.as_string().len(), 1_000_000);
    }
    // ---------------------------------------------------------------------
    // parse_font_weight
    // ---------------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_valid_minimal_and_full_roundtrip() {
        assert_eq!(
            parse_font_weight("normal").unwrap(),
            StyleFontWeight::Normal
        );
        for weight in [
            StyleFontWeight::Lighter,
            StyleFontWeight::W100,
            StyleFontWeight::W200,
            StyleFontWeight::W300,
            StyleFontWeight::Normal,
            StyleFontWeight::W500,
            StyleFontWeight::W600,
            StyleFontWeight::Bold,
            StyleFontWeight::W800,
            StyleFontWeight::W900,
            StyleFontWeight::Bolder,
        ] {
            let css = weight.print_as_css_value();
            assert_eq!(
                parse_font_weight(&css).unwrap(),
                weight,
                "encode==decode failed for {weight:?} (printed as {css:?})"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_numeric_aliases_collapse_onto_keywords() {
        // 400/700 are accepted but re-print as keywords, so the *string* round-trip
        // is deliberately lossy in one direction.
        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
        assert_eq!(StyleFontWeight::Normal.print_as_css_value(), "normal");
        assert_eq!(StyleFontWeight::Bold.print_as_css_value(), "bold");
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_rejects_empty_and_whitespace_only() {
        for input in ["", " ", "   ", "\t\n", "\r\n\t "] {
            let err = parse_font_weight(input).unwrap_err();
            assert!(
                matches!(
                    err,
                    CssFontWeightParseError::InvalidValue(InvalidValueErr(""))
                ),
                "expected trimmed InvalidValue(\"\") for {input:?}, got {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_rejects_garbage_and_reports_the_trimmed_input() {
        assert_eq!(
            parse_font_weight("  thin  ").unwrap_err(),
            CssFontWeightParseError::InvalidValue(InvalidValueErr("thin")),
            "the error must carry the trimmed input"
        );
        for input in [
            "thin",
            "boldest",
            "bold;garbage",
            "normal!",
            "\u{0}\u{1}\u{7f}",
            "-",
        ] {
            assert!(
                parse_font_weight(input).is_err(),
                "{input:?} must not parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_rejects_boundary_numbers_without_ever_yielding_invalidnumber() {
        // `parse_font_weight` only matches literal keyword/number strings; it never
        // calls `str::parse`, so the `InvalidNumber` variant is unreachable here.
        for input in [
            "0",
            "-0",
            "450",
            "1000",
            "0400",
            "+400",
            "400.0",
            "4e2",
            "9223372036854775807",
            "-9223372036854775808",
            "18446744073709551616",
            "NaN",
            "inf",
            "-inf",
            "1e309",
        ] {
            let err = parse_font_weight(input).unwrap_err();
            assert!(
                matches!(err, CssFontWeightParseError::InvalidValue(_)),
                "{input:?} should be an InvalidValue, got {err:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_trims_unicode_whitespace() {
        // `str::trim` uses `char::is_whitespace`, so U+00A0 / U+2028 are stripped —
        // stricter CSS tokenisers would not do this.
        assert_eq!(
            parse_font_weight("\u{00A0}bold\u{00A0}").unwrap(),
            StyleFontWeight::Bold
        );
        assert_eq!(
            parse_font_weight("\u{2028}400").unwrap(),
            StyleFontWeight::Normal
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_unicode_garbage_is_rejected_and_displayable() {
        let err = parse_font_weight("\u{1F600}").unwrap_err();
        assert_eq!(
            err,
            CssFontWeightParseError::InvalidValue(InvalidValueErr("\u{1F600}"))
        );
        // Display / Debug must not panic on multibyte payloads.
        let msg = format!("{err}");
        assert!(msg.contains('\u{1F600}'), "unexpected message: {msg}");
        assert!(!format!("{err:?}").is_empty());
        assert!(
            parse_font_weight("bold\u{0301}").is_err(),
            "combining mark must not be trimmed"
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_weight_survives_extremely_long_and_deeply_nested_input() {
        let long = "bold".repeat(250_000); // 1_000_000 chars
        assert!(parse_font_weight(&long).is_err());
        let nested = "(".repeat(10_000);
        assert!(parse_font_weight(&nested).is_err());
        let long_digits = "9".repeat(100_000);
        assert!(parse_font_weight(&long_digits).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_font_style
    // ---------------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_style_valid_minimal_and_full_roundtrip() {
        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
        for style in [
            StyleFontStyle::Normal,
            StyleFontStyle::Italic,
            StyleFontStyle::Oblique,
        ] {
            let css = style.print_as_css_value();
            assert_eq!(
                parse_font_style(&css).unwrap(),
                style,
                "encode==decode failed for {style:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_style_rejects_empty_whitespace_and_garbage() {
        for input in ["", "   ", "\t\n"] {
            assert_eq!(
                parse_font_style(input).unwrap_err(),
                CssFontStyleParseError::InvalidValue(InvalidValueErr(""))
            );
        }
        for input in [
            "slanted",
            "italics",
            "ITALIC",
            "italic;garbage",
            "oblique 14deg",
            "0",
            "-0",
            "NaN",
            "inf",
            "9223372036854775807",
        ] {
            assert!(parse_font_style(input).is_err(), "{input:?} must not parse");
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_style_leading_trailing_junk_and_unicode() {
        assert_eq!(
            parse_font_style("  italic  ").unwrap(),
            StyleFontStyle::Italic
        );
        assert_eq!(
            parse_font_style(" italic;").unwrap_err(),
            CssFontStyleParseError::InvalidValue(InvalidValueErr("italic;"))
        );
        let err = parse_font_style("\u{1F600}\u{0301}").unwrap_err();
        assert!(!format!("{err}").is_empty());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_font_style_survives_extremely_long_and_deeply_nested_input() {
        let long = "italic".repeat(200_000); // 1_200_000 chars
        assert!(parse_font_style(&long).is_err());
        let nested = "[".repeat(10_000);
        assert!(parse_font_style(&nested).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_style_font_size
    // ---------------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_valid_minimal_and_metric_roundtrip() {
        assert_eq!(
            parse_style_font_size("16px").unwrap().inner,
            PixelValue::px(16.0)
        );
        // NOTE: `SizeMetric::Vmin` is deliberately excluded — see
        // `parse_style_font_size_vmin_is_shadowed_by_the_in_suffix`.
        for metric in [
            SizeMetric::Px,
            SizeMetric::Pt,
            SizeMetric::Em,
            SizeMetric::Rem,
            SizeMetric::In,
            SizeMetric::Cm,
            SizeMetric::Mm,
            SizeMetric::Percent,
            SizeMetric::Vw,
            SizeMetric::Vh,
            SizeMetric::Vmax,
        ] {
            let size = StyleFontSize {
                inner: PixelValue::from_metric(metric, 12.0),
            };
            let css = size.print_as_css_value();
            assert_eq!(
                parse_style_font_size(&css).unwrap(),
                size,
                "encode==decode failed for {metric:?} (printed as {css:?})"
            );
        }
        // The default (12pt) must survive a print/parse round-trip too.
        let default = StyleFontSize::default();
        assert_eq!(default.print_as_css_value(), "12pt");
        assert_eq!(parse_style_font_size("12pt").unwrap(), default);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_vmin_is_shadowed_by_the_in_suffix() {
        // FIXED (was a characterization of the bug): "in" used to be tested before
        // "vmin", so "12vmin" stripped to "12vm" and failed to parse. The suffix
        // table in css/src/props/basic/pixel.rs now orders "vmin" ahead of "in", so
        // font-size in vmin round-trips.
        let size = StyleFontSize {
            inner: PixelValue::from_metric(SizeMetric::Vmin, 12.0),
        };
        let css = size.print_as_css_value();
        assert_eq!(css, "12vmin");
        assert_eq!(
            parse_style_font_size(&css).unwrap().inner,
            PixelValue::from_metric(SizeMetric::Vmin, 12.0)
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_rejects_empty_and_whitespace_only() {
        for input in ["", " ", "   ", "\t\n"] {
            let err = parse_style_font_size(input).unwrap_err();
            assert_eq!(
                err,
                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::EmptyString),
                "unexpected error for {input:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_rejects_garbage_and_bare_units() {
        let err = parse_style_font_size("px").unwrap_err();
        assert!(
            matches!(
                err,
                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
                    "px",
                    SizeMetric::Px
                ))
            ),
            "expected NoValueGiven, got {err:?}"
        );
        for input in [
            "medium",
            "larger",
            "16PX", // unit matching is case-sensitive
            "16px;junk",
            "16 px junk",
            "\u{1F600}",
            "--",
            "px16",
        ] {
            assert!(
                parse_style_font_size(input).is_err(),
                "{input:?} must not parse"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_accepts_unitless_numbers_as_px() {
        // Deviation from CSS (which only allows a unitless `0`): any bare number is
        // accepted and silently treated as `px`.
        assert_eq!(
            parse_style_font_size("0").unwrap().inner,
            PixelValue::px(0.0)
        );
        assert_eq!(
            parse_style_font_size("16").unwrap().inner,
            PixelValue::px(16.0)
        );
        assert_eq!(
            parse_style_font_size("-16").unwrap().inner,
            PixelValue::px(-16.0)
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_boundary_numbers_saturate_instead_of_panicking() {
        // Signed zero collapses to +0.
        assert_eq!(
            parse_style_font_size("-0px").unwrap().inner.number.get(),
            0.0
        );
        assert_eq!(
            parse_style_font_size("0px").unwrap().inner.number.get(),
            0.0
        );
        // f32 overflow -> +/-inf -> the isize encoding saturates (no UB, no panic).
        let big = parse_style_font_size("1e40px").unwrap().inner.number.get();
        assert!(
            big.is_finite() && big > 0.0,
            "expected a saturated finite value, got {big}"
        );
        let small = parse_style_font_size("-1e40px").unwrap().inner.number.get();
        assert!(
            small.is_finite() && small < 0.0,
            "expected a saturated finite value, got {small}"
        );
        // Literal infinities are accepted by `f32::from_str` and saturate as well.
        let inf = parse_style_font_size("inf").unwrap().inner.number.get();
        assert!(inf.is_finite() && inf > 0.0);
        let neg_inf = parse_style_font_size("-infinitypx")
            .unwrap()
            .inner
            .number
            .get();
        assert!(neg_inf.is_finite() && neg_inf < 0.0);
        // i64::MAX / u64::MAX as bare numbers: no overflow panic.
        for input in ["9223372036854775807", "18446744073709551615px"] {
            let v = parse_style_font_size(input).unwrap().inner.number.get();
            assert!(v.is_finite(), "{input:?} produced {v}");
        }
        // Sub-milli precision is truncated by the fixed-point encoding, not rounded.
        assert_eq!(
            parse_style_font_size("16.0004px").unwrap().inner,
            PixelValue::px(16.0)
        );
        assert_eq!(
            parse_style_font_size("1e-40px").unwrap().inner.number.get(),
            0.0
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_nan_is_silently_coerced_to_zero() {
        // `f32::from_str` accepts "NaN", and the fixed-point cast maps NaN -> 0.
        // A stricter CSS parser would reject this outright; asserted as-is.
        let parsed = parse_style_font_size("NaN").unwrap();
        assert_eq!(parsed.inner.metric, SizeMetric::Px);
        assert_eq!(parsed.inner.number.get(), 0.0);
        assert_eq!(
            parse_style_font_size("nanpx").unwrap().inner.number.get(),
            0.0
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_leading_trailing_whitespace_is_trimmed() {
        assert_eq!(
            parse_style_font_size("  16px  ").unwrap().inner,
            PixelValue::px(16.0)
        );
        // Whitespace *between* number and unit is tolerated as well.
        assert_eq!(
            parse_style_font_size("16 px").unwrap().inner,
            PixelValue::px(16.0)
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_size_survives_extremely_long_and_deeply_nested_input() {
        let long_digits = "1".repeat(50_000);
        let parsed = parse_style_font_size(&long_digits).unwrap();
        assert!(parsed.inner.number.get().is_finite());
        let long_garbage = "z".repeat(1_000_000);
        assert!(parse_style_font_size(&long_garbage).is_err());
        let nested = "(".repeat(10_000);
        assert!(parse_style_font_size(&nested).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_style_font_family
    // ---------------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_valid_minimal() {
        let parsed = parse_style_font_family("Arial").unwrap();
        assert_eq!(parsed.len(), 1);
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("Arial".into())
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_never_returns_err() {
        // Every failure path inside the parser falls back to "treat it as an
        // unquoted family name", so the `Err` half of the signature (and with it
        // `CssStyleFontFamilyParseError`) is unreachable. Documented, not weakened.
        let nested = "(".repeat(10_000);
        let long = "x".repeat(1_000_000);
        let inputs: Vec<&str> = vec![
            "",
            "   ",
            "\t\n",
            ",",
            ",,,",
            "'unclosed",
            "\"unclosed",
            "\"Arial'",
            "'Arial\"",
            "\u{1F600}",
            "system:",
            "system:bogus",
            "url(x.ttf)",
            "font-ref(0xdeadbeef)",
            "\u{0}\u{7f}",
            &nested,
            &long,
        ];
        for input in inputs {
            assert!(
                parse_style_font_family(input).is_ok(),
                "parse_style_font_family unexpectedly failed for {:?}",
                &input[..input.len().min(32)]
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_empty_input_yields_one_empty_family() {
        // Deviation from CSS (an empty font-family list is invalid there): the
        // parser produces a single, empty `System` name instead of erroring.
        let parsed = parse_style_font_family("").unwrap();
        assert_eq!(parsed.len(), 1);
        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
        let parsed = parse_style_font_family("   ").unwrap();
        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
        let parsed = parse_style_font_family(",,,").unwrap();
        assert_eq!(parsed.len(), 4, "N commas produce N+1 (empty) families");
        assert!(parsed
            .iter()
            .all(|f| *f == StyleFontFamily::System("".into())));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_unclosed_quotes_keep_the_quote_character() {
        // `strip_quotes` errors, and the parser then keeps the *raw* token — so the
        // quote survives into the family name rather than surfacing UnclosedQuotes.
        let parsed = parse_style_font_family("'unclosed").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("'unclosed".into())
        );
        let parsed = parse_style_font_family("\"Arial'").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("\"Arial'".into())
        );
        // An empty quoted string strips down to an empty family name.
        let parsed = parse_style_font_family("\"\"").unwrap();
        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_system_prefix_is_case_sensitive_and_falls_back() {
        // Unknown `system:` types fall through to a literal family name.
        let parsed = parse_style_font_family("system:bogus").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("system:bogus".into())
        );
        let parsed = parse_style_font_family("system:").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("system:".into())
        );
        // Uppercase prefix is not recognised as a system font.
        let parsed = parse_style_font_family("SYSTEM:UI").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("SYSTEM:UI".into())
        );
        let parsed = parse_style_font_family("system:UI").unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("system:UI".into())
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_never_produces_file_or_ref_variants() {
        for input in [
            "url(x.ttf)",
            "font-ref(0x1)",
            "Arial",
            "system:ui",
            "'a'",
            "\u{1F600}",
        ] {
            let parsed = parse_style_font_family(input).unwrap();
            assert!(
                parsed.iter().all(|f| matches!(
                    f,
                    StyleFontFamily::System(_) | StyleFontFamily::SystemType(_)
                )),
                "the parser must only ever yield System/SystemType, got {parsed:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_handles_unicode_names() {
        let parsed = parse_style_font_family("日本語, \u{1F600}, e\u{0301}").unwrap();
        assert_eq!(parsed.len(), 3);
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("日本語".into())
        );
        assert_eq!(
            parsed.as_slice()[1],
            StyleFontFamily::System("\u{1F600}".into())
        );
        assert_eq!(
            parsed.as_slice()[2],
            StyleFontFamily::System("e\u{0301}".into())
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_style_font_family_survives_extremely_long_and_deeply_nested_input() {
        let huge_name = "x".repeat(1_000_000);
        let parsed = parse_style_font_family(&huge_name).unwrap();
        assert_eq!(parsed.len(), 1);
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System(huge_name.as_str().into())
        );
        let many = "Arial,".repeat(10_000);
        let parsed = parse_style_font_family(&many).unwrap();
        assert_eq!(parsed.len(), 10_001, "trailing comma adds one empty family");
        let nested = "(".repeat(10_000);
        let parsed = parse_style_font_family(&nested).unwrap();
        assert_eq!(parsed.len(), 1);
    }
    // ---------------------------------------------------------------------
    // as_string <-> parse_style_font_family round-trips
    // ---------------------------------------------------------------------
    #[cfg(feature = "parser")]
    #[test]
    fn style_font_family_as_string_roundtrips_through_the_parser() {
        for name in [
            "Arial",
            "Times New Roman",
            "",
            "日本語",
            "a\u{00A0}b",
            "Fo\"o",
            "serif",
        ] {
            let family = StyleFontFamily::System(name.into());
            let css = family.as_string();
            let parsed = parse_style_font_family(&css).unwrap();
            assert_eq!(parsed.len(), 1, "{name:?} printed as {css:?}");
            assert_eq!(
                parsed.as_slice()[0],
                family,
                "encode==decode failed for {name:?}"
            );
        }
        for ft in [
            SystemFontType::Ui,
            SystemFontType::UiBold,
            SystemFontType::Monospace,
            SystemFontType::MonospaceBold,
            SystemFontType::MonospaceItalic,
            SystemFontType::Title,
            SystemFontType::TitleBold,
            SystemFontType::Menu,
            SystemFontType::Small,
            SystemFontType::Serif,
            SystemFontType::SerifBold,
        ] {
            let family = StyleFontFamily::SystemType(ft);
            let parsed = parse_style_font_family(&family.as_string()).unwrap();
            assert_eq!(
                parsed.as_slice()[0],
                family,
                "encode==decode failed for {ft:?}"
            );
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn style_font_family_as_string_does_not_escape_commas() {
        // LOSSY: `as_string()` quotes on whitespace only, so a comma inside a family
        // name re-parses as two families. Asserted as-is; reported as a defect.
        let family = StyleFontFamily::System("Foo,Bar".into());
        assert_eq!(family.as_string(), "Foo,Bar");
        let parsed = parse_style_font_family(&family.as_string()).unwrap();
        assert_eq!(parsed.len(), 2);
    }
    #[cfg(feature = "parser")]
    #[test]
    fn style_font_family_file_and_ref_do_not_roundtrip() {
        // `url(...)` / `font-ref(...)` are printable but not parseable: they come
        // back as plain `System` names.
        let file = StyleFontFamily::File("f.ttf".into());
        let parsed = parse_style_font_family(&file.as_string()).unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("url(f.ttf)".into())
        );
        let font = StyleFontFamily::Ref(FontRef::new(core::ptr::null(), noop_destructor));
        let parsed = parse_style_font_family(&font.as_string()).unwrap();
        assert_eq!(
            parsed.as_slice()[0],
            StyleFontFamily::System("font-ref(0x0)".into())
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn style_font_family_vec_print_as_css_value_roundtrips() {
        let css = "Arial, \"Times New Roman\", system:ui";
        let parsed = parse_style_font_family(css).unwrap();
        assert_eq!(parsed.print_as_css_value(), css);
        let reparsed = parse_style_font_family(&parsed.print_as_css_value()).unwrap();
        assert_eq!(reparsed, parsed, "encode==decode failed for a font stack");
    }
    // ---------------------------------------------------------------------
    // Error to_contained / to_shared
    // ---------------------------------------------------------------------
    #[test]
    fn css_font_weight_parse_error_invalid_value_roundtrips() {
        for value in ["", "thin", "\u{1F600}", "a\u{0}b"] {
            let shared = CssFontWeightParseError::InvalidValue(InvalidValueErr(value));
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                CssFontWeightParseErrorOwned::InvalidValue(InvalidValueErrOwned {
                    value: value.into()
                })
            );
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_contained/to_shared must round-trip"
            );
            assert!(!format!("{shared}").is_empty());
        }
    }
    #[test]
    fn css_font_weight_parse_error_invalid_number_roundtrips() {
        let cases = [
            "".parse::<i32>().unwrap_err(),
            "x".parse::<i32>().unwrap_err(),
            "99999999999999999999".parse::<i32>().unwrap_err(),
            "-99999999999999999999".parse::<i32>().unwrap_err(),
        ];
        for err in cases {
            let shared = CssFontWeightParseError::InvalidNumber(err);
            let owned = shared.to_contained();
            assert_eq!(
                owned.to_shared(),
                shared,
                "kind must survive the FFI round-trip"
            );
            assert!(!format!("{shared}").is_empty());
        }
    }
    #[test]
    fn css_font_weight_parse_error_zero_kind_roundtrip_is_lossy() {
        // `IntErrorKind::Zero` cannot be reconstructed on stable Rust — the source
        // documents this; assert the documented degradation to InvalidDigit.
        let zero_err = "0".parse::<core::num::NonZeroU32>().unwrap_err();
        let shared = CssFontWeightParseError::InvalidNumber(zero_err);
        let owned = shared.to_contained();
        assert_eq!(
            owned,
            CssFontWeightParseErrorOwned::InvalidNumber(CParseIntError::Zero),
            "the Zero kind must survive into the owned form"
        );
        assert_ne!(
            owned.to_shared(),
            shared,
            "to_std() cannot rebuild a Zero-kind ParseIntError (documented)"
        );
    }
    #[test]
    fn css_font_style_parse_error_roundtrips() {
        for value in ["", "slanted", "\u{1F600}"] {
            let shared = CssFontStyleParseError::InvalidValue(InvalidValueErr(value));
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                CssFontStyleParseErrorOwned::InvalidValue(InvalidValueErrOwned {
                    value: value.into()
                })
            );
            assert_eq!(owned.to_shared(), shared);
            assert!(!format!("{shared}").is_empty());
        }
    }
    #[test]
    fn css_style_font_size_parse_error_roundtrips_every_variant() {
        let cases = [
            CssPixelValueParseError::EmptyString,
            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
            CssPixelValueParseError::NoValueGiven("%", SizeMetric::Percent),
            CssPixelValueParseError::ValueParseErr("abc".parse::<f32>().unwrap_err(), "abc"),
            CssPixelValueParseError::ValueParseErr("".parse::<f32>().unwrap_err(), ""),
            CssPixelValueParseError::InvalidPixelValue("medium"),
            CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
        ];
        for inner in cases {
            let shared = CssStyleFontSizeParseError::PixelValue(inner);
            let owned = shared.to_contained();
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_contained/to_shared must round-trip"
            );
            assert!(!format!("{shared}").is_empty());
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn css_style_font_family_parse_error_roundtrips_every_variant() {
        let cases = [
            CssStyleFontFamilyParseError::InvalidStyleFontFamily(""),
            CssStyleFontFamilyParseError::InvalidStyleFontFamily("bogus"),
            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\"Arial")),
            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\u{1F600}")),
        ];
        for shared in cases {
            let owned = shared.to_contained();
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_contained/to_shared must round-trip"
            );
            assert!(!format!("{shared}").is_empty());
        }
    }
    // ---------------------------------------------------------------------
    // FormatAsRustCode / defaults / ordering
    // ---------------------------------------------------------------------
    #[test]
    fn font_enum_defaults_and_ordering() {
        assert_eq!(StyleFontWeight::default(), StyleFontWeight::Normal);
        assert_eq!(StyleFontStyle::default(), StyleFontStyle::Normal);
        assert_eq!(StyleFontSize::default().inner, PixelValue::const_pt(12));
        // Derived Ord follows declaration order (numeric weights are ordered).
        assert!(StyleFontWeight::W100 < StyleFontWeight::W900);
        assert!(StyleFontWeight::Normal < StyleFontWeight::Bold);
        assert!(StyleFontWeight::Lighter < StyleFontWeight::W100);
        assert!(StyleFontWeight::Bolder > StyleFontWeight::W900);
    }
    #[test]
    fn format_as_rust_code_matches_the_debug_variant_names() {
        for weight in [
            StyleFontWeight::Lighter,
            StyleFontWeight::W100,
            StyleFontWeight::W200,
            StyleFontWeight::W300,
            StyleFontWeight::Normal,
            StyleFontWeight::W500,
            StyleFontWeight::W600,
            StyleFontWeight::Bold,
            StyleFontWeight::W800,
            StyleFontWeight::W900,
            StyleFontWeight::Bolder,
        ] {
            assert_eq!(
                weight.format_as_rust_code(0),
                format!("StyleFontWeight::{weight:?}")
            );
        }
        for style in [
            StyleFontStyle::Normal,
            StyleFontStyle::Italic,
            StyleFontStyle::Oblique,
        ] {
            assert_eq!(
                style.format_as_rust_code(0),
                format!("StyleFontStyle::{style:?}")
            );
        }
        assert_eq!(
            StyleFontFamily::SystemType(SystemFontType::Ui).format_as_rust_code(0),
            "StyleFontFamily::SystemType(SystemFontType::Ui)"
        );
        assert!(StyleFontFamily::System("Arial".into())
            .format_as_rust_code(0)
            .starts_with("StyleFontFamily::System(STRING_"));
        assert!(StyleFontFamily::File("a.ttf".into())
            .format_as_rust_code(0)
            .starts_with("StyleFontFamily::File(STRING_"));
    }
    // ---------------------------------------------------------------------
    // Panose::zero / FontMetrics::zero + getters
    // ---------------------------------------------------------------------
    #[test]
    fn panose_zero_is_the_neutral_element() {
        const P: Panose = Panose::zero();
        assert_eq!(P, Panose::default());
        assert_eq!(hash_of(&P), hash_of(&Panose::default()));
        assert_eq!(P.family_type, 0);
        assert_eq!(P.serif_style, 0);
        assert_eq!(P.weight, 0);
        assert_eq!(P.proportion, 0);
        assert_eq!(P.contrast, 0);
        assert_eq!(P.stroke_variation, 0);
        assert_eq!(P.arm_style, 0);
        assert_eq!(P.letterform, 0);
        assert_eq!(P.midline, 0);
        assert_eq!(P.x_height, 0);
        let mut max = Panose::zero();
        max.family_type = u8::MAX;
        assert!(max > P, "derived Ord must order by the first field");
    }
    #[test]
    fn font_metrics_zero_invariants() {
        const M: FontMetrics = FontMetrics::zero();
        assert_eq!(M, FontMetrics::default());
        // Documented: a zero font still declares a sane em square / weight class.
        assert_eq!(M.units_per_em, 1000);
        assert_eq!(M.us_weight_class, 400);
        assert_eq!(M.us_width_class, 5);
        assert_eq!(M.panose, Panose::zero());
        assert_eq!(M.get_ascender(), 0);
        assert_eq!(M.get_descender(), 0);
        assert_eq!(M.get_line_gap(), 0);
        assert_eq!(M.get_advance_width_max(), 0);
        assert_eq!(M.get_min_left_side_bearing(), 0);
        assert_eq!(M.get_min_right_side_bearing(), 0);
        assert_eq!(M.get_x_min(), 0);
        assert_eq!(M.get_y_min(), 0);
        assert_eq!(M.get_x_max(), 0);
        assert_eq!(M.get_y_max(), 0);
        assert_eq!(M.get_x_max_extent(), 0);
        assert_eq!(M.get_x_avg_char_width(), 0);
        assert_eq!(M.get_y_subscript_x_size(), 0);
        assert_eq!(M.get_y_subscript_y_size(), 0);
        assert_eq!(M.get_y_subscript_x_offset(), 0);
        assert_eq!(M.get_y_subscript_y_offset(), 0);
        assert_eq!(M.get_y_superscript_x_size(), 0);
        assert_eq!(M.get_y_superscript_y_size(), 0);
        assert_eq!(M.get_y_superscript_x_offset(), 0);
        assert_eq!(M.get_y_superscript_y_offset(), 0);
        assert_eq!(M.get_y_strikeout_size(), 0);
        assert_eq!(M.get_y_strikeout_position(), 0);
        assert!(!M.use_typo_metrics());
        assert!(matches!(M.ul_code_page_range1, OptionU32::None));
        assert!(matches!(M.ul_code_page_range2, OptionU32::None));
        assert!(matches!(M.s_typo_ascender, OptionI16::None));
        assert!(matches!(M.s_typo_descender, OptionI16::None));
        assert!(matches!(M.s_typo_line_gap, OptionI16::None));
        assert!(matches!(M.us_win_ascent, OptionU16::None));
        assert!(matches!(M.us_win_descent, OptionU16::None));
        assert!(matches!(M.sx_height, OptionI16::None));
        assert!(matches!(M.s_cap_height, OptionI16::None));
    }
    #[test]
    fn font_metrics_getters_return_extreme_values_unchanged() {
        let mut m = FontMetrics::zero();
        m.ascender = i16::MAX;
        m.descender = i16::MIN;
        m.line_gap = i16::MIN;
        m.advance_width_max = u16::MAX;
        m.min_left_side_bearing = i16::MIN;
        m.min_right_side_bearing = i16::MAX;
        m.x_min = i16::MIN;
        m.y_min = i16::MIN;
        m.x_max = i16::MAX;
        m.y_max = i16::MAX;
        m.x_max_extent = i16::MAX;
        m.x_avg_char_width = i16::MIN;
        m.y_subscript_x_size = i16::MAX;
        m.y_subscript_y_size = i16::MIN;
        m.y_subscript_x_offset = i16::MAX;
        m.y_subscript_y_offset = i16::MIN;
        m.y_superscript_x_size = i16::MAX;
        m.y_superscript_y_size = i16::MIN;
        m.y_superscript_x_offset = i16::MAX;
        m.y_superscript_y_offset = i16::MIN;
        m.y_strikeout_size = i16::MAX;
        m.y_strikeout_position = i16::MIN;
        // Getters are plain field reads: no clamping, no sign flips, no panics.
        assert_eq!(m.get_ascender(), i16::MAX);
        assert_eq!(m.get_descender(), i16::MIN);
        assert_eq!(m.get_line_gap(), i16::MIN);
        assert_eq!(m.get_advance_width_max(), u16::MAX);
        assert_eq!(m.get_min_left_side_bearing(), i16::MIN);
        assert_eq!(m.get_min_right_side_bearing(), i16::MAX);
        assert_eq!(m.get_x_min(), i16::MIN);
        assert_eq!(m.get_y_min(), i16::MIN);
        assert_eq!(m.get_x_max(), i16::MAX);
        assert_eq!(m.get_y_max(), i16::MAX);
        assert_eq!(m.get_x_max_extent(), i16::MAX);
        assert_eq!(m.get_x_avg_char_width(), i16::MIN);
        assert_eq!(m.get_y_subscript_x_size(), i16::MAX);
        assert_eq!(m.get_y_subscript_y_size(), i16::MIN);
        assert_eq!(m.get_y_subscript_x_offset(), i16::MAX);
        assert_eq!(m.get_y_subscript_y_offset(), i16::MIN);
        assert_eq!(m.get_y_superscript_x_size(), i16::MAX);
        assert_eq!(m.get_y_superscript_y_size(), i16::MIN);
        assert_eq!(m.get_y_superscript_x_offset(), i16::MAX);
        assert_eq!(m.get_y_superscript_y_offset(), i16::MIN);
        assert_eq!(m.get_y_strikeout_size(), i16::MAX);
        assert_eq!(m.get_y_strikeout_position(), i16::MIN);
        // An "inverted" font (ascender < descender) is accepted verbatim — the
        // getters do no validation.
        assert!(m.get_ascender() > m.get_descender());
    }
    #[test]
    fn font_metrics_use_typo_metrics_reads_exactly_bit_7() {
        let mut m = FontMetrics::zero();
        for bit in 0..16u16 {
            m.fs_selection = 1 << bit;
            assert_eq!(
                m.use_typo_metrics(),
                bit == 7,
                "fs_selection bit {bit} must not affect USE_TYPO_METRICS"
            );
        }
        m.fs_selection = u16::MAX;
        assert!(m.use_typo_metrics());
        m.fs_selection = u16::MAX ^ 0x0080;
        assert!(!m.use_typo_metrics(), "clearing bit 7 must clear the flag");
        m.fs_selection = 0;
        assert!(!m.use_typo_metrics());
    }
}