Skip to main content

azul_css/props/basic/
font.rs

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
7use alloc::{
8    boxed::Box,
9    string::{String, ToString},
10    vec::Vec,
11};
12use 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")]
22use crate::props::basic::parse::{strip_quotes, UnclosedQuotesError};
23use crate::system::SystemFontType;
24use crate::{
25    codegen::format::{FormatAsRustCode, GetHash},
26    corety::{AzString, U8Vec},
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)]
45pub 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
60impl PrintAsCssValue for StyleFontWeight {
61    fn print_as_css_value(&self) -> String {
62        match self {
63            Self::Lighter => "lighter".to_string(),
64            Self::W100 => "100".to_string(),
65            Self::W200 => "200".to_string(),
66            Self::W300 => "300".to_string(),
67            Self::Normal => "normal".to_string(),
68            Self::W500 => "500".to_string(),
69            Self::W600 => "600".to_string(),
70            Self::Bold => "bold".to_string(),
71            Self::W800 => "800".to_string(),
72            Self::W900 => "900".to_string(),
73            Self::Bolder => "bolder".to_string(),
74        }
75    }
76}
77
78impl FormatAsRustCode for StyleFontWeight {
79    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        format!(
84            "StyleFontWeight::{}",
85            match self {
86                Lighter => "Lighter",
87                W100 => "W100",
88                W200 => "W200",
89                W300 => "W300",
90                Normal => "Normal",
91                W500 => "W500",
92                W600 => "W600",
93                Bold => "Bold",
94                W800 => "W800",
95                W900 => "W900",
96                Bolder => "Bolder",
97            }
98        )
99    }
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)]
108pub enum StyleFontStyle {
109    #[default]
110    Normal,
111    Italic,
112    Oblique,
113}
114
115impl PrintAsCssValue for StyleFontStyle {
116    fn print_as_css_value(&self) -> String {
117        match self {
118            Self::Normal => "normal".to_string(),
119            Self::Italic => "italic".to_string(),
120            Self::Oblique => "oblique".to_string(),
121        }
122    }
123}
124
125impl FormatAsRustCode for StyleFontStyle {
126    fn format_as_rust_code(&self, _tabs: usize) -> String {
127        use StyleFontStyle::{Italic, Normal, Oblique};
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)]
144pub struct StyleFontSize {
145    pub inner: PixelValue,
146}
147
148impl 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
157impl_pixel_value!(StyleFontSize);
158impl 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
167pub 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)]
176pub 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.
196static FONT_REF_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
197
198#[must_use]
199fn next_font_ref_id() -> u64 {
200    FONT_REF_ID_COUNTER.fetch_add(1, AtomicOrdering::SeqCst)
201}
202
203impl fmt::Debug for FontRef {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "FontRef(0x{:x}", self.parsed as usize)?;
206        if let Some(c) = unsafe { self.copies.as_ref() } {
207            write!(f, ", copies: {})", c.load(AtomicOrdering::SeqCst))?;
208        } else {
209            write!(f, ")")?;
210        }
211        Ok(())
212    }
213}
214
215impl 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    pub fn new(parsed: *const c_void, destructor: FontRefDestructorCallbackType) -> Self {
222        Self {
223            parsed,
224            copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
225            id: next_font_ref_id(),
226            run_destructor: true,
227            parsed_destructor: destructor,
228        }
229    }
230
231    /// Get a raw pointer to the parsed font data
232    #[inline]
233    #[must_use]
234    pub const fn get_parsed(&self) -> *const c_void {
235        self.parsed
236    }
237}
238impl_option!(
239    FontRef,
240    OptionFontRef,
241    copy = false,
242    [Debug, Clone, PartialEq, Eq, Hash]
243);
244unsafe impl Send for FontRef {}
245unsafe 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).
248impl PartialEq for FontRef {
249    fn eq(&self, rhs: &Self) -> bool {
250        self.id == rhs.id
251    }
252}
253impl PartialOrd for FontRef {
254    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255        Some(self.id.cmp(&other.id))
256    }
257}
258impl Ord for FontRef {
259    fn cmp(&self, other: &Self) -> Ordering {
260        self.id.cmp(&other.id)
261    }
262}
263impl Eq for FontRef {}
264impl Hash for FontRef {
265    fn hash<H: Hasher>(&self, state: &mut H) {
266        self.id.hash(state);
267    }
268}
269impl Clone for FontRef {
270    fn clone(&self) -> Self {
271        if !self.copies.is_null() {
272            unsafe {
273                (*self.copies).fetch_add(1, AtomicOrdering::SeqCst);
274            }
275        }
276        Self {
277            parsed: self.parsed,
278            copies: self.copies,
279            id: self.id, // same font → same identity
280            run_destructor: self.run_destructor,
281            parsed_destructor: self.parsed_destructor,
282        }
283    }
284}
285impl Drop for FontRef {
286    fn drop(&mut self) {
287        if self.run_destructor
288            && !self.copies.is_null()
289            && unsafe { (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst) } == 1
290        {
291            unsafe {
292                (self.parsed_destructor)(self.parsed.cast_mut());
293                drop(Box::from_raw(self.copies.cast_mut()));
294            }
295        }
296    }
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`, `system:monospace`)
306/// - `File(AzString)`: A font loaded from a file URL
307/// - `Ref(FontRef)`: A reference to a pre-loaded font
308#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
309#[repr(C, u8)]
310pub enum StyleFontFamily {
311    /// Named font family (e.g., "Arial", "Times New Roman", "monospace")
312    System(AzString),
313    /// Semantic system font type (e.g., `system:ui`, `system:monospace:bold`)
314    /// Resolved at runtime based on platform and accessibility settings
315    SystemType(SystemFontType),
316    /// Font loaded from a file URL
317    File(AzString),
318    /// Reference to a pre-loaded font
319    Ref(FontRef),
320}
321
322impl_option!(
323    StyleFontFamily,
324    OptionStyleFontFamily,
325    copy = false,
326    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
327);
328
329impl StyleFontFamily {
330    pub fn as_string(&self) -> String {
331        match &self {
332            Self::System(s) => {
333                let owned = s.clone().into_library_owned_string();
334                if owned.contains(char::is_whitespace) {
335                    format!("\"{owned}\"")
336                } else {
337                    owned
338                }
339            }
340            Self::SystemType(st) => st.as_css_str().to_string(),
341            Self::File(s) => format!("url({})", s.clone().into_library_owned_string()),
342            Self::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
343        }
344    }
345
346    /// The RAW family name, for querying the font backend (fontconfig). Unlike
347    /// `as_string()` this does NOT apply CSS serialization — a multi-word name comes
348    /// back as `Times New Roman`, not `"Times New Roman"`, since the backend matches on
349    /// the bare name and the quotes would corrupt the query.
350    #[must_use]
351    pub fn as_query_string(&self) -> String {
352        match &self {
353            Self::System(s) | Self::File(s) => s.clone().into_library_owned_string(),
354            Self::SystemType(st) => st.as_css_str().to_string(),
355            Self::Ref(s) => format!("font-ref(0x{:x})", s.parsed as usize),
356        }
357    }
358}
359
360impl_vec!(
361    StyleFontFamily,
362    StyleFontFamilyVec,
363    StyleFontFamilyVecDestructor,
364    StyleFontFamilyVecDestructorType,
365    StyleFontFamilyVecSlice,
366    OptionStyleFontFamily
367);
368impl_vec_clone!(
369    StyleFontFamily,
370    StyleFontFamilyVec,
371    StyleFontFamilyVecDestructor
372);
373impl_vec_debug!(StyleFontFamily, StyleFontFamilyVec);
374impl_vec_eq!(StyleFontFamily, StyleFontFamilyVec);
375impl_vec_ord!(StyleFontFamily, StyleFontFamilyVec);
376impl_vec_hash!(StyleFontFamily, StyleFontFamilyVec);
377impl_vec_partialeq!(StyleFontFamily, StyleFontFamilyVec);
378impl_vec_partialord!(StyleFontFamily, StyleFontFamilyVec);
379
380impl PrintAsCssValue for StyleFontFamilyVec {
381    fn print_as_css_value(&self) -> String {
382        self.iter()
383            .map(StyleFontFamily::as_string)
384            .collect::<Vec<_>>()
385            .join(", ")
386    }
387}
388
389// Formatting to Rust code for StyleFontFamilyVec
390impl FormatAsRustCode for StyleFontFamilyVec {
391    fn format_as_rust_code(&self, _tabs: usize) -> String {
392        format!(
393            "StyleFontFamilyVec::from_const_slice(STYLE_FONT_FAMILY_{}_ITEMS)",
394            self.get_hash()
395        )
396    }
397}
398
399// --- PARSERS ---
400
401// -- Font Weight Parser --
402
403#[derive(Clone, PartialEq, Eq)]
404pub enum CssFontWeightParseError<'a> {
405    InvalidValue(InvalidValueErr<'a>),
406    InvalidNumber(ParseIntError),
407}
408
409// Formatting to Rust code for StyleFontFamily
410impl FormatAsRustCode for StyleFontFamily {
411    fn format_as_rust_code(&self, _tabs: usize) -> String {
412        match self {
413            Self::System(id) => {
414                format!("StyleFontFamily::System(STRING_{})", id.get_hash())
415            }
416            Self::SystemType(st) => {
417                format!("StyleFontFamily::SystemType(SystemFontType::{st:?})")
418            }
419            Self::File(path) => {
420                format!("StyleFontFamily::File(STRING_{})", path.get_hash())
421            }
422            Self::Ref(font_ref) => {
423                format!("StyleFontFamily::Ref({:0x})", font_ref.parsed as usize)
424            }
425        }
426    }
427}
428impl_debug_as_display!(CssFontWeightParseError<'a>);
429impl_display! { CssFontWeightParseError<'a>, {
430    InvalidValue(e) => format!("Invalid font-weight keyword: \"{}\"", e.0),
431    InvalidNumber(e) => format!("Invalid font-weight number: {}", e),
432}}
433impl<'a> From<InvalidValueErr<'a>> for CssFontWeightParseError<'a> {
434    fn from(e: InvalidValueErr<'a>) -> Self {
435        CssFontWeightParseError::InvalidValue(e)
436    }
437}
438impl From<ParseIntError> for CssFontWeightParseError<'_> {
439    fn from(e: ParseIntError) -> Self {
440        CssFontWeightParseError::InvalidNumber(e)
441    }
442}
443#[allow(variant_size_differences)]
444// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
445#[derive(Debug, Clone, PartialEq, Eq)]
446#[repr(C, u8)]
447pub enum CssFontWeightParseErrorOwned {
448    InvalidValue(InvalidValueErrOwned),
449    InvalidNumber(crate::props::basic::error::ParseIntError),
450}
451
452impl CssFontWeightParseError<'_> {
453    #[must_use]
454    pub fn to_contained(&self) -> CssFontWeightParseErrorOwned {
455        match self {
456            Self::InvalidValue(e) => CssFontWeightParseErrorOwned::InvalidValue(e.to_contained()),
457            Self::InvalidNumber(e) => CssFontWeightParseErrorOwned::InvalidNumber(e.clone().into()),
458        }
459    }
460}
461
462impl CssFontWeightParseErrorOwned {
463    #[must_use]
464    pub fn to_shared(&self) -> CssFontWeightParseError<'_> {
465        match self {
466            Self::InvalidValue(e) => CssFontWeightParseError::InvalidValue(e.to_shared()),
467            Self::InvalidNumber(e) => CssFontWeightParseError::InvalidNumber(e.to_std()),
468        }
469    }
470}
471
472#[cfg(feature = "parser")]
473/// # Errors
474///
475/// Returns an error if `input` is not a valid CSS `font-weight` value.
476pub fn parse_font_weight(input: &str) -> Result<StyleFontWeight, CssFontWeightParseError<'_>> {
477    let input = input.trim();
478    match input {
479        "lighter" => Ok(StyleFontWeight::Lighter),
480        "normal" | "400" => Ok(StyleFontWeight::Normal),
481        "bold" | "700" => Ok(StyleFontWeight::Bold),
482        "bolder" => Ok(StyleFontWeight::Bolder),
483        "100" => Ok(StyleFontWeight::W100),
484        "200" => Ok(StyleFontWeight::W200),
485        "300" => Ok(StyleFontWeight::W300),
486        "500" => Ok(StyleFontWeight::W500),
487        "600" => Ok(StyleFontWeight::W600),
488        "800" => Ok(StyleFontWeight::W800),
489        "900" => Ok(StyleFontWeight::W900),
490        _ => Err(InvalidValueErr(input).into()),
491    }
492}
493
494// -- Font Style Parser --
495
496#[derive(Clone, PartialEq, Eq)]
497pub enum CssFontStyleParseError<'a> {
498    InvalidValue(InvalidValueErr<'a>),
499}
500impl_debug_as_display!(CssFontStyleParseError<'a>);
501impl_display! { CssFontStyleParseError<'a>, {
502    InvalidValue(e) => format!("Invalid font-style: \"{}\"", e.0),
503}}
504impl_from! { InvalidValueErr<'a>, CssFontStyleParseError::InvalidValue }
505
506#[derive(Debug, Clone, PartialEq, Eq)]
507#[repr(C, u8)]
508pub enum CssFontStyleParseErrorOwned {
509    InvalidValue(InvalidValueErrOwned),
510}
511impl CssFontStyleParseError<'_> {
512    #[must_use]
513    pub fn to_contained(&self) -> CssFontStyleParseErrorOwned {
514        match self {
515            Self::InvalidValue(e) => CssFontStyleParseErrorOwned::InvalidValue(e.to_contained()),
516        }
517    }
518}
519impl CssFontStyleParseErrorOwned {
520    #[must_use]
521    pub fn to_shared(&self) -> CssFontStyleParseError<'_> {
522        match self {
523            Self::InvalidValue(e) => CssFontStyleParseError::InvalidValue(e.to_shared()),
524        }
525    }
526}
527
528#[cfg(feature = "parser")]
529/// # Errors
530///
531/// Returns an error if `input` is not a valid CSS `font-style` value.
532pub fn parse_font_style(input: &str) -> Result<StyleFontStyle, CssFontStyleParseError<'_>> {
533    match input.trim() {
534        "normal" => Ok(StyleFontStyle::Normal),
535        "italic" => Ok(StyleFontStyle::Italic),
536        "oblique" => Ok(StyleFontStyle::Oblique),
537        other => Err(InvalidValueErr(other).into()),
538    }
539}
540
541// -- Font Size Parser --
542
543#[derive(Clone, PartialEq, Eq)]
544pub enum CssStyleFontSizeParseError<'a> {
545    PixelValue(CssPixelValueParseError<'a>),
546}
547impl_debug_as_display!(CssStyleFontSizeParseError<'a>);
548impl_display! { CssStyleFontSizeParseError<'a>, {
549    PixelValue(e) => format!("Invalid font-size: {}", e),
550}}
551impl_from! { CssPixelValueParseError<'a>, CssStyleFontSizeParseError::PixelValue }
552
553#[derive(Debug, Clone, PartialEq, Eq)]
554#[repr(C, u8)]
555pub enum CssStyleFontSizeParseErrorOwned {
556    PixelValue(CssPixelValueParseErrorOwned),
557}
558impl CssStyleFontSizeParseError<'_> {
559    #[must_use]
560    pub fn to_contained(&self) -> CssStyleFontSizeParseErrorOwned {
561        match self {
562            Self::PixelValue(e) => CssStyleFontSizeParseErrorOwned::PixelValue(e.to_contained()),
563        }
564    }
565}
566impl CssStyleFontSizeParseErrorOwned {
567    #[must_use]
568    pub fn to_shared(&self) -> CssStyleFontSizeParseError<'_> {
569        match self {
570            Self::PixelValue(e) => CssStyleFontSizeParseError::PixelValue(e.to_shared()),
571        }
572    }
573}
574
575#[cfg(feature = "parser")]
576/// # Errors
577///
578/// Returns an error if `input` is not a valid CSS `font-size` value.
579pub fn parse_style_font_size(input: &str) -> Result<StyleFontSize, CssStyleFontSizeParseError<'_>> {
580    Ok(StyleFontSize {
581        inner: parse_pixel_value(input)?,
582    })
583}
584
585// -- Font Family Parser --
586
587#[derive(PartialEq, Eq, Clone)]
588pub enum CssStyleFontFamilyParseError<'a> {
589    InvalidStyleFontFamily(&'a str),
590    UnclosedQuotes(UnclosedQuotesError<'a>),
591}
592impl_debug_as_display!(CssStyleFontFamilyParseError<'a>);
593impl_display! { CssStyleFontFamilyParseError<'a>, {
594    InvalidStyleFontFamily(val) => format!("Invalid font-family: \"{}\"", val),
595    UnclosedQuotes(val) => format!("Unclosed quotes in font-family: \"{}\"", val.0),
596}}
597impl<'a> From<UnclosedQuotesError<'a>> for CssStyleFontFamilyParseError<'a> {
598    fn from(err: UnclosedQuotesError<'a>) -> Self {
599        CssStyleFontFamilyParseError::UnclosedQuotes(err)
600    }
601}
602
603#[derive(Debug, Clone, PartialEq, Eq)]
604#[repr(C, u8)]
605pub enum CssStyleFontFamilyParseErrorOwned {
606    InvalidStyleFontFamily(AzString),
607    UnclosedQuotes(AzString),
608}
609impl CssStyleFontFamilyParseError<'_> {
610    #[must_use]
611    pub fn to_contained(&self) -> CssStyleFontFamilyParseErrorOwned {
612        match self {
613            CssStyleFontFamilyParseError::InvalidStyleFontFamily(s) => {
614                CssStyleFontFamilyParseErrorOwned::InvalidStyleFontFamily((*s).to_string().into())
615            }
616            CssStyleFontFamilyParseError::UnclosedQuotes(e) => {
617                CssStyleFontFamilyParseErrorOwned::UnclosedQuotes(e.0.to_string().into())
618            }
619        }
620    }
621}
622impl CssStyleFontFamilyParseErrorOwned {
623    #[must_use]
624    pub fn to_shared(&self) -> CssStyleFontFamilyParseError<'_> {
625        match self {
626            Self::InvalidStyleFontFamily(s) => {
627                CssStyleFontFamilyParseError::InvalidStyleFontFamily(s)
628            }
629            Self::UnclosedQuotes(s) => {
630                CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError(s))
631            }
632        }
633    }
634}
635
636#[cfg(feature = "parser")]
637/// # Errors
638///
639/// Returns an error if `input` is not a valid CSS `font-family` value.
640pub fn parse_style_font_family(
641    input: &str,
642) -> Result<StyleFontFamilyVec, CssStyleFontFamilyParseError<'_>> {
643    let multiple_fonts = input.split(',');
644    let mut fonts = Vec::with_capacity(1);
645
646    for font in multiple_fonts {
647        let font = font.trim();
648
649        // Check for system font type syntax: system:ui, system:monospace:bold, etc.
650        if font.starts_with("system:") {
651            if let Some(system_type) = SystemFontType::from_css_str(font) {
652                fonts.push(StyleFontFamily::SystemType(system_type));
653                continue;
654            }
655            // Invalid system font type, fall through to treat as regular font name
656        }
657
658        if let Ok(stripped) = strip_quotes(font) {
659            fonts.push(StyleFontFamily::System(stripped.0.to_string().into()));
660        } else {
661            // It could be an unquoted font name like `Times New Roman`.
662            fonts.push(StyleFontFamily::System(font.to_string().into()));
663        }
664    }
665
666    Ok(fonts.into())
667}
668
669// --- Font Metrics ---
670
671use crate::corety::{OptionI16, OptionU16, OptionU32};
672
673/// PANOSE classification values for font identification (10 bytes).
674/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#panose>
675#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
676#[repr(C)]
677#[derive(Default)]
678pub struct Panose {
679    pub family_type: u8,
680    pub serif_style: u8,
681    pub weight: u8,
682    pub proportion: u8,
683    pub contrast: u8,
684    pub stroke_variation: u8,
685    pub arm_style: u8,
686    pub letterform: u8,
687    pub midline: u8,
688    pub x_height: u8,
689}
690
691impl Panose {
692    #[must_use]
693    pub const fn zero() -> Self {
694        Self {
695            family_type: 0,
696            serif_style: 0,
697            weight: 0,
698            proportion: 0,
699            contrast: 0,
700            stroke_variation: 0,
701            arm_style: 0,
702            letterform: 0,
703            midline: 0,
704            x_height: 0,
705        }
706    }
707}
708
709/// Font metrics structure containing all font-related measurements from
710/// the font file tables (head, hhea, and os/2 tables).
711#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
712#[repr(C)]
713pub struct FontMetrics {
714    // os/2 version 1 table (u32 fields - align 4, placed first)
715    pub ul_code_page_range1: OptionU32,
716    pub ul_code_page_range2: OptionU32,
717
718    // os/2 table (u32 fields)
719    pub ul_unicode_range1: u32,
720    pub ul_unicode_range2: u32,
721    pub ul_unicode_range3: u32,
722    pub ul_unicode_range4: u32,
723    pub ach_vend_id: u32,
724
725    // os/2 version 0 table (Option<i16>/Option<u16> - align 2)
726    pub s_typo_ascender: OptionI16,
727    pub s_typo_descender: OptionI16,
728    pub s_typo_line_gap: OptionI16,
729    pub us_win_ascent: OptionU16,
730    pub us_win_descent: OptionU16,
731
732    // +spec:font-metrics:d3b654 - cap-height and x-height metrics for visual text centering (leading-trim)
733    // os/2 version 2 table
734    pub sx_height: OptionI16,
735    pub s_cap_height: OptionI16,
736    pub us_default_char: OptionU16,
737    pub us_break_char: OptionU16,
738    pub us_max_context: OptionU16,
739
740    // os/2 version 3 table
741    pub us_lower_optical_point_size: OptionU16,
742    pub us_upper_optical_point_size: OptionU16,
743
744    // head table (u16/i16 - align 2)
745    pub units_per_em: u16,
746    pub font_flags: u16,
747    pub x_min: i16,
748    pub y_min: i16,
749    pub x_max: i16,
750    pub y_max: i16,
751
752    // hhea table
753    pub ascender: i16,
754    pub descender: i16,
755    pub line_gap: i16,
756    pub advance_width_max: u16,
757    pub min_left_side_bearing: i16,
758    pub min_right_side_bearing: i16,
759    pub x_max_extent: i16,
760    pub caret_slope_rise: i16,
761    pub caret_slope_run: i16,
762    pub caret_offset: i16,
763    pub num_h_metrics: u16,
764
765    // os/2 table (u16/i16 fields)
766    pub x_avg_char_width: i16,
767    pub us_weight_class: u16,
768    pub us_width_class: u16,
769    pub fs_type: u16,
770    pub y_subscript_x_size: i16,
771    pub y_subscript_y_size: i16,
772    pub y_subscript_x_offset: i16,
773    pub y_subscript_y_offset: i16,
774    pub y_superscript_x_size: i16,
775    pub y_superscript_y_size: i16,
776    pub y_superscript_x_offset: i16,
777    pub y_superscript_y_offset: i16,
778    pub y_strikeout_size: i16,
779    pub y_strikeout_position: i16,
780    pub s_family_class: i16,
781    pub fs_selection: u16,
782    pub us_first_char_index: u16,
783    pub us_last_char_index: u16,
784
785    // panose (align 1 - last)
786    pub panose: Panose,
787}
788
789impl Default for FontMetrics {
790    fn default() -> Self {
791        Self::zero()
792    }
793}
794
795impl FontMetrics {
796    /// Only for testing, zero-sized font, will always return 0 for every metric
797    /// (`units_per_em = 1000`)
798    #[must_use]
799    pub const fn zero() -> Self {
800        Self {
801            ul_code_page_range1: OptionU32::None,
802            ul_code_page_range2: OptionU32::None,
803            ul_unicode_range1: 0,
804            ul_unicode_range2: 0,
805            ul_unicode_range3: 0,
806            ul_unicode_range4: 0,
807            ach_vend_id: 0,
808            s_typo_ascender: OptionI16::None,
809            s_typo_descender: OptionI16::None,
810            s_typo_line_gap: OptionI16::None,
811            us_win_ascent: OptionU16::None,
812            us_win_descent: OptionU16::None,
813            sx_height: OptionI16::None,
814            s_cap_height: OptionI16::None,
815            us_default_char: OptionU16::None,
816            us_break_char: OptionU16::None,
817            us_max_context: OptionU16::None,
818            us_lower_optical_point_size: OptionU16::None,
819            us_upper_optical_point_size: OptionU16::None,
820            units_per_em: 1000,
821            font_flags: 0,
822            x_min: 0,
823            y_min: 0,
824            x_max: 0,
825            y_max: 0,
826            ascender: 0,
827            descender: 0,
828            line_gap: 0,
829            advance_width_max: 0,
830            min_left_side_bearing: 0,
831            min_right_side_bearing: 0,
832            x_max_extent: 0,
833            caret_slope_rise: 0,
834            caret_slope_run: 0,
835            caret_offset: 0,
836            num_h_metrics: 0,
837            x_avg_char_width: 0,
838            us_weight_class: 400,
839            us_width_class: 5,
840            fs_type: 0,
841            y_subscript_x_size: 0,
842            y_subscript_y_size: 0,
843            y_subscript_x_offset: 0,
844            y_subscript_y_offset: 0,
845            y_superscript_x_size: 0,
846            y_superscript_y_size: 0,
847            y_superscript_x_offset: 0,
848            y_superscript_y_offset: 0,
849            y_strikeout_size: 0,
850            y_strikeout_position: 0,
851            s_family_class: 0,
852            fs_selection: 0,
853            us_first_char_index: 0,
854            us_last_char_index: 0,
855            panose: Panose::zero(),
856        }
857    }
858
859    /// Returns the ascender value from the hhea table
860    #[must_use]
861    pub const fn get_ascender(&self) -> i16 {
862        self.ascender
863    }
864
865    /// Returns the descender value from the hhea table
866    #[must_use]
867    pub const fn get_descender(&self) -> i16 {
868        self.descender
869    }
870
871    /// Returns the line gap value from the hhea table
872    #[must_use]
873    pub const fn get_line_gap(&self) -> i16 {
874        self.line_gap
875    }
876
877    /// Returns the maximum advance width from the hhea table
878    #[must_use]
879    pub const fn get_advance_width_max(&self) -> u16 {
880        self.advance_width_max
881    }
882
883    /// Returns the minimum left side bearing from the hhea table
884    #[must_use]
885    pub const fn get_min_left_side_bearing(&self) -> i16 {
886        self.min_left_side_bearing
887    }
888
889    /// Returns the minimum right side bearing from the hhea table
890    #[must_use]
891    pub const fn get_min_right_side_bearing(&self) -> i16 {
892        self.min_right_side_bearing
893    }
894
895    /// Returns the `x_min` value from the head table
896    #[must_use]
897    pub const fn get_x_min(&self) -> i16 {
898        self.x_min
899    }
900
901    /// Returns the `y_min` value from the head table
902    #[must_use]
903    pub const fn get_y_min(&self) -> i16 {
904        self.y_min
905    }
906
907    /// Returns the `x_max` value from the head table
908    #[must_use]
909    pub const fn get_x_max(&self) -> i16 {
910        self.x_max
911    }
912
913    /// Returns the `y_max` value from the head table
914    #[must_use]
915    pub const fn get_y_max(&self) -> i16 {
916        self.y_max
917    }
918
919    /// Returns the maximum extent in the x direction from the hhea table
920    #[must_use]
921    pub const fn get_x_max_extent(&self) -> i16 {
922        self.x_max_extent
923    }
924
925    /// Returns the average character width from the os/2 table
926    #[must_use]
927    pub const fn get_x_avg_char_width(&self) -> i16 {
928        self.x_avg_char_width
929    }
930
931    /// Returns the subscript x size from the os/2 table
932    #[must_use]
933    pub const fn get_y_subscript_x_size(&self) -> i16 {
934        self.y_subscript_x_size
935    }
936
937    /// Returns the subscript y size from the os/2 table
938    #[must_use]
939    pub const fn get_y_subscript_y_size(&self) -> i16 {
940        self.y_subscript_y_size
941    }
942
943    /// Returns the subscript x offset from the os/2 table
944    #[must_use]
945    pub const fn get_y_subscript_x_offset(&self) -> i16 {
946        self.y_subscript_x_offset
947    }
948
949    /// Returns the subscript y offset from the os/2 table
950    #[must_use]
951    pub const fn get_y_subscript_y_offset(&self) -> i16 {
952        self.y_subscript_y_offset
953    }
954
955    /// Returns the superscript x size from the os/2 table
956    #[must_use]
957    pub const fn get_y_superscript_x_size(&self) -> i16 {
958        self.y_superscript_x_size
959    }
960
961    /// Returns the superscript y size from the os/2 table
962    #[must_use]
963    pub const fn get_y_superscript_y_size(&self) -> i16 {
964        self.y_superscript_y_size
965    }
966
967    /// Returns the superscript x offset from the os/2 table
968    #[must_use]
969    pub const fn get_y_superscript_x_offset(&self) -> i16 {
970        self.y_superscript_x_offset
971    }
972
973    /// Returns the superscript y offset from the os/2 table
974    #[must_use]
975    pub const fn get_y_superscript_y_offset(&self) -> i16 {
976        self.y_superscript_y_offset
977    }
978
979    /// Returns the strikeout size from the os/2 table
980    #[must_use]
981    pub const fn get_y_strikeout_size(&self) -> i16 {
982        self.y_strikeout_size
983    }
984
985    /// Returns the strikeout position from the os/2 table
986    #[must_use]
987    pub const fn get_y_strikeout_position(&self) -> i16 {
988        self.y_strikeout_position
989    }
990
991    /// Returns whether typographic metrics should be used (from `fs_selection` flag)
992    #[must_use]
993    pub const fn use_typo_metrics(&self) -> bool {
994        // Bit 7 of fs_selection indicates USE_TYPO_METRICS
995        (self.fs_selection & 0x0080) != 0
996    }
997}
998
999#[cfg(all(test, feature = "parser"))]
1000mod tests {
1001    use super::*;
1002
1003    #[test]
1004    fn test_parse_font_weight_keywords() {
1005        assert_eq!(
1006            parse_font_weight("normal").unwrap(),
1007            StyleFontWeight::Normal
1008        );
1009        assert_eq!(parse_font_weight("bold").unwrap(), StyleFontWeight::Bold);
1010        assert_eq!(
1011            parse_font_weight("lighter").unwrap(),
1012            StyleFontWeight::Lighter
1013        );
1014        assert_eq!(
1015            parse_font_weight("bolder").unwrap(),
1016            StyleFontWeight::Bolder
1017        );
1018    }
1019
1020    #[test]
1021    fn test_parse_font_weight_numbers() {
1022        assert_eq!(parse_font_weight("100").unwrap(), StyleFontWeight::W100);
1023        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
1024        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
1025        assert_eq!(parse_font_weight("900").unwrap(), StyleFontWeight::W900);
1026    }
1027
1028    #[test]
1029    fn test_parse_font_weight_invalid() {
1030        assert!(parse_font_weight("thin").is_err());
1031        assert!(parse_font_weight("").is_err());
1032        assert!(parse_font_weight("450").is_err());
1033        assert!(parse_font_weight("boldest").is_err());
1034    }
1035
1036    #[test]
1037    fn test_parse_font_style() {
1038        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
1039        assert_eq!(parse_font_style("italic").unwrap(), StyleFontStyle::Italic);
1040        assert_eq!(
1041            parse_font_style("oblique").unwrap(),
1042            StyleFontStyle::Oblique
1043        );
1044        assert_eq!(
1045            parse_font_style("  italic  ").unwrap(),
1046            StyleFontStyle::Italic
1047        );
1048        assert!(parse_font_style("slanted").is_err());
1049    }
1050
1051    #[test]
1052    fn test_parse_font_size() {
1053        assert_eq!(
1054            parse_style_font_size("16px").unwrap().inner,
1055            PixelValue::px(16.0)
1056        );
1057        assert_eq!(
1058            parse_style_font_size("1.2em").unwrap().inner,
1059            PixelValue::em(1.2)
1060        );
1061        assert_eq!(
1062            parse_style_font_size("12pt").unwrap().inner,
1063            PixelValue::pt(12.0)
1064        );
1065        assert_eq!(
1066            parse_style_font_size("120%").unwrap().inner,
1067            PixelValue::percent(120.0)
1068        );
1069        assert!(parse_style_font_size("medium").is_err());
1070    }
1071
1072    #[test]
1073    fn test_parse_font_family() {
1074        // Single unquoted
1075        let result = parse_style_font_family("Arial").unwrap();
1076        assert_eq!(result.len(), 1);
1077        assert_eq!(
1078            result.as_slice()[0],
1079            StyleFontFamily::System("Arial".into())
1080        );
1081
1082        // Single quoted
1083        let result = parse_style_font_family("\"Times New Roman\"").unwrap();
1084        assert_eq!(result.len(), 1);
1085        assert_eq!(
1086            result.as_slice()[0],
1087            StyleFontFamily::System("Times New Roman".into())
1088        );
1089
1090        // Multiple
1091        let result = parse_style_font_family("Georgia, serif").unwrap();
1092        assert_eq!(result.len(), 2);
1093        assert_eq!(
1094            result.as_slice()[0],
1095            StyleFontFamily::System("Georgia".into())
1096        );
1097        assert_eq!(
1098            result.as_slice()[1],
1099            StyleFontFamily::System("serif".into())
1100        );
1101
1102        // Multiple with quotes and extra whitespace
1103        let result = parse_style_font_family("  'Courier New'  , monospace  ").unwrap();
1104        assert_eq!(result.len(), 2);
1105        assert_eq!(
1106            result.as_slice()[0],
1107            StyleFontFamily::System("Courier New".into())
1108        );
1109        assert_eq!(
1110            result.as_slice()[1],
1111            StyleFontFamily::System("monospace".into())
1112        );
1113    }
1114
1115    #[test]
1116    fn test_parse_system_font_type() {
1117        use crate::system::SystemFontType;
1118
1119        // Single system font type
1120        let result = parse_style_font_family("system:ui").unwrap();
1121        assert_eq!(result.len(), 1);
1122        assert_eq!(
1123            result.as_slice()[0],
1124            StyleFontFamily::SystemType(SystemFontType::Ui)
1125        );
1126
1127        // System font type with bold variant
1128        let result = parse_style_font_family("system:monospace:bold").unwrap();
1129        assert_eq!(result.len(), 1);
1130        assert_eq!(
1131            result.as_slice()[0],
1132            StyleFontFamily::SystemType(SystemFontType::MonospaceBold)
1133        );
1134
1135        // System font type with italic variant
1136        let result = parse_style_font_family("system:monospace:italic").unwrap();
1137        assert_eq!(result.len(), 1);
1138        assert_eq!(
1139            result.as_slice()[0],
1140            StyleFontFamily::SystemType(SystemFontType::MonospaceItalic)
1141        );
1142
1143        // System font type with fallback
1144        let result = parse_style_font_family("system:ui, Arial, sans-serif").unwrap();
1145        assert_eq!(result.len(), 3);
1146        assert_eq!(
1147            result.as_slice()[0],
1148            StyleFontFamily::SystemType(SystemFontType::Ui)
1149        );
1150        assert_eq!(
1151            result.as_slice()[1],
1152            StyleFontFamily::System("Arial".into())
1153        );
1154        assert_eq!(
1155            result.as_slice()[2],
1156            StyleFontFamily::System("sans-serif".into())
1157        );
1158
1159        // All system font types
1160        assert!(parse_style_font_family("system:ui").is_ok());
1161        assert!(parse_style_font_family("system:ui:bold").is_ok());
1162        assert!(parse_style_font_family("system:monospace").is_ok());
1163        assert!(parse_style_font_family("system:monospace:bold").is_ok());
1164        assert!(parse_style_font_family("system:monospace:italic").is_ok());
1165        assert!(parse_style_font_family("system:title").is_ok());
1166        assert!(parse_style_font_family("system:title:bold").is_ok());
1167        assert!(parse_style_font_family("system:menu").is_ok());
1168        assert!(parse_style_font_family("system:small").is_ok());
1169        assert!(parse_style_font_family("system:serif").is_ok());
1170        assert!(parse_style_font_family("system:serif:bold").is_ok());
1171
1172        // Invalid system font type should be parsed as regular font name
1173        let result = parse_style_font_family("system:invalid").unwrap();
1174        assert_eq!(result.len(), 1);
1175        assert_eq!(
1176            result.as_slice()[0],
1177            StyleFontFamily::System("system:invalid".into())
1178        );
1179    }
1180
1181    #[test]
1182    fn test_system_font_type_css_roundtrip() {
1183        use crate::system::SystemFontType;
1184
1185        // Test that as_css_str() and from_css_str() are inverses
1186        let types = [
1187            SystemFontType::Ui,
1188            SystemFontType::UiBold,
1189            SystemFontType::Monospace,
1190            SystemFontType::MonospaceBold,
1191            SystemFontType::MonospaceItalic,
1192            SystemFontType::Title,
1193            SystemFontType::TitleBold,
1194            SystemFontType::Menu,
1195            SystemFontType::Small,
1196            SystemFontType::Serif,
1197            SystemFontType::SerifBold,
1198        ];
1199
1200        for ft in &types {
1201            let css = ft.as_css_str();
1202            let parsed = SystemFontType::from_css_str(css).unwrap();
1203            assert_eq!(*ft, parsed, "Roundtrip failed for {ft:?}");
1204        }
1205    }
1206}
1207
1208#[cfg(test)]
1209#[allow(clippy::too_many_lines, clippy::float_cmp)]
1210mod autotest_generated {
1211    use std::collections::hash_map::DefaultHasher;
1212
1213    use super::*;
1214    use crate::props::basic::{error::ParseIntError as CParseIntError, length::SizeMetric};
1215
1216    fn hash_of<T: Hash>(value: &T) -> u64 {
1217        let mut hasher = DefaultHasher::new();
1218        value.hash(&mut hasher);
1219        hasher.finish()
1220    }
1221
1222    /// Leaks nothing: the matching destructor below reconstructs the `Box`.
1223    fn boxed_font_data(value: u64) -> *const c_void {
1224        Box::into_raw(Box::new(value)).cast::<c_void>().cast_const()
1225    }
1226
1227    extern "C" fn noop_destructor(_ptr: *mut c_void) {}
1228
1229    // One counter per test: `cargo test` runs tests in parallel within a single
1230    // process, so a shared counter would race.
1231    static SINGLE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1232    extern "C" fn single_counting_destructor(ptr: *mut c_void) {
1233        SINGLE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1234        if !ptr.is_null() {
1235            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1236        }
1237    }
1238
1239    static CLONE_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1240    extern "C" fn clone_counting_destructor(ptr: *mut c_void) {
1241        CLONE_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1242        if !ptr.is_null() {
1243            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1244        }
1245    }
1246
1247    static MANY_DTOR_CALLS: AtomicUsize = AtomicUsize::new(0);
1248    extern "C" fn many_counting_destructor(ptr: *mut c_void) {
1249        MANY_DTOR_CALLS.fetch_add(1, AtomicOrdering::SeqCst);
1250        if !ptr.is_null() {
1251            unsafe { drop(Box::from_raw(ptr.cast::<u64>())) };
1252        }
1253    }
1254
1255    // ---------------------------------------------------------------------
1256    // next_font_ref_id (private)
1257    // ---------------------------------------------------------------------
1258
1259    #[test]
1260    fn next_font_ref_id_is_monotonic_and_never_zero() {
1261        let a = next_font_ref_id();
1262        let b = next_font_ref_id();
1263        // `id == 0` is the "un-initialised / raw-reconstructed" sentinel, so the
1264        // counter must never hand it out.
1265        assert!(a >= 1, "id 0 is reserved as the null-handle sentinel");
1266        assert!(b > a, "ids must be strictly increasing ({a} -> {b})");
1267    }
1268
1269    // ---------------------------------------------------------------------
1270    // FontRef::new / FontRef::get_parsed
1271    // ---------------------------------------------------------------------
1272
1273    #[test]
1274    fn font_ref_new_post_construction_invariants() {
1275        let ptr = boxed_font_data(0xDEAD);
1276        let font = FontRef::new(ptr, single_counting_destructor);
1277
1278        assert_eq!(
1279            font.get_parsed(),
1280            ptr,
1281            "get_parsed must return the pointer passed to new()"
1282        );
1283        assert!(font.run_destructor);
1284        assert!(font.id >= 1);
1285        assert!(!font.copies.is_null());
1286        assert_eq!(unsafe { (*font.copies).load(AtomicOrdering::SeqCst) }, 1);
1287
1288        assert_eq!(SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
1289        drop(font);
1290        assert_eq!(
1291            SINGLE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
1292            1,
1293            "the destructor must run exactly once when the last handle drops"
1294        );
1295    }
1296
1297    #[test]
1298    fn font_ref_new_accepts_null_pointer_without_panicking() {
1299        let font = FontRef::new(core::ptr::null(), noop_destructor);
1300        assert!(font.get_parsed().is_null());
1301        assert!(font.id >= 1);
1302        // Debug must not choke on a null `parsed`.
1303        let dbg = format!("{font:?}");
1304        assert!(
1305            dbg.starts_with("FontRef(0x0"),
1306            "unexpected Debug output: {dbg}"
1307        );
1308        assert!(dbg.contains("copies: 1"), "unexpected Debug output: {dbg}");
1309    }
1310
1311    #[test]
1312    fn font_ref_clone_shares_identity_and_defers_the_destructor() {
1313        let ptr = boxed_font_data(42);
1314        let original = FontRef::new(ptr, clone_counting_destructor);
1315        let copy = original.clone();
1316
1317        assert_eq!(original, copy, "shallow clones are the same font");
1318        assert_eq!(original.id, copy.id);
1319        assert_eq!(hash_of(&original), hash_of(&copy));
1320        assert_eq!(original.cmp(&copy), Ordering::Equal);
1321        assert_eq!(original.get_parsed(), copy.get_parsed());
1322        assert_eq!(
1323            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
1324            2
1325        );
1326
1327        drop(copy);
1328        assert_eq!(
1329            CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst),
1330            0,
1331            "dropping one of two handles must not free the parsed data"
1332        );
1333        drop(original);
1334        assert_eq!(CLONE_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
1335    }
1336
1337    #[test]
1338    fn font_ref_many_clones_run_the_destructor_exactly_once() {
1339        let ptr = boxed_font_data(7);
1340        let original = FontRef::new(ptr, many_counting_destructor);
1341        let clones: Vec<FontRef> = (0..1000).map(|_| original.clone()).collect();
1342
1343        assert_eq!(
1344            unsafe { (*original.copies).load(AtomicOrdering::SeqCst) },
1345            1001
1346        );
1347        assert!(clones.iter().all(|c| *c == original));
1348
1349        drop(clones);
1350        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 0);
1351        drop(original);
1352        assert_eq!(MANY_DTOR_CALLS.load(AtomicOrdering::SeqCst), 1);
1353    }
1354
1355    #[test]
1356    fn font_ref_identity_is_the_id_not_the_pointer() {
1357        // Two independently-constructed handles over the *same* pointer value must
1358        // NOT compare equal — that is the whole point of the `id` field (a freed
1359        // font's heap address can be reused by a later font).
1360        let a = FontRef::new(core::ptr::null(), noop_destructor);
1361        let b = FontRef::new(core::ptr::null(), noop_destructor);
1362
1363        assert_eq!(a.get_parsed(), b.get_parsed(), "same (null) pointer");
1364        assert_ne!(a, b, "same pointer must not forge identity");
1365        assert_ne!(a.id, b.id);
1366        assert_ne!(hash_of(&a), hash_of(&b));
1367        assert_eq!(
1368            a.cmp(&b),
1369            Ordering::Less,
1370            "ids are handed out in increasing order"
1371        );
1372        assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
1373    }
1374
1375    #[test]
1376    fn font_ref_raw_zero_handle_is_drop_safe() {
1377        // A handle reconstructed from raw parts (id == 0, no refcount) must not be
1378        // dereferenced by Debug/Drop.
1379        let make = || FontRef {
1380            parsed: core::ptr::null(),
1381            copies: core::ptr::null(),
1382            id: 0,
1383            run_destructor: false,
1384            parsed_destructor: noop_destructor,
1385        };
1386        let raw = make();
1387        let raw2 = make();
1388
1389        assert!(raw.get_parsed().is_null());
1390        assert_eq!(format!("{raw:?}"), "FontRef(0x0)");
1391        assert_eq!(raw, raw2, "both carry the id==0 sentinel");
1392
1393        let cloned = raw.clone();
1394        assert_eq!(cloned.id, 0);
1395        assert!(
1396            cloned.copies.is_null(),
1397            "cloning must not allocate a refcount for a raw handle"
1398        );
1399
1400        drop(cloned);
1401        drop(raw2);
1402        drop(raw); // must not double-free / deref null
1403    }
1404
1405    // ---------------------------------------------------------------------
1406    // StyleFontFamily::as_string
1407    // ---------------------------------------------------------------------
1408
1409    #[test]
1410    fn style_font_family_as_string_quotes_only_when_whitespace_is_present() {
1411        assert_eq!(StyleFontFamily::System("Arial".into()).as_string(), "Arial");
1412        assert_eq!(
1413            StyleFontFamily::System("Times New Roman".into()).as_string(),
1414            "\"Times New Roman\""
1415        );
1416        // An empty family name is not quoted (it has no whitespace).
1417        assert_eq!(StyleFontFamily::System("".into()).as_string(), "");
1418        // Tabs / newlines count as whitespace.
1419        assert_eq!(
1420            StyleFontFamily::System("a\tb".into()).as_string(),
1421            "\"a\tb\""
1422        );
1423        assert_eq!(
1424            StyleFontFamily::System("a\nb".into()).as_string(),
1425            "\"a\nb\""
1426        );
1427    }
1428
1429    #[test]
1430    fn style_font_family_as_string_handles_unicode() {
1431        // No ASCII whitespace -> unquoted, bytes preserved.
1432        assert_eq!(
1433            StyleFontFamily::System("日本語".into()).as_string(),
1434            "日本語"
1435        );
1436        assert_eq!(
1437            StyleFontFamily::System("\u{1F600}".into()).as_string(),
1438            "\u{1F600}"
1439        );
1440        // Combining marks are not whitespace.
1441        assert_eq!(
1442            StyleFontFamily::System("e\u{0301}".into()).as_string(),
1443            "e\u{0301}"
1444        );
1445        // U+00A0 NO-BREAK SPACE *is* `char::is_whitespace`, so it gets quoted.
1446        assert_eq!(
1447            StyleFontFamily::System("a\u{00A0}b".into()).as_string(),
1448            "\"a\u{00A0}b\""
1449        );
1450    }
1451
1452    #[test]
1453    fn style_font_family_as_string_file_and_systemtype_and_ref() {
1454        // `File` is never quoted, even when it contains whitespace.
1455        assert_eq!(
1456            StyleFontFamily::File("my font.ttf".into()).as_string(),
1457            "url(my font.ttf)"
1458        );
1459        assert_eq!(
1460            StyleFontFamily::SystemType(SystemFontType::MonospaceBold).as_string(),
1461            "system:monospace:bold"
1462        );
1463
1464        let ptr = 0xdead_beef_usize as *const c_void;
1465        let fam = StyleFontFamily::Ref(FontRef::new(ptr, noop_destructor));
1466        assert_eq!(fam.as_string(), "font-ref(0xdeadbeef)");
1467    }
1468
1469    #[test]
1470    fn style_font_family_as_string_on_huge_name_does_not_panic() {
1471        let huge = "x".repeat(1_000_000);
1472        let fam = StyleFontFamily::System(huge.as_str().into());
1473        assert_eq!(fam.as_string().len(), 1_000_000);
1474    }
1475
1476    // ---------------------------------------------------------------------
1477    // parse_font_weight
1478    // ---------------------------------------------------------------------
1479
1480    #[cfg(feature = "parser")]
1481    #[test]
1482    fn parse_font_weight_valid_minimal_and_full_roundtrip() {
1483        assert_eq!(
1484            parse_font_weight("normal").unwrap(),
1485            StyleFontWeight::Normal
1486        );
1487
1488        for weight in [
1489            StyleFontWeight::Lighter,
1490            StyleFontWeight::W100,
1491            StyleFontWeight::W200,
1492            StyleFontWeight::W300,
1493            StyleFontWeight::Normal,
1494            StyleFontWeight::W500,
1495            StyleFontWeight::W600,
1496            StyleFontWeight::Bold,
1497            StyleFontWeight::W800,
1498            StyleFontWeight::W900,
1499            StyleFontWeight::Bolder,
1500        ] {
1501            let css = weight.print_as_css_value();
1502            assert_eq!(
1503                parse_font_weight(&css).unwrap(),
1504                weight,
1505                "encode==decode failed for {weight:?} (printed as {css:?})"
1506            );
1507        }
1508    }
1509
1510    #[cfg(feature = "parser")]
1511    #[test]
1512    fn parse_font_weight_numeric_aliases_collapse_onto_keywords() {
1513        // 400/700 are accepted but re-print as keywords, so the *string* round-trip
1514        // is deliberately lossy in one direction.
1515        assert_eq!(parse_font_weight("400").unwrap(), StyleFontWeight::Normal);
1516        assert_eq!(parse_font_weight("700").unwrap(), StyleFontWeight::Bold);
1517        assert_eq!(StyleFontWeight::Normal.print_as_css_value(), "normal");
1518        assert_eq!(StyleFontWeight::Bold.print_as_css_value(), "bold");
1519    }
1520
1521    #[cfg(feature = "parser")]
1522    #[test]
1523    fn parse_font_weight_rejects_empty_and_whitespace_only() {
1524        for input in ["", " ", "   ", "\t\n", "\r\n\t "] {
1525            let err = parse_font_weight(input).unwrap_err();
1526            assert!(
1527                matches!(
1528                    err,
1529                    CssFontWeightParseError::InvalidValue(InvalidValueErr(""))
1530                ),
1531                "expected trimmed InvalidValue(\"\") for {input:?}, got {err:?}"
1532            );
1533        }
1534    }
1535
1536    #[cfg(feature = "parser")]
1537    #[test]
1538    fn parse_font_weight_rejects_garbage_and_reports_the_trimmed_input() {
1539        assert_eq!(
1540            parse_font_weight("  thin  ").unwrap_err(),
1541            CssFontWeightParseError::InvalidValue(InvalidValueErr("thin")),
1542            "the error must carry the trimmed input"
1543        );
1544        for input in [
1545            "thin",
1546            "boldest",
1547            "bold;garbage",
1548            "normal!",
1549            "\u{0}\u{1}\u{7f}",
1550            "-",
1551        ] {
1552            assert!(
1553                parse_font_weight(input).is_err(),
1554                "{input:?} must not parse"
1555            );
1556        }
1557    }
1558
1559    #[cfg(feature = "parser")]
1560    #[test]
1561    fn parse_font_weight_rejects_boundary_numbers_without_ever_yielding_invalidnumber() {
1562        // `parse_font_weight` only matches literal keyword/number strings; it never
1563        // calls `str::parse`, so the `InvalidNumber` variant is unreachable here.
1564        for input in [
1565            "0",
1566            "-0",
1567            "450",
1568            "1000",
1569            "0400",
1570            "+400",
1571            "400.0",
1572            "4e2",
1573            "9223372036854775807",
1574            "-9223372036854775808",
1575            "18446744073709551616",
1576            "NaN",
1577            "inf",
1578            "-inf",
1579            "1e309",
1580        ] {
1581            let err = parse_font_weight(input).unwrap_err();
1582            assert!(
1583                matches!(err, CssFontWeightParseError::InvalidValue(_)),
1584                "{input:?} should be an InvalidValue, got {err:?}"
1585            );
1586        }
1587    }
1588
1589    #[cfg(feature = "parser")]
1590    #[test]
1591    fn parse_font_weight_trims_unicode_whitespace() {
1592        // `str::trim` uses `char::is_whitespace`, so U+00A0 / U+2028 are stripped —
1593        // stricter CSS tokenisers would not do this.
1594        assert_eq!(
1595            parse_font_weight("\u{00A0}bold\u{00A0}").unwrap(),
1596            StyleFontWeight::Bold
1597        );
1598        assert_eq!(
1599            parse_font_weight("\u{2028}400").unwrap(),
1600            StyleFontWeight::Normal
1601        );
1602    }
1603
1604    #[cfg(feature = "parser")]
1605    #[test]
1606    fn parse_font_weight_unicode_garbage_is_rejected_and_displayable() {
1607        let err = parse_font_weight("\u{1F600}").unwrap_err();
1608        assert_eq!(
1609            err,
1610            CssFontWeightParseError::InvalidValue(InvalidValueErr("\u{1F600}"))
1611        );
1612        // Display / Debug must not panic on multibyte payloads.
1613        let msg = format!("{err}");
1614        assert!(msg.contains('\u{1F600}'), "unexpected message: {msg}");
1615        assert!(!format!("{err:?}").is_empty());
1616
1617        assert!(
1618            parse_font_weight("bold\u{0301}").is_err(),
1619            "combining mark must not be trimmed"
1620        );
1621    }
1622
1623    #[cfg(feature = "parser")]
1624    #[test]
1625    fn parse_font_weight_survives_extremely_long_and_deeply_nested_input() {
1626        let long = "bold".repeat(250_000); // 1_000_000 chars
1627        assert!(parse_font_weight(&long).is_err());
1628
1629        let nested = "(".repeat(10_000);
1630        assert!(parse_font_weight(&nested).is_err());
1631
1632        let long_digits = "9".repeat(100_000);
1633        assert!(parse_font_weight(&long_digits).is_err());
1634    }
1635
1636    // ---------------------------------------------------------------------
1637    // parse_font_style
1638    // ---------------------------------------------------------------------
1639
1640    #[cfg(feature = "parser")]
1641    #[test]
1642    fn parse_font_style_valid_minimal_and_full_roundtrip() {
1643        assert_eq!(parse_font_style("normal").unwrap(), StyleFontStyle::Normal);
1644        for style in [
1645            StyleFontStyle::Normal,
1646            StyleFontStyle::Italic,
1647            StyleFontStyle::Oblique,
1648        ] {
1649            let css = style.print_as_css_value();
1650            assert_eq!(
1651                parse_font_style(&css).unwrap(),
1652                style,
1653                "encode==decode failed for {style:?}"
1654            );
1655        }
1656    }
1657
1658    #[cfg(feature = "parser")]
1659    #[test]
1660    fn parse_font_style_rejects_empty_whitespace_and_garbage() {
1661        for input in ["", "   ", "\t\n"] {
1662            assert_eq!(
1663                parse_font_style(input).unwrap_err(),
1664                CssFontStyleParseError::InvalidValue(InvalidValueErr(""))
1665            );
1666        }
1667        for input in [
1668            "slanted",
1669            "italics",
1670            "ITALIC",
1671            "italic;garbage",
1672            "oblique 14deg",
1673            "0",
1674            "-0",
1675            "NaN",
1676            "inf",
1677            "9223372036854775807",
1678        ] {
1679            assert!(parse_font_style(input).is_err(), "{input:?} must not parse");
1680        }
1681    }
1682
1683    #[cfg(feature = "parser")]
1684    #[test]
1685    fn parse_font_style_leading_trailing_junk_and_unicode() {
1686        assert_eq!(
1687            parse_font_style("  italic  ").unwrap(),
1688            StyleFontStyle::Italic
1689        );
1690        assert_eq!(
1691            parse_font_style(" italic;").unwrap_err(),
1692            CssFontStyleParseError::InvalidValue(InvalidValueErr("italic;"))
1693        );
1694        let err = parse_font_style("\u{1F600}\u{0301}").unwrap_err();
1695        assert!(!format!("{err}").is_empty());
1696    }
1697
1698    #[cfg(feature = "parser")]
1699    #[test]
1700    fn parse_font_style_survives_extremely_long_and_deeply_nested_input() {
1701        let long = "italic".repeat(200_000); // 1_200_000 chars
1702        assert!(parse_font_style(&long).is_err());
1703        let nested = "[".repeat(10_000);
1704        assert!(parse_font_style(&nested).is_err());
1705    }
1706
1707    // ---------------------------------------------------------------------
1708    // parse_style_font_size
1709    // ---------------------------------------------------------------------
1710
1711    #[cfg(feature = "parser")]
1712    #[test]
1713    fn parse_style_font_size_valid_minimal_and_metric_roundtrip() {
1714        assert_eq!(
1715            parse_style_font_size("16px").unwrap().inner,
1716            PixelValue::px(16.0)
1717        );
1718
1719        // NOTE: `SizeMetric::Vmin` is deliberately excluded — see
1720        // `parse_style_font_size_vmin_is_shadowed_by_the_in_suffix`.
1721        for metric in [
1722            SizeMetric::Px,
1723            SizeMetric::Pt,
1724            SizeMetric::Em,
1725            SizeMetric::Rem,
1726            SizeMetric::In,
1727            SizeMetric::Cm,
1728            SizeMetric::Mm,
1729            SizeMetric::Percent,
1730            SizeMetric::Vw,
1731            SizeMetric::Vh,
1732            SizeMetric::Vmax,
1733        ] {
1734            let size = StyleFontSize {
1735                inner: PixelValue::from_metric(metric, 12.0),
1736            };
1737            let css = size.print_as_css_value();
1738            assert_eq!(
1739                parse_style_font_size(&css).unwrap(),
1740                size,
1741                "encode==decode failed for {metric:?} (printed as {css:?})"
1742            );
1743        }
1744
1745        // The default (12pt) must survive a print/parse round-trip too.
1746        let default = StyleFontSize::default();
1747        assert_eq!(default.print_as_css_value(), "12pt");
1748        assert_eq!(parse_style_font_size("12pt").unwrap(), default);
1749    }
1750
1751    #[cfg(feature = "parser")]
1752    #[test]
1753    fn parse_style_font_size_vmin_is_shadowed_by_the_in_suffix() {
1754        // FIXED (was a characterization of the bug): "in" used to be tested before
1755        // "vmin", so "12vmin" stripped to "12vm" and failed to parse. The suffix
1756        // table in css/src/props/basic/pixel.rs now orders "vmin" ahead of "in", so
1757        // font-size in vmin round-trips.
1758        let size = StyleFontSize {
1759            inner: PixelValue::from_metric(SizeMetric::Vmin, 12.0),
1760        };
1761        let css = size.print_as_css_value();
1762        assert_eq!(css, "12vmin");
1763
1764        assert_eq!(
1765            parse_style_font_size(&css).unwrap().inner,
1766            PixelValue::from_metric(SizeMetric::Vmin, 12.0)
1767        );
1768    }
1769
1770    #[cfg(feature = "parser")]
1771    #[test]
1772    fn parse_style_font_size_rejects_empty_and_whitespace_only() {
1773        for input in ["", " ", "   ", "\t\n"] {
1774            let err = parse_style_font_size(input).unwrap_err();
1775            assert_eq!(
1776                err,
1777                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::EmptyString),
1778                "unexpected error for {input:?}"
1779            );
1780        }
1781    }
1782
1783    #[cfg(feature = "parser")]
1784    #[test]
1785    fn parse_style_font_size_rejects_garbage_and_bare_units() {
1786        let err = parse_style_font_size("px").unwrap_err();
1787        assert!(
1788            matches!(
1789                err,
1790                CssStyleFontSizeParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
1791                    "px",
1792                    SizeMetric::Px
1793                ))
1794            ),
1795            "expected NoValueGiven, got {err:?}"
1796        );
1797
1798        for input in [
1799            "medium",
1800            "larger",
1801            "16PX", // unit matching is case-sensitive
1802            "16px;junk",
1803            "16 px junk",
1804            "\u{1F600}",
1805            "--",
1806            "px16",
1807        ] {
1808            assert!(
1809                parse_style_font_size(input).is_err(),
1810                "{input:?} must not parse"
1811            );
1812        }
1813    }
1814
1815    #[cfg(feature = "parser")]
1816    #[test]
1817    fn parse_style_font_size_accepts_unitless_numbers_as_px() {
1818        // Deviation from CSS (which only allows a unitless `0`): any bare number is
1819        // accepted and silently treated as `px`.
1820        assert_eq!(
1821            parse_style_font_size("0").unwrap().inner,
1822            PixelValue::px(0.0)
1823        );
1824        assert_eq!(
1825            parse_style_font_size("16").unwrap().inner,
1826            PixelValue::px(16.0)
1827        );
1828        assert_eq!(
1829            parse_style_font_size("-16").unwrap().inner,
1830            PixelValue::px(-16.0)
1831        );
1832    }
1833
1834    #[cfg(feature = "parser")]
1835    #[test]
1836    fn parse_style_font_size_boundary_numbers_saturate_instead_of_panicking() {
1837        // Signed zero collapses to +0.
1838        assert_eq!(
1839            parse_style_font_size("-0px").unwrap().inner.number.get(),
1840            0.0
1841        );
1842        assert_eq!(
1843            parse_style_font_size("0px").unwrap().inner.number.get(),
1844            0.0
1845        );
1846
1847        // f32 overflow -> +/-inf -> the isize encoding saturates (no UB, no panic).
1848        let big = parse_style_font_size("1e40px").unwrap().inner.number.get();
1849        assert!(
1850            big.is_finite() && big > 0.0,
1851            "expected a saturated finite value, got {big}"
1852        );
1853        let small = parse_style_font_size("-1e40px").unwrap().inner.number.get();
1854        assert!(
1855            small.is_finite() && small < 0.0,
1856            "expected a saturated finite value, got {small}"
1857        );
1858
1859        // Literal infinities are accepted by `f32::from_str` and saturate as well.
1860        let inf = parse_style_font_size("inf").unwrap().inner.number.get();
1861        assert!(inf.is_finite() && inf > 0.0);
1862        let neg_inf = parse_style_font_size("-infinitypx")
1863            .unwrap()
1864            .inner
1865            .number
1866            .get();
1867        assert!(neg_inf.is_finite() && neg_inf < 0.0);
1868
1869        // i64::MAX / u64::MAX as bare numbers: no overflow panic.
1870        for input in ["9223372036854775807", "18446744073709551615px"] {
1871            let v = parse_style_font_size(input).unwrap().inner.number.get();
1872            assert!(v.is_finite(), "{input:?} produced {v}");
1873        }
1874
1875        // Sub-milli precision is truncated by the fixed-point encoding, not rounded.
1876        assert_eq!(
1877            parse_style_font_size("16.0004px").unwrap().inner,
1878            PixelValue::px(16.0)
1879        );
1880        assert_eq!(
1881            parse_style_font_size("1e-40px").unwrap().inner.number.get(),
1882            0.0
1883        );
1884    }
1885
1886    #[cfg(feature = "parser")]
1887    #[test]
1888    fn parse_style_font_size_nan_is_silently_coerced_to_zero() {
1889        // `f32::from_str` accepts "NaN", and the fixed-point cast maps NaN -> 0.
1890        // A stricter CSS parser would reject this outright; asserted as-is.
1891        let parsed = parse_style_font_size("NaN").unwrap();
1892        assert_eq!(parsed.inner.metric, SizeMetric::Px);
1893        assert_eq!(parsed.inner.number.get(), 0.0);
1894        assert_eq!(
1895            parse_style_font_size("nanpx").unwrap().inner.number.get(),
1896            0.0
1897        );
1898    }
1899
1900    #[cfg(feature = "parser")]
1901    #[test]
1902    fn parse_style_font_size_leading_trailing_whitespace_is_trimmed() {
1903        assert_eq!(
1904            parse_style_font_size("  16px  ").unwrap().inner,
1905            PixelValue::px(16.0)
1906        );
1907        // Whitespace *between* number and unit is tolerated as well.
1908        assert_eq!(
1909            parse_style_font_size("16 px").unwrap().inner,
1910            PixelValue::px(16.0)
1911        );
1912    }
1913
1914    #[cfg(feature = "parser")]
1915    #[test]
1916    fn parse_style_font_size_survives_extremely_long_and_deeply_nested_input() {
1917        let long_digits = "1".repeat(50_000);
1918        let parsed = parse_style_font_size(&long_digits).unwrap();
1919        assert!(parsed.inner.number.get().is_finite());
1920
1921        let long_garbage = "z".repeat(1_000_000);
1922        assert!(parse_style_font_size(&long_garbage).is_err());
1923
1924        let nested = "(".repeat(10_000);
1925        assert!(parse_style_font_size(&nested).is_err());
1926    }
1927
1928    // ---------------------------------------------------------------------
1929    // parse_style_font_family
1930    // ---------------------------------------------------------------------
1931
1932    #[cfg(feature = "parser")]
1933    #[test]
1934    fn parse_style_font_family_valid_minimal() {
1935        let parsed = parse_style_font_family("Arial").unwrap();
1936        assert_eq!(parsed.len(), 1);
1937        assert_eq!(
1938            parsed.as_slice()[0],
1939            StyleFontFamily::System("Arial".into())
1940        );
1941    }
1942
1943    #[cfg(feature = "parser")]
1944    #[test]
1945    fn parse_style_font_family_never_returns_err() {
1946        // Every failure path inside the parser falls back to "treat it as an
1947        // unquoted family name", so the `Err` half of the signature (and with it
1948        // `CssStyleFontFamilyParseError`) is unreachable. Documented, not weakened.
1949        let nested = "(".repeat(10_000);
1950        let long = "x".repeat(1_000_000);
1951        let inputs: Vec<&str> = vec![
1952            "",
1953            "   ",
1954            "\t\n",
1955            ",",
1956            ",,,",
1957            "'unclosed",
1958            "\"unclosed",
1959            "\"Arial'",
1960            "'Arial\"",
1961            "\u{1F600}",
1962            "system:",
1963            "system:bogus",
1964            "url(x.ttf)",
1965            "font-ref(0xdeadbeef)",
1966            "\u{0}\u{7f}",
1967            &nested,
1968            &long,
1969        ];
1970        for input in inputs {
1971            assert!(
1972                parse_style_font_family(input).is_ok(),
1973                "parse_style_font_family unexpectedly failed for {:?}",
1974                &input[..input.len().min(32)]
1975            );
1976        }
1977    }
1978
1979    #[cfg(feature = "parser")]
1980    #[test]
1981    fn parse_style_font_family_empty_input_yields_one_empty_family() {
1982        // Deviation from CSS (an empty font-family list is invalid there): the
1983        // parser produces a single, empty `System` name instead of erroring.
1984        let parsed = parse_style_font_family("").unwrap();
1985        assert_eq!(parsed.len(), 1);
1986        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
1987
1988        let parsed = parse_style_font_family("   ").unwrap();
1989        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
1990
1991        let parsed = parse_style_font_family(",,,").unwrap();
1992        assert_eq!(parsed.len(), 4, "N commas produce N+1 (empty) families");
1993        assert!(parsed
1994            .iter()
1995            .all(|f| *f == StyleFontFamily::System("".into())));
1996    }
1997
1998    #[cfg(feature = "parser")]
1999    #[test]
2000    fn parse_style_font_family_unclosed_quotes_keep_the_quote_character() {
2001        // `strip_quotes` errors, and the parser then keeps the *raw* token — so the
2002        // quote survives into the family name rather than surfacing UnclosedQuotes.
2003        let parsed = parse_style_font_family("'unclosed").unwrap();
2004        assert_eq!(
2005            parsed.as_slice()[0],
2006            StyleFontFamily::System("'unclosed".into())
2007        );
2008
2009        let parsed = parse_style_font_family("\"Arial'").unwrap();
2010        assert_eq!(
2011            parsed.as_slice()[0],
2012            StyleFontFamily::System("\"Arial'".into())
2013        );
2014
2015        // An empty quoted string strips down to an empty family name.
2016        let parsed = parse_style_font_family("\"\"").unwrap();
2017        assert_eq!(parsed.as_slice()[0], StyleFontFamily::System("".into()));
2018    }
2019
2020    #[cfg(feature = "parser")]
2021    #[test]
2022    fn parse_style_font_family_system_prefix_is_case_sensitive_and_falls_back() {
2023        // Unknown `system:` types fall through to a literal family name.
2024        let parsed = parse_style_font_family("system:bogus").unwrap();
2025        assert_eq!(
2026            parsed.as_slice()[0],
2027            StyleFontFamily::System("system:bogus".into())
2028        );
2029        let parsed = parse_style_font_family("system:").unwrap();
2030        assert_eq!(
2031            parsed.as_slice()[0],
2032            StyleFontFamily::System("system:".into())
2033        );
2034        // Uppercase prefix is not recognised as a system font.
2035        let parsed = parse_style_font_family("SYSTEM:UI").unwrap();
2036        assert_eq!(
2037            parsed.as_slice()[0],
2038            StyleFontFamily::System("SYSTEM:UI".into())
2039        );
2040        let parsed = parse_style_font_family("system:UI").unwrap();
2041        assert_eq!(
2042            parsed.as_slice()[0],
2043            StyleFontFamily::System("system:UI".into())
2044        );
2045    }
2046
2047    #[cfg(feature = "parser")]
2048    #[test]
2049    fn parse_style_font_family_never_produces_file_or_ref_variants() {
2050        for input in [
2051            "url(x.ttf)",
2052            "font-ref(0x1)",
2053            "Arial",
2054            "system:ui",
2055            "'a'",
2056            "\u{1F600}",
2057        ] {
2058            let parsed = parse_style_font_family(input).unwrap();
2059            assert!(
2060                parsed.iter().all(|f| matches!(
2061                    f,
2062                    StyleFontFamily::System(_) | StyleFontFamily::SystemType(_)
2063                )),
2064                "the parser must only ever yield System/SystemType, got {parsed:?}"
2065            );
2066        }
2067    }
2068
2069    #[cfg(feature = "parser")]
2070    #[test]
2071    fn parse_style_font_family_handles_unicode_names() {
2072        let parsed = parse_style_font_family("日本語, \u{1F600}, e\u{0301}").unwrap();
2073        assert_eq!(parsed.len(), 3);
2074        assert_eq!(
2075            parsed.as_slice()[0],
2076            StyleFontFamily::System("日本語".into())
2077        );
2078        assert_eq!(
2079            parsed.as_slice()[1],
2080            StyleFontFamily::System("\u{1F600}".into())
2081        );
2082        assert_eq!(
2083            parsed.as_slice()[2],
2084            StyleFontFamily::System("e\u{0301}".into())
2085        );
2086    }
2087
2088    #[cfg(feature = "parser")]
2089    #[test]
2090    fn parse_style_font_family_survives_extremely_long_and_deeply_nested_input() {
2091        let huge_name = "x".repeat(1_000_000);
2092        let parsed = parse_style_font_family(&huge_name).unwrap();
2093        assert_eq!(parsed.len(), 1);
2094        assert_eq!(
2095            parsed.as_slice()[0],
2096            StyleFontFamily::System(huge_name.as_str().into())
2097        );
2098
2099        let many = "Arial,".repeat(10_000);
2100        let parsed = parse_style_font_family(&many).unwrap();
2101        assert_eq!(parsed.len(), 10_001, "trailing comma adds one empty family");
2102
2103        let nested = "(".repeat(10_000);
2104        let parsed = parse_style_font_family(&nested).unwrap();
2105        assert_eq!(parsed.len(), 1);
2106    }
2107
2108    // ---------------------------------------------------------------------
2109    // as_string <-> parse_style_font_family round-trips
2110    // ---------------------------------------------------------------------
2111
2112    #[cfg(feature = "parser")]
2113    #[test]
2114    fn style_font_family_as_string_roundtrips_through_the_parser() {
2115        for name in [
2116            "Arial",
2117            "Times New Roman",
2118            "",
2119            "日本語",
2120            "a\u{00A0}b",
2121            "Fo\"o",
2122            "serif",
2123        ] {
2124            let family = StyleFontFamily::System(name.into());
2125            let css = family.as_string();
2126            let parsed = parse_style_font_family(&css).unwrap();
2127            assert_eq!(parsed.len(), 1, "{name:?} printed as {css:?}");
2128            assert_eq!(
2129                parsed.as_slice()[0],
2130                family,
2131                "encode==decode failed for {name:?}"
2132            );
2133        }
2134
2135        for ft in [
2136            SystemFontType::Ui,
2137            SystemFontType::UiBold,
2138            SystemFontType::Monospace,
2139            SystemFontType::MonospaceBold,
2140            SystemFontType::MonospaceItalic,
2141            SystemFontType::Title,
2142            SystemFontType::TitleBold,
2143            SystemFontType::Menu,
2144            SystemFontType::Small,
2145            SystemFontType::Serif,
2146            SystemFontType::SerifBold,
2147        ] {
2148            let family = StyleFontFamily::SystemType(ft);
2149            let parsed = parse_style_font_family(&family.as_string()).unwrap();
2150            assert_eq!(
2151                parsed.as_slice()[0],
2152                family,
2153                "encode==decode failed for {ft:?}"
2154            );
2155        }
2156    }
2157
2158    #[cfg(feature = "parser")]
2159    #[test]
2160    fn style_font_family_as_string_does_not_escape_commas() {
2161        // LOSSY: `as_string()` quotes on whitespace only, so a comma inside a family
2162        // name re-parses as two families. Asserted as-is; reported as a defect.
2163        let family = StyleFontFamily::System("Foo,Bar".into());
2164        assert_eq!(family.as_string(), "Foo,Bar");
2165        let parsed = parse_style_font_family(&family.as_string()).unwrap();
2166        assert_eq!(parsed.len(), 2);
2167    }
2168
2169    #[cfg(feature = "parser")]
2170    #[test]
2171    fn style_font_family_file_and_ref_do_not_roundtrip() {
2172        // `url(...)` / `font-ref(...)` are printable but not parseable: they come
2173        // back as plain `System` names.
2174        let file = StyleFontFamily::File("f.ttf".into());
2175        let parsed = parse_style_font_family(&file.as_string()).unwrap();
2176        assert_eq!(
2177            parsed.as_slice()[0],
2178            StyleFontFamily::System("url(f.ttf)".into())
2179        );
2180
2181        let font = StyleFontFamily::Ref(FontRef::new(core::ptr::null(), noop_destructor));
2182        let parsed = parse_style_font_family(&font.as_string()).unwrap();
2183        assert_eq!(
2184            parsed.as_slice()[0],
2185            StyleFontFamily::System("font-ref(0x0)".into())
2186        );
2187    }
2188
2189    #[cfg(feature = "parser")]
2190    #[test]
2191    fn style_font_family_vec_print_as_css_value_roundtrips() {
2192        let css = "Arial, \"Times New Roman\", system:ui";
2193        let parsed = parse_style_font_family(css).unwrap();
2194        assert_eq!(parsed.print_as_css_value(), css);
2195        let reparsed = parse_style_font_family(&parsed.print_as_css_value()).unwrap();
2196        assert_eq!(reparsed, parsed, "encode==decode failed for a font stack");
2197    }
2198
2199    // ---------------------------------------------------------------------
2200    // Error to_contained / to_shared
2201    // ---------------------------------------------------------------------
2202
2203    #[test]
2204    fn css_font_weight_parse_error_invalid_value_roundtrips() {
2205        for value in ["", "thin", "\u{1F600}", "a\u{0}b"] {
2206            let shared = CssFontWeightParseError::InvalidValue(InvalidValueErr(value));
2207            let owned = shared.to_contained();
2208            assert_eq!(
2209                owned,
2210                CssFontWeightParseErrorOwned::InvalidValue(InvalidValueErrOwned {
2211                    value: value.into()
2212                })
2213            );
2214            assert_eq!(
2215                owned.to_shared(),
2216                shared,
2217                "to_contained/to_shared must round-trip"
2218            );
2219            assert!(!format!("{shared}").is_empty());
2220        }
2221    }
2222
2223    #[test]
2224    fn css_font_weight_parse_error_invalid_number_roundtrips() {
2225        let cases = [
2226            "".parse::<i32>().unwrap_err(),
2227            "x".parse::<i32>().unwrap_err(),
2228            "99999999999999999999".parse::<i32>().unwrap_err(),
2229            "-99999999999999999999".parse::<i32>().unwrap_err(),
2230        ];
2231        for err in cases {
2232            let shared = CssFontWeightParseError::InvalidNumber(err);
2233            let owned = shared.to_contained();
2234            assert_eq!(
2235                owned.to_shared(),
2236                shared,
2237                "kind must survive the FFI round-trip"
2238            );
2239            assert!(!format!("{shared}").is_empty());
2240        }
2241    }
2242
2243    #[test]
2244    fn css_font_weight_parse_error_zero_kind_roundtrip_is_lossy() {
2245        // `IntErrorKind::Zero` cannot be reconstructed on stable Rust — the source
2246        // documents this; assert the documented degradation to InvalidDigit.
2247        let zero_err = "0".parse::<core::num::NonZeroU32>().unwrap_err();
2248        let shared = CssFontWeightParseError::InvalidNumber(zero_err);
2249        let owned = shared.to_contained();
2250        assert_eq!(
2251            owned,
2252            CssFontWeightParseErrorOwned::InvalidNumber(CParseIntError::Zero),
2253            "the Zero kind must survive into the owned form"
2254        );
2255        assert_ne!(
2256            owned.to_shared(),
2257            shared,
2258            "to_std() cannot rebuild a Zero-kind ParseIntError (documented)"
2259        );
2260    }
2261
2262    #[test]
2263    fn css_font_style_parse_error_roundtrips() {
2264        for value in ["", "slanted", "\u{1F600}"] {
2265            let shared = CssFontStyleParseError::InvalidValue(InvalidValueErr(value));
2266            let owned = shared.to_contained();
2267            assert_eq!(
2268                owned,
2269                CssFontStyleParseErrorOwned::InvalidValue(InvalidValueErrOwned {
2270                    value: value.into()
2271                })
2272            );
2273            assert_eq!(owned.to_shared(), shared);
2274            assert!(!format!("{shared}").is_empty());
2275        }
2276    }
2277
2278    #[test]
2279    fn css_style_font_size_parse_error_roundtrips_every_variant() {
2280        let cases = [
2281            CssPixelValueParseError::EmptyString,
2282            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
2283            CssPixelValueParseError::NoValueGiven("%", SizeMetric::Percent),
2284            CssPixelValueParseError::ValueParseErr("abc".parse::<f32>().unwrap_err(), "abc"),
2285            CssPixelValueParseError::ValueParseErr("".parse::<f32>().unwrap_err(), ""),
2286            CssPixelValueParseError::InvalidPixelValue("medium"),
2287            CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
2288        ];
2289        for inner in cases {
2290            let shared = CssStyleFontSizeParseError::PixelValue(inner);
2291            let owned = shared.to_contained();
2292            assert_eq!(
2293                owned.to_shared(),
2294                shared,
2295                "to_contained/to_shared must round-trip"
2296            );
2297            assert!(!format!("{shared}").is_empty());
2298        }
2299    }
2300
2301    #[cfg(feature = "parser")]
2302    #[test]
2303    fn css_style_font_family_parse_error_roundtrips_every_variant() {
2304        let cases = [
2305            CssStyleFontFamilyParseError::InvalidStyleFontFamily(""),
2306            CssStyleFontFamilyParseError::InvalidStyleFontFamily("bogus"),
2307            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\"Arial")),
2308            CssStyleFontFamilyParseError::UnclosedQuotes(UnclosedQuotesError("\u{1F600}")),
2309        ];
2310        for shared in cases {
2311            let owned = shared.to_contained();
2312            assert_eq!(
2313                owned.to_shared(),
2314                shared,
2315                "to_contained/to_shared must round-trip"
2316            );
2317            assert!(!format!("{shared}").is_empty());
2318        }
2319    }
2320
2321    // ---------------------------------------------------------------------
2322    // FormatAsRustCode / defaults / ordering
2323    // ---------------------------------------------------------------------
2324
2325    #[test]
2326    fn font_enum_defaults_and_ordering() {
2327        assert_eq!(StyleFontWeight::default(), StyleFontWeight::Normal);
2328        assert_eq!(StyleFontStyle::default(), StyleFontStyle::Normal);
2329        assert_eq!(StyleFontSize::default().inner, PixelValue::const_pt(12));
2330
2331        // Derived Ord follows declaration order (numeric weights are ordered).
2332        assert!(StyleFontWeight::W100 < StyleFontWeight::W900);
2333        assert!(StyleFontWeight::Normal < StyleFontWeight::Bold);
2334        assert!(StyleFontWeight::Lighter < StyleFontWeight::W100);
2335        assert!(StyleFontWeight::Bolder > StyleFontWeight::W900);
2336    }
2337
2338    #[test]
2339    fn format_as_rust_code_matches_the_debug_variant_names() {
2340        for weight in [
2341            StyleFontWeight::Lighter,
2342            StyleFontWeight::W100,
2343            StyleFontWeight::W200,
2344            StyleFontWeight::W300,
2345            StyleFontWeight::Normal,
2346            StyleFontWeight::W500,
2347            StyleFontWeight::W600,
2348            StyleFontWeight::Bold,
2349            StyleFontWeight::W800,
2350            StyleFontWeight::W900,
2351            StyleFontWeight::Bolder,
2352        ] {
2353            assert_eq!(
2354                weight.format_as_rust_code(0),
2355                format!("StyleFontWeight::{weight:?}")
2356            );
2357        }
2358        for style in [
2359            StyleFontStyle::Normal,
2360            StyleFontStyle::Italic,
2361            StyleFontStyle::Oblique,
2362        ] {
2363            assert_eq!(
2364                style.format_as_rust_code(0),
2365                format!("StyleFontStyle::{style:?}")
2366            );
2367        }
2368        assert_eq!(
2369            StyleFontFamily::SystemType(SystemFontType::Ui).format_as_rust_code(0),
2370            "StyleFontFamily::SystemType(SystemFontType::Ui)"
2371        );
2372        assert!(StyleFontFamily::System("Arial".into())
2373            .format_as_rust_code(0)
2374            .starts_with("StyleFontFamily::System(STRING_"));
2375        assert!(StyleFontFamily::File("a.ttf".into())
2376            .format_as_rust_code(0)
2377            .starts_with("StyleFontFamily::File(STRING_"));
2378    }
2379
2380    // ---------------------------------------------------------------------
2381    // Panose::zero / FontMetrics::zero + getters
2382    // ---------------------------------------------------------------------
2383
2384    #[test]
2385    fn panose_zero_is_the_neutral_element() {
2386        const P: Panose = Panose::zero();
2387        assert_eq!(P, Panose::default());
2388        assert_eq!(hash_of(&P), hash_of(&Panose::default()));
2389        assert_eq!(P.family_type, 0);
2390        assert_eq!(P.serif_style, 0);
2391        assert_eq!(P.weight, 0);
2392        assert_eq!(P.proportion, 0);
2393        assert_eq!(P.contrast, 0);
2394        assert_eq!(P.stroke_variation, 0);
2395        assert_eq!(P.arm_style, 0);
2396        assert_eq!(P.letterform, 0);
2397        assert_eq!(P.midline, 0);
2398        assert_eq!(P.x_height, 0);
2399
2400        let mut max = Panose::zero();
2401        max.family_type = u8::MAX;
2402        assert!(max > P, "derived Ord must order by the first field");
2403    }
2404
2405    #[test]
2406    fn font_metrics_zero_invariants() {
2407        const M: FontMetrics = FontMetrics::zero();
2408        assert_eq!(M, FontMetrics::default());
2409
2410        // Documented: a zero font still declares a sane em square / weight class.
2411        assert_eq!(M.units_per_em, 1000);
2412        assert_eq!(M.us_weight_class, 400);
2413        assert_eq!(M.us_width_class, 5);
2414        assert_eq!(M.panose, Panose::zero());
2415
2416        assert_eq!(M.get_ascender(), 0);
2417        assert_eq!(M.get_descender(), 0);
2418        assert_eq!(M.get_line_gap(), 0);
2419        assert_eq!(M.get_advance_width_max(), 0);
2420        assert_eq!(M.get_min_left_side_bearing(), 0);
2421        assert_eq!(M.get_min_right_side_bearing(), 0);
2422        assert_eq!(M.get_x_min(), 0);
2423        assert_eq!(M.get_y_min(), 0);
2424        assert_eq!(M.get_x_max(), 0);
2425        assert_eq!(M.get_y_max(), 0);
2426        assert_eq!(M.get_x_max_extent(), 0);
2427        assert_eq!(M.get_x_avg_char_width(), 0);
2428        assert_eq!(M.get_y_subscript_x_size(), 0);
2429        assert_eq!(M.get_y_subscript_y_size(), 0);
2430        assert_eq!(M.get_y_subscript_x_offset(), 0);
2431        assert_eq!(M.get_y_subscript_y_offset(), 0);
2432        assert_eq!(M.get_y_superscript_x_size(), 0);
2433        assert_eq!(M.get_y_superscript_y_size(), 0);
2434        assert_eq!(M.get_y_superscript_x_offset(), 0);
2435        assert_eq!(M.get_y_superscript_y_offset(), 0);
2436        assert_eq!(M.get_y_strikeout_size(), 0);
2437        assert_eq!(M.get_y_strikeout_position(), 0);
2438        assert!(!M.use_typo_metrics());
2439
2440        assert!(matches!(M.ul_code_page_range1, OptionU32::None));
2441        assert!(matches!(M.ul_code_page_range2, OptionU32::None));
2442        assert!(matches!(M.s_typo_ascender, OptionI16::None));
2443        assert!(matches!(M.s_typo_descender, OptionI16::None));
2444        assert!(matches!(M.s_typo_line_gap, OptionI16::None));
2445        assert!(matches!(M.us_win_ascent, OptionU16::None));
2446        assert!(matches!(M.us_win_descent, OptionU16::None));
2447        assert!(matches!(M.sx_height, OptionI16::None));
2448        assert!(matches!(M.s_cap_height, OptionI16::None));
2449    }
2450
2451    #[test]
2452    fn font_metrics_getters_return_extreme_values_unchanged() {
2453        let mut m = FontMetrics::zero();
2454        m.ascender = i16::MAX;
2455        m.descender = i16::MIN;
2456        m.line_gap = i16::MIN;
2457        m.advance_width_max = u16::MAX;
2458        m.min_left_side_bearing = i16::MIN;
2459        m.min_right_side_bearing = i16::MAX;
2460        m.x_min = i16::MIN;
2461        m.y_min = i16::MIN;
2462        m.x_max = i16::MAX;
2463        m.y_max = i16::MAX;
2464        m.x_max_extent = i16::MAX;
2465        m.x_avg_char_width = i16::MIN;
2466        m.y_subscript_x_size = i16::MAX;
2467        m.y_subscript_y_size = i16::MIN;
2468        m.y_subscript_x_offset = i16::MAX;
2469        m.y_subscript_y_offset = i16::MIN;
2470        m.y_superscript_x_size = i16::MAX;
2471        m.y_superscript_y_size = i16::MIN;
2472        m.y_superscript_x_offset = i16::MAX;
2473        m.y_superscript_y_offset = i16::MIN;
2474        m.y_strikeout_size = i16::MAX;
2475        m.y_strikeout_position = i16::MIN;
2476
2477        // Getters are plain field reads: no clamping, no sign flips, no panics.
2478        assert_eq!(m.get_ascender(), i16::MAX);
2479        assert_eq!(m.get_descender(), i16::MIN);
2480        assert_eq!(m.get_line_gap(), i16::MIN);
2481        assert_eq!(m.get_advance_width_max(), u16::MAX);
2482        assert_eq!(m.get_min_left_side_bearing(), i16::MIN);
2483        assert_eq!(m.get_min_right_side_bearing(), i16::MAX);
2484        assert_eq!(m.get_x_min(), i16::MIN);
2485        assert_eq!(m.get_y_min(), i16::MIN);
2486        assert_eq!(m.get_x_max(), i16::MAX);
2487        assert_eq!(m.get_y_max(), i16::MAX);
2488        assert_eq!(m.get_x_max_extent(), i16::MAX);
2489        assert_eq!(m.get_x_avg_char_width(), i16::MIN);
2490        assert_eq!(m.get_y_subscript_x_size(), i16::MAX);
2491        assert_eq!(m.get_y_subscript_y_size(), i16::MIN);
2492        assert_eq!(m.get_y_subscript_x_offset(), i16::MAX);
2493        assert_eq!(m.get_y_subscript_y_offset(), i16::MIN);
2494        assert_eq!(m.get_y_superscript_x_size(), i16::MAX);
2495        assert_eq!(m.get_y_superscript_y_size(), i16::MIN);
2496        assert_eq!(m.get_y_superscript_x_offset(), i16::MAX);
2497        assert_eq!(m.get_y_superscript_y_offset(), i16::MIN);
2498        assert_eq!(m.get_y_strikeout_size(), i16::MAX);
2499        assert_eq!(m.get_y_strikeout_position(), i16::MIN);
2500
2501        // An "inverted" font (ascender < descender) is accepted verbatim — the
2502        // getters do no validation.
2503        assert!(m.get_ascender() > m.get_descender());
2504    }
2505
2506    #[test]
2507    fn font_metrics_use_typo_metrics_reads_exactly_bit_7() {
2508        let mut m = FontMetrics::zero();
2509        for bit in 0..16u16 {
2510            m.fs_selection = 1 << bit;
2511            assert_eq!(
2512                m.use_typo_metrics(),
2513                bit == 7,
2514                "fs_selection bit {bit} must not affect USE_TYPO_METRICS"
2515            );
2516        }
2517        m.fs_selection = u16::MAX;
2518        assert!(m.use_typo_metrics());
2519        m.fs_selection = u16::MAX ^ 0x0080;
2520        assert!(!m.use_typo_metrics(), "clearing bit 7 must clear the flag");
2521        m.fs_selection = 0;
2522        assert!(!m.use_typo_metrics());
2523    }
2524}