Skip to main content

boko/ir/
style.rs

1//! Style pool with SoA (Struct of Arrays) layout for efficient storage.
2
3use std::collections::HashMap;
4use std::fmt::Write;
5use std::hash::{Hash, Hasher};
6
7/// Trait for converting IR style values back to CSS strings.
8pub trait ToCss {
9    /// Write this value as CSS to the buffer.
10    fn to_css(&self, buf: &mut String);
11
12    /// Convert to a CSS string (convenience method).
13    fn to_css_string(&self) -> String {
14        let mut buf = String::new();
15        self.to_css(&mut buf);
16        buf
17    }
18}
19
20/// Unique identifier for a style in the StylePool.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22pub struct StyleId(pub u32);
23
24impl StyleId {
25    /// The default style (always 0).
26    pub const DEFAULT: StyleId = StyleId(0);
27}
28
29/// Font weight (100-900, with named constants).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub struct FontWeight(pub u16);
32
33impl FontWeight {
34    pub const NORMAL: FontWeight = FontWeight(400);
35    pub const BOLD: FontWeight = FontWeight(700);
36}
37
38impl ToCss for FontWeight {
39    fn to_css(&self, buf: &mut String) {
40        match self.0 {
41            400 => buf.push_str("normal"),
42            700 => buf.push_str("bold"),
43            w => write!(buf, "{}", w).unwrap(),
44        }
45    }
46}
47
48/// Font style (normal, italic, oblique).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
50pub enum FontStyle {
51    #[default]
52    Normal,
53    Italic,
54    Oblique,
55}
56
57impl ToCss for FontStyle {
58    fn to_css(&self, buf: &mut String) {
59        buf.push_str(match self {
60            FontStyle::Normal => "normal",
61            FontStyle::Italic => "italic",
62            FontStyle::Oblique => "oblique",
63        });
64    }
65}
66
67/// Font variant (normal, small-caps).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
69pub enum FontVariant {
70    #[default]
71    Normal,
72    SmallCaps,
73}
74
75/// Text transform values.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77pub enum TextTransform {
78    #[default]
79    None,
80    Uppercase,
81    Lowercase,
82    Capitalize,
83}
84
85impl ToCss for TextTransform {
86    fn to_css(&self, buf: &mut String) {
87        buf.push_str(match self {
88            TextTransform::None => "none",
89            TextTransform::Uppercase => "uppercase",
90            TextTransform::Lowercase => "lowercase",
91            TextTransform::Capitalize => "capitalize",
92        });
93    }
94}
95
96/// Hyphenation mode.
97/// Default is `Manual` so that explicit `hyphens: auto` is emitted in KFX output.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
99pub enum Hyphens {
100    Auto,
101    #[default]
102    Manual,
103    None,
104}
105
106impl ToCss for Hyphens {
107    fn to_css(&self, buf: &mut String) {
108        buf.push_str(match self {
109            Hyphens::Auto => "auto",
110            Hyphens::Manual => "manual",
111            Hyphens::None => "none",
112        });
113    }
114}
115
116/// Text decoration line style.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
118pub enum DecorationStyle {
119    #[default]
120    None,
121    Solid,
122    Dotted,
123    Dashed,
124    Double,
125}
126
127impl ToCss for DecorationStyle {
128    fn to_css(&self, buf: &mut String) {
129        buf.push_str(match self {
130            DecorationStyle::None => "none",
131            DecorationStyle::Solid => "solid",
132            DecorationStyle::Dotted => "dotted",
133            DecorationStyle::Dashed => "dashed",
134            DecorationStyle::Double => "double",
135        });
136    }
137}
138
139/// Float positioning.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
141pub enum Float {
142    #[default]
143    None,
144    Left,
145    Right,
146}
147
148impl ToCss for Float {
149    fn to_css(&self, buf: &mut String) {
150        buf.push_str(match self {
151            Float::None => "none",
152            Float::Left => "left",
153            Float::Right => "right",
154        });
155    }
156}
157
158/// Page break behavior.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
160pub enum BreakValue {
161    #[default]
162    Auto,
163    Always,
164    Avoid,
165    /// Break to a new column (for multi-column layouts)
166    Column,
167}
168
169impl ToCss for BreakValue {
170    fn to_css(&self, buf: &mut String) {
171        buf.push_str(match self {
172            BreakValue::Auto => "auto",
173            BreakValue::Always => "always",
174            BreakValue::Avoid => "avoid",
175            BreakValue::Column => "column",
176        });
177    }
178}
179
180/// Border style values.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
182pub enum BorderStyle {
183    #[default]
184    None,
185    Solid,
186    Dotted,
187    Dashed,
188    Double,
189    Groove,
190    Ridge,
191    Inset,
192    Outset,
193}
194
195impl ToCss for BorderStyle {
196    fn to_css(&self, buf: &mut String) {
197        buf.push_str(match self {
198            BorderStyle::None => "none",
199            BorderStyle::Solid => "solid",
200            BorderStyle::Dotted => "dotted",
201            BorderStyle::Dashed => "dashed",
202            BorderStyle::Double => "double",
203            BorderStyle::Groove => "groove",
204            BorderStyle::Ridge => "ridge",
205            BorderStyle::Inset => "inset",
206            BorderStyle::Outset => "outset",
207        });
208    }
209}
210
211/// List style position.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
213pub enum ListStylePosition {
214    #[default]
215    Outside,
216    Inside,
217}
218
219impl ToCss for ListStylePosition {
220    fn to_css(&self, buf: &mut String) {
221        buf.push_str(match self {
222            ListStylePosition::Outside => "outside",
223            ListStylePosition::Inside => "inside",
224        });
225    }
226}
227
228/// CSS visibility values.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
230pub enum Visibility {
231    #[default]
232    Visible,
233    Hidden,
234    Collapse,
235}
236
237impl ToCss for Visibility {
238    fn to_css(&self, buf: &mut String) {
239        buf.push_str(match self {
240            Visibility::Visible => "visible",
241            Visibility::Hidden => "hidden",
242            Visibility::Collapse => "collapse",
243        });
244    }
245}
246
247/// CSS box-sizing values.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
249pub enum BoxSizing {
250    /// Width/height include only content (CSS default)
251    #[default]
252    ContentBox,
253    /// Width/height include padding and border
254    BorderBox,
255}
256
257impl ToCss for BoxSizing {
258    fn to_css(&self, buf: &mut String) {
259        buf.push_str(match self {
260            BoxSizing::ContentBox => "content-box",
261            BoxSizing::BorderBox => "border-box",
262        });
263    }
264}
265
266impl ToCss for FontVariant {
267    fn to_css(&self, buf: &mut String) {
268        buf.push_str(match self {
269            FontVariant::Normal => "normal",
270            FontVariant::SmallCaps => "small-caps",
271        });
272    }
273}
274
275/// Text alignment.
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
277pub enum TextAlign {
278    #[default]
279    Start,
280    End,
281    Left,
282    Right,
283    Center,
284    Justify,
285}
286
287impl ToCss for TextAlign {
288    fn to_css(&self, buf: &mut String) {
289        buf.push_str(match self {
290            TextAlign::Start => "start",
291            TextAlign::End => "end",
292            TextAlign::Left => "left",
293            TextAlign::Right => "right",
294            TextAlign::Center => "center",
295            TextAlign::Justify => "justify",
296        });
297    }
298}
299
300/// Display mode.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
302pub enum Display {
303    #[default]
304    Block,
305    Inline,
306    None,
307    ListItem,
308    TableCell,
309    TableRow,
310}
311
312/// CSS list-style-type values.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
314pub enum ListStyleType {
315    /// No marker
316    #[default]
317    None,
318    /// • (default for ul)
319    Disc,
320    /// ○
321    Circle,
322    /// ▪
323    Square,
324    /// 1, 2, 3 (default for ol)
325    Decimal,
326    /// a, b, c
327    LowerAlpha,
328    /// A, B, C
329    UpperAlpha,
330    /// i, ii, iii
331    LowerRoman,
332    /// I, II, III
333    UpperRoman,
334}
335
336impl ToCss for ListStyleType {
337    fn to_css(&self, buf: &mut String) {
338        buf.push_str(match self {
339            ListStyleType::None => "none",
340            ListStyleType::Disc => "disc",
341            ListStyleType::Circle => "circle",
342            ListStyleType::Square => "square",
343            ListStyleType::Decimal => "decimal",
344            ListStyleType::LowerAlpha => "lower-alpha",
345            ListStyleType::UpperAlpha => "upper-alpha",
346            ListStyleType::LowerRoman => "lower-roman",
347            ListStyleType::UpperRoman => "upper-roman",
348        });
349    }
350}
351
352impl ToCss for Display {
353    fn to_css(&self, buf: &mut String) {
354        buf.push_str(match self {
355            Display::Block => "block",
356            Display::Inline => "inline",
357            Display::None => "none",
358            Display::ListItem => "list-item",
359            Display::TableCell => "table-cell",
360            Display::TableRow => "table-row",
361        });
362    }
363}
364
365/// RGBA color (8 bits per channel).
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
367pub struct Color {
368    pub r: u8,
369    pub g: u8,
370    pub b: u8,
371    pub a: u8,
372}
373
374impl Color {
375    pub const BLACK: Color = Color {
376        r: 0,
377        g: 0,
378        b: 0,
379        a: 255,
380    };
381    pub const WHITE: Color = Color {
382        r: 255,
383        g: 255,
384        b: 255,
385        a: 255,
386    };
387    pub const TRANSPARENT: Color = Color {
388        r: 0,
389        g: 0,
390        b: 0,
391        a: 0,
392    };
393
394    /// Create a new opaque color.
395    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
396        Self { r, g, b, a: 255 }
397    }
398
399    /// Create a new color with alpha.
400    pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
401        Self { r, g, b, a }
402    }
403}
404
405impl ToCss for Color {
406    fn to_css(&self, buf: &mut String) {
407        if self.a == 255 {
408            // Opaque: use #RRGGBB
409            write!(buf, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b).unwrap();
410        } else if self.a == 0 {
411            buf.push_str("transparent");
412        } else {
413            // With alpha: use rgba()
414            let alpha = self.a as f32 / 255.0;
415            write!(buf, "rgba({},{},{},{:.2})", self.r, self.g, self.b, alpha).unwrap();
416        }
417    }
418}
419
420/// Length value with unit.
421#[derive(Debug, Clone, Copy, PartialEq, Default)]
422pub enum Length {
423    #[default]
424    Auto,
425    Px(f32),
426    Em(f32),
427    Rem(f32),
428    Percent(f32),
429}
430
431impl Eq for Length {}
432
433impl Hash for Length {
434    fn hash<H: Hasher>(&self, state: &mut H) {
435        match self {
436            Length::Auto => 0u8.hash(state),
437            Length::Px(v) => {
438                1u8.hash(state);
439                v.to_bits().hash(state);
440            }
441            Length::Em(v) => {
442                2u8.hash(state);
443                v.to_bits().hash(state);
444            }
445            Length::Rem(v) => {
446                3u8.hash(state);
447                v.to_bits().hash(state);
448            }
449            Length::Percent(v) => {
450                4u8.hash(state);
451                v.to_bits().hash(state);
452            }
453        }
454    }
455}
456
457impl ToCss for Length {
458    fn to_css(&self, buf: &mut String) {
459        match self {
460            Length::Auto => buf.push_str("auto"),
461            Length::Px(v) => {
462                if *v == 0.0 {
463                    buf.push('0');
464                } else {
465                    write!(buf, "{}px", v).unwrap();
466                }
467            }
468            Length::Em(v) => write!(buf, "{}em", v).unwrap(),
469            Length::Rem(v) => write!(buf, "{}rem", v).unwrap(),
470            Length::Percent(v) => write!(buf, "{}%", v).unwrap(),
471        }
472    }
473}
474
475/// Computed style for a node (all properties resolved).
476#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
477pub struct ComputedStyle {
478    // Font properties
479    pub font_family: Option<String>,
480    pub font_size: Length,
481    pub font_weight: FontWeight,
482    pub font_style: FontStyle,
483
484    // Colors
485    pub color: Option<Color>,
486    pub background_color: Option<Color>,
487
488    // Text
489    pub text_align: TextAlign,
490    pub text_indent: Length,
491    pub line_height: Length,
492    pub text_decoration_underline: bool,
493    pub text_decoration_line_through: bool,
494
495    // Box model
496    pub display: Display,
497    pub margin_top: Length,
498    pub margin_bottom: Length,
499    pub margin_left: Length,
500    pub margin_right: Length,
501    pub padding_top: Length,
502    pub padding_bottom: Length,
503    pub padding_left: Length,
504    pub padding_right: Length,
505
506    // Vertical alignment for inline elements
507    pub vertical_align_super: bool,
508    pub vertical_align_sub: bool,
509
510    // List properties
511    pub list_style_type: ListStyleType,
512
513    // Font variant
514    pub font_variant: FontVariant,
515
516    // Text spacing
517    pub letter_spacing: Length,
518    pub word_spacing: Length,
519
520    // Text transform
521    pub text_transform: TextTransform,
522
523    // Hyphenation
524    pub hyphens: Hyphens,
525
526    // No-break (white-space: nowrap)
527    pub no_break: bool,
528
529    // Phase 2: Text decoration extensions
530    pub underline_style: DecorationStyle,
531    pub overline: bool,
532    pub underline_color: Option<Color>,
533
534    // Phase 3: Layout properties
535    pub width: Length,
536    pub height: Length,
537    pub max_width: Length,
538    pub min_height: Length,
539    pub float: Float,
540
541    // Phase 4: Page break properties
542    pub break_before: BreakValue,
543    pub break_after: BreakValue,
544    pub break_inside: BreakValue,
545
546    // Phase 5: Border properties (4 sides)
547    pub border_style_top: BorderStyle,
548    pub border_style_right: BorderStyle,
549    pub border_style_bottom: BorderStyle,
550    pub border_style_left: BorderStyle,
551    pub border_width_top: Length,
552    pub border_width_right: Length,
553    pub border_width_bottom: Length,
554    pub border_width_left: Length,
555    pub border_color_top: Option<Color>,
556    pub border_color_right: Option<Color>,
557    pub border_color_bottom: Option<Color>,
558    pub border_color_left: Option<Color>,
559    // Border radius (corners)
560    pub border_radius_top_left: Length,
561    pub border_radius_top_right: Length,
562    pub border_radius_bottom_left: Length,
563    pub border_radius_bottom_right: Length,
564
565    // Phase 6: List properties
566    pub list_style_position: ListStylePosition,
567
568    // Phase 7: Amazon properties
569    pub language: Option<String>,
570    pub visibility: Visibility,
571    pub box_sizing: BoxSizing,
572}
573
574impl ComputedStyle {
575    /// Check if this style differs from the default (has any non-default properties).
576    pub fn is_default(&self) -> bool {
577        *self == ComputedStyle::default()
578    }
579
580    // --- Modifier checks for exporters ---
581
582    /// Check if the style is bold (font-weight >= 700).
583    #[inline]
584    pub fn is_bold(&self) -> bool {
585        self.font_weight.0 >= 700
586    }
587
588    /// Check if the style is italic.
589    #[inline]
590    pub fn is_italic(&self) -> bool {
591        matches!(self.font_style, FontStyle::Italic | FontStyle::Oblique)
592    }
593
594    /// Check if the style has underline decoration.
595    #[inline]
596    pub fn is_underline(&self) -> bool {
597        self.text_decoration_underline
598    }
599
600    /// Check if the style has strikethrough decoration.
601    #[inline]
602    pub fn is_strikethrough(&self) -> bool {
603        self.text_decoration_line_through
604    }
605
606    /// Check if the style is superscript.
607    #[inline]
608    pub fn is_superscript(&self) -> bool {
609        self.vertical_align_super
610    }
611
612    /// Check if the style is subscript.
613    #[inline]
614    pub fn is_subscript(&self) -> bool {
615        self.vertical_align_sub
616    }
617
618    /// Check if the style uses a monospace font.
619    pub fn is_monospace(&self) -> bool {
620        self.font_family
621            .as_ref()
622            .map(|f| {
623                let lower = f.to_lowercase();
624                lower.contains("mono")
625                    || lower.contains("courier")
626                    || lower.contains("consolas")
627                    || lower.contains("menlo")
628            })
629            .unwrap_or(false)
630    }
631
632    /// Check if the list style is ordered (numbered).
633    pub fn is_ordered_list(&self) -> bool {
634        matches!(
635            self.list_style_type,
636            ListStyleType::Decimal
637                | ListStyleType::LowerAlpha
638                | ListStyleType::UpperAlpha
639                | ListStyleType::LowerRoman
640                | ListStyleType::UpperRoman
641        )
642    }
643
644    /// Check if the style uses small-caps font variant.
645    #[inline]
646    pub fn is_small_caps(&self) -> bool {
647        matches!(self.font_variant, FontVariant::SmallCaps)
648    }
649}
650
651impl ToCss for ComputedStyle {
652    fn to_css(&self, buf: &mut String) {
653        let default = ComputedStyle::default();
654
655        // Font properties
656        if let Some(ref family) = self.font_family {
657            write!(buf, "font-family: {}; ", family).unwrap();
658        }
659        if self.font_size != default.font_size {
660            buf.push_str("font-size: ");
661            self.font_size.to_css(buf);
662            buf.push_str("; ");
663        }
664        if self.font_weight != default.font_weight {
665            buf.push_str("font-weight: ");
666            self.font_weight.to_css(buf);
667            buf.push_str("; ");
668        }
669        if self.font_style != default.font_style {
670            buf.push_str("font-style: ");
671            self.font_style.to_css(buf);
672            buf.push_str("; ");
673        }
674
675        // Colors
676        if let Some(color) = self.color {
677            buf.push_str("color: ");
678            color.to_css(buf);
679            buf.push_str("; ");
680        }
681        if let Some(bg) = self.background_color {
682            buf.push_str("background-color: ");
683            bg.to_css(buf);
684            buf.push_str("; ");
685        }
686
687        // Text properties
688        if self.text_align != default.text_align {
689            buf.push_str("text-align: ");
690            self.text_align.to_css(buf);
691            buf.push_str("; ");
692        }
693        if self.text_indent != default.text_indent {
694            buf.push_str("text-indent: ");
695            self.text_indent.to_css(buf);
696            buf.push_str("; ");
697        }
698        if self.line_height != default.line_height {
699            buf.push_str("line-height: ");
700            self.line_height.to_css(buf);
701            buf.push_str("; ");
702        }
703
704        // Text decorations
705        let mut decorations = Vec::new();
706        if self.text_decoration_underline {
707            decorations.push("underline");
708        }
709        if self.text_decoration_line_through {
710            decorations.push("line-through");
711        }
712        if !decorations.is_empty() {
713            write!(buf, "text-decoration: {}; ", decorations.join(" ")).unwrap();
714        }
715
716        // Display (only if not block, which is the semantic default for most elements)
717        if self.display != default.display {
718            buf.push_str("display: ");
719            self.display.to_css(buf);
720            buf.push_str("; ");
721        }
722
723        // Margins
724        if self.margin_top != default.margin_top {
725            buf.push_str("margin-top: ");
726            self.margin_top.to_css(buf);
727            buf.push_str("; ");
728        }
729        if self.margin_bottom != default.margin_bottom {
730            buf.push_str("margin-bottom: ");
731            self.margin_bottom.to_css(buf);
732            buf.push_str("; ");
733        }
734        if self.margin_left != default.margin_left {
735            buf.push_str("margin-left: ");
736            self.margin_left.to_css(buf);
737            buf.push_str("; ");
738        }
739        if self.margin_right != default.margin_right {
740            buf.push_str("margin-right: ");
741            self.margin_right.to_css(buf);
742            buf.push_str("; ");
743        }
744
745        // Padding
746        if self.padding_top != default.padding_top {
747            buf.push_str("padding-top: ");
748            self.padding_top.to_css(buf);
749            buf.push_str("; ");
750        }
751        if self.padding_bottom != default.padding_bottom {
752            buf.push_str("padding-bottom: ");
753            self.padding_bottom.to_css(buf);
754            buf.push_str("; ");
755        }
756        if self.padding_left != default.padding_left {
757            buf.push_str("padding-left: ");
758            self.padding_left.to_css(buf);
759            buf.push_str("; ");
760        }
761        if self.padding_right != default.padding_right {
762            buf.push_str("padding-right: ");
763            self.padding_right.to_css(buf);
764            buf.push_str("; ");
765        }
766
767        // Vertical alignment
768        if self.vertical_align_super {
769            buf.push_str("vertical-align: super; ");
770        } else if self.vertical_align_sub {
771            buf.push_str("vertical-align: sub; ");
772        }
773
774        // List style
775        if self.list_style_type != default.list_style_type {
776            buf.push_str("list-style-type: ");
777            self.list_style_type.to_css(buf);
778            buf.push_str("; ");
779        }
780
781        // Font variant
782        if self.font_variant != FontVariant::Normal {
783            buf.push_str("font-variant: ");
784            self.font_variant.to_css(buf);
785            buf.push_str("; ");
786        }
787
788        // Letter spacing
789        if self.letter_spacing != default.letter_spacing {
790            buf.push_str("letter-spacing: ");
791            self.letter_spacing.to_css(buf);
792            buf.push_str("; ");
793        }
794
795        // Word spacing
796        if self.word_spacing != default.word_spacing {
797            buf.push_str("word-spacing: ");
798            self.word_spacing.to_css(buf);
799            buf.push_str("; ");
800        }
801
802        // Text transform
803        if self.text_transform != default.text_transform {
804            buf.push_str("text-transform: ");
805            self.text_transform.to_css(buf);
806            buf.push_str("; ");
807        }
808
809        // Hyphens
810        if self.hyphens != default.hyphens {
811            buf.push_str("hyphens: ");
812            self.hyphens.to_css(buf);
813            buf.push_str("; ");
814        }
815
816        // White-space nowrap
817        if self.no_break {
818            buf.push_str("white-space: nowrap; ");
819        }
820
821        // Underline style (if different from boolean)
822        if self.underline_style != default.underline_style {
823            buf.push_str("text-decoration-style: ");
824            self.underline_style.to_css(buf);
825            buf.push_str("; ");
826        }
827
828        // Overline
829        if self.overline {
830            buf.push_str("text-decoration-line: overline; ");
831        }
832
833        // Underline color
834        if let Some(color) = self.underline_color {
835            buf.push_str("text-decoration-color: ");
836            color.to_css(buf);
837            buf.push_str("; ");
838        }
839
840        // Width
841        if self.width != default.width {
842            buf.push_str("width: ");
843            self.width.to_css(buf);
844            buf.push_str("; ");
845        }
846
847        // Height
848        if self.height != default.height {
849            buf.push_str("height: ");
850            self.height.to_css(buf);
851            buf.push_str("; ");
852        }
853
854        // Max-width
855        if self.max_width != default.max_width {
856            buf.push_str("max-width: ");
857            self.max_width.to_css(buf);
858            buf.push_str("; ");
859        }
860
861        // Min-height
862        if self.min_height != default.min_height {
863            buf.push_str("min-height: ");
864            self.min_height.to_css(buf);
865            buf.push_str("; ");
866        }
867
868        // Float
869        if self.float != default.float {
870            buf.push_str("float: ");
871            self.float.to_css(buf);
872            buf.push_str("; ");
873        }
874
875        // Break before
876        if self.break_before != default.break_before {
877            buf.push_str("break-before: ");
878            self.break_before.to_css(buf);
879            buf.push_str("; ");
880        }
881
882        // Break after
883        if self.break_after != default.break_after {
884            buf.push_str("break-after: ");
885            self.break_after.to_css(buf);
886            buf.push_str("; ");
887        }
888
889        // Break inside
890        if self.break_inside != default.break_inside {
891            buf.push_str("break-inside: ");
892            self.break_inside.to_css(buf);
893            buf.push_str("; ");
894        }
895
896        // Border styles
897        if self.border_style_top != default.border_style_top {
898            buf.push_str("border-top-style: ");
899            self.border_style_top.to_css(buf);
900            buf.push_str("; ");
901        }
902        if self.border_style_right != default.border_style_right {
903            buf.push_str("border-right-style: ");
904            self.border_style_right.to_css(buf);
905            buf.push_str("; ");
906        }
907        if self.border_style_bottom != default.border_style_bottom {
908            buf.push_str("border-bottom-style: ");
909            self.border_style_bottom.to_css(buf);
910            buf.push_str("; ");
911        }
912        if self.border_style_left != default.border_style_left {
913            buf.push_str("border-left-style: ");
914            self.border_style_left.to_css(buf);
915            buf.push_str("; ");
916        }
917
918        // Border widths
919        if self.border_width_top != default.border_width_top {
920            buf.push_str("border-top-width: ");
921            self.border_width_top.to_css(buf);
922            buf.push_str("; ");
923        }
924        if self.border_width_right != default.border_width_right {
925            buf.push_str("border-right-width: ");
926            self.border_width_right.to_css(buf);
927            buf.push_str("; ");
928        }
929        if self.border_width_bottom != default.border_width_bottom {
930            buf.push_str("border-bottom-width: ");
931            self.border_width_bottom.to_css(buf);
932            buf.push_str("; ");
933        }
934        if self.border_width_left != default.border_width_left {
935            buf.push_str("border-left-width: ");
936            self.border_width_left.to_css(buf);
937            buf.push_str("; ");
938        }
939
940        // Border colors
941        if let Some(color) = self.border_color_top {
942            buf.push_str("border-top-color: ");
943            color.to_css(buf);
944            buf.push_str("; ");
945        }
946        if let Some(color) = self.border_color_right {
947            buf.push_str("border-right-color: ");
948            color.to_css(buf);
949            buf.push_str("; ");
950        }
951        if let Some(color) = self.border_color_bottom {
952            buf.push_str("border-bottom-color: ");
953            color.to_css(buf);
954            buf.push_str("; ");
955        }
956        if let Some(color) = self.border_color_left {
957            buf.push_str("border-left-color: ");
958            color.to_css(buf);
959            buf.push_str("; ");
960        }
961
962        // Border radius
963        if self.border_radius_top_left != default.border_radius_top_left {
964            buf.push_str("border-top-left-radius: ");
965            self.border_radius_top_left.to_css(buf);
966            buf.push_str("; ");
967        }
968        if self.border_radius_top_right != default.border_radius_top_right {
969            buf.push_str("border-top-right-radius: ");
970            self.border_radius_top_right.to_css(buf);
971            buf.push_str("; ");
972        }
973        if self.border_radius_bottom_left != default.border_radius_bottom_left {
974            buf.push_str("border-bottom-left-radius: ");
975            self.border_radius_bottom_left.to_css(buf);
976            buf.push_str("; ");
977        }
978        if self.border_radius_bottom_right != default.border_radius_bottom_right {
979            buf.push_str("border-bottom-right-radius: ");
980            self.border_radius_bottom_right.to_css(buf);
981            buf.push_str("; ");
982        }
983
984        // List style position
985        if self.list_style_position != default.list_style_position {
986            buf.push_str("list-style-position: ");
987            self.list_style_position.to_css(buf);
988            buf.push_str("; ");
989        }
990
991        // Visibility
992        if self.visibility != default.visibility {
993            buf.push_str("visibility: ");
994            self.visibility.to_css(buf);
995            buf.push_str("; ");
996        }
997
998        // Language (output as data attribute comment - not standard CSS)
999        // Note: language is stored but typically output via HTML lang attribute
1000    }
1001}
1002
1003/// SoA style pool for efficient storage and deduplication.
1004///
1005/// Styles are interned: identical styles share the same StyleId.
1006/// This is memory-efficient when many elements share the same style.
1007#[derive(Clone)]
1008pub struct StylePool {
1009    /// All unique styles.
1010    styles: Vec<ComputedStyle>,
1011    /// Hash-based deduplication map.
1012    intern_map: HashMap<ComputedStyle, StyleId>,
1013}
1014
1015impl Default for StylePool {
1016    fn default() -> Self {
1017        Self::new()
1018    }
1019}
1020
1021impl StylePool {
1022    /// Create a new style pool with the default style at index 0.
1023    pub fn new() -> Self {
1024        let default_style = ComputedStyle::default();
1025        let mut intern_map = HashMap::new();
1026        intern_map.insert(default_style.clone(), StyleId::DEFAULT);
1027
1028        Self {
1029            styles: vec![default_style],
1030            intern_map,
1031        }
1032    }
1033
1034    /// Intern a style, returning its StyleId.
1035    ///
1036    /// If an identical style already exists, returns the existing ID.
1037    /// Otherwise, allocates a new style and returns its ID.
1038    pub fn intern(&mut self, style: ComputedStyle) -> StyleId {
1039        if let Some(&id) = self.intern_map.get(&style) {
1040            return id;
1041        }
1042
1043        let id = StyleId(self.styles.len() as u32);
1044        self.intern_map.insert(style.clone(), id);
1045        self.styles.push(style);
1046        id
1047    }
1048
1049    /// Get a style by ID.
1050    pub fn get(&self, id: StyleId) -> Option<&ComputedStyle> {
1051        self.styles.get(id.0 as usize)
1052    }
1053
1054    /// Get the number of unique styles.
1055    pub fn len(&self) -> usize {
1056        self.styles.len()
1057    }
1058
1059    /// Check if the pool is empty (should never be, as default style is always present).
1060    pub fn is_empty(&self) -> bool {
1061        self.styles.is_empty()
1062    }
1063
1064    /// Iterate over all (StyleId, ComputedStyle) pairs.
1065    pub fn iter(&self) -> impl Iterator<Item = (StyleId, &ComputedStyle)> {
1066        self.styles
1067            .iter()
1068            .enumerate()
1069            .map(|(i, s)| (StyleId(i as u32), s))
1070    }
1071}
1072
1073impl std::fmt::Debug for StylePool {
1074    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1075        f.debug_struct("StylePool")
1076            .field("count", &self.styles.len())
1077            .finish()
1078    }
1079}
1080
1081#[cfg(test)]
1082#[allow(clippy::field_reassign_with_default)]
1083mod tests {
1084    use super::*;
1085
1086    #[test]
1087    fn test_color_to_css_opaque() {
1088        assert_eq!(Color::BLACK.to_css_string(), "#000000");
1089        assert_eq!(Color::WHITE.to_css_string(), "#ffffff");
1090        assert_eq!(Color::rgb(255, 0, 0).to_css_string(), "#ff0000");
1091        assert_eq!(Color::rgb(0, 128, 255).to_css_string(), "#0080ff");
1092    }
1093
1094    #[test]
1095    fn test_color_to_css_transparent() {
1096        assert_eq!(Color::TRANSPARENT.to_css_string(), "transparent");
1097    }
1098
1099    #[test]
1100    fn test_color_to_css_alpha() {
1101        let color = Color::rgba(255, 0, 0, 128);
1102        let css = color.to_css_string();
1103        assert!(css.starts_with("rgba(255,0,0,"));
1104        assert!(css.contains("0.50")); // ~128/255
1105    }
1106
1107    #[test]
1108    fn test_length_to_css() {
1109        assert_eq!(Length::Auto.to_css_string(), "auto");
1110        assert_eq!(Length::Px(0.0).to_css_string(), "0");
1111        assert_eq!(Length::Px(16.0).to_css_string(), "16px");
1112        assert_eq!(Length::Em(1.5).to_css_string(), "1.5em");
1113        assert_eq!(Length::Rem(2.0).to_css_string(), "2rem");
1114        assert_eq!(Length::Percent(50.0).to_css_string(), "50%");
1115    }
1116
1117    #[test]
1118    fn test_font_weight_to_css() {
1119        assert_eq!(FontWeight::NORMAL.to_css_string(), "normal");
1120        assert_eq!(FontWeight::BOLD.to_css_string(), "bold");
1121        assert_eq!(FontWeight(300).to_css_string(), "300");
1122        assert_eq!(FontWeight(600).to_css_string(), "600");
1123    }
1124
1125    #[test]
1126    fn test_font_style_to_css() {
1127        assert_eq!(FontStyle::Normal.to_css_string(), "normal");
1128        assert_eq!(FontStyle::Italic.to_css_string(), "italic");
1129        assert_eq!(FontStyle::Oblique.to_css_string(), "oblique");
1130    }
1131
1132    #[test]
1133    fn test_text_align_to_css() {
1134        assert_eq!(TextAlign::Left.to_css_string(), "left");
1135        assert_eq!(TextAlign::Center.to_css_string(), "center");
1136        assert_eq!(TextAlign::Justify.to_css_string(), "justify");
1137    }
1138
1139    #[test]
1140    fn test_display_to_css() {
1141        assert_eq!(Display::Block.to_css_string(), "block");
1142        assert_eq!(Display::Inline.to_css_string(), "inline");
1143        assert_eq!(Display::None.to_css_string(), "none");
1144    }
1145
1146    #[test]
1147    fn test_computed_style_to_css_default() {
1148        let style = ComputedStyle::default();
1149        // Default style should produce empty CSS (no non-default properties)
1150        assert_eq!(style.to_css_string(), "");
1151    }
1152
1153    #[test]
1154    fn test_computed_style_to_css_bold() {
1155        let mut style = ComputedStyle::default();
1156        style.font_weight = FontWeight::BOLD;
1157        let css = style.to_css_string();
1158        assert!(css.contains("font-weight: bold;"));
1159    }
1160
1161    #[test]
1162    fn test_computed_style_to_css_multiple() {
1163        let mut style = ComputedStyle::default();
1164        style.font_weight = FontWeight::BOLD;
1165        style.font_style = FontStyle::Italic;
1166        style.color = Some(Color::rgb(255, 0, 0));
1167        style.text_align = TextAlign::Center;
1168
1169        let css = style.to_css_string();
1170        assert!(css.contains("font-weight: bold;"));
1171        assert!(css.contains("font-style: italic;"));
1172        assert!(css.contains("color: #ff0000;"));
1173        assert!(css.contains("text-align: center;"));
1174    }
1175
1176    #[test]
1177    fn test_computed_style_to_css_decorations() {
1178        let mut style = ComputedStyle::default();
1179        style.text_decoration_underline = true;
1180        style.text_decoration_line_through = true;
1181
1182        let css = style.to_css_string();
1183        assert!(css.contains("text-decoration: underline line-through;"));
1184    }
1185
1186    #[test]
1187    fn test_style_pool_interning() {
1188        let mut pool = StylePool::new();
1189
1190        let mut style1 = ComputedStyle::default();
1191        style1.font_weight = FontWeight::BOLD;
1192
1193        let id1 = pool.intern(style1.clone());
1194        let id2 = pool.intern(style1);
1195
1196        // Same style should get same ID
1197        assert_eq!(id1, id2);
1198        assert_eq!(pool.len(), 2); // default + bold
1199    }
1200
1201    #[test]
1202    fn test_style_pool_iter() {
1203        let mut pool = StylePool::new();
1204
1205        let mut style = ComputedStyle::default();
1206        style.font_weight = FontWeight::BOLD;
1207        pool.intern(style);
1208
1209        let ids: Vec<StyleId> = pool.iter().map(|(id, _)| id).collect();
1210        assert_eq!(ids, vec![StyleId(0), StyleId(1)]);
1211    }
1212}