Skip to main content

cssbox_dom/
css.rs

1//! CSS parsing and property resolution.
2//!
3//! Uses lightningcss for parsing CSS property values into typed Rust structs.
4
5use cssbox_core::style::*;
6use cssbox_core::values::*;
7
8/// A parsed CSS declaration (property: value pair).
9#[derive(Debug, Clone)]
10pub struct CssDeclaration {
11    pub property: String,
12    pub value: String,
13    pub important: bool,
14}
15
16/// Parse a CSS style string (e.g., from a `style` attribute) into declarations.
17pub fn parse_style_attribute(style: &str) -> Vec<CssDeclaration> {
18    let mut declarations = Vec::new();
19
20    for decl_str in style.split(';') {
21        let decl_str = decl_str.trim();
22        if decl_str.is_empty() {
23            continue;
24        }
25
26        if let Some((property, value)) = decl_str.split_once(':') {
27            let property = property.trim().to_lowercase();
28            let value = value.trim().to_string();
29            let important = value.contains("!important");
30            let value = value.replace("!important", "").trim().to_string();
31
32            declarations.push(CssDeclaration {
33                property,
34                value,
35                important,
36            });
37        }
38    }
39
40    declarations
41}
42
43/// Parse a CSS stylesheet string into a list of rules.
44pub fn parse_stylesheet(css: &str) -> Vec<CssRule> {
45    let mut rules = Vec::new();
46    let mut pos = 0;
47    let bytes = css.as_bytes();
48    let len = bytes.len();
49
50    while pos < len {
51        // Skip whitespace and comments
52        pos = skip_whitespace_comments(css, pos);
53        if pos >= len {
54            break;
55        }
56
57        // Find selector (everything before '{')
58        let selector_start = pos;
59        while pos < len && bytes[pos] != b'{' {
60            pos += 1;
61        }
62        if pos >= len {
63            break;
64        }
65        let selector = css[selector_start..pos].trim().to_string();
66        pos += 1; // skip '{'
67
68        // Find declarations (everything before '}')
69        let decl_start = pos;
70        let mut depth = 1;
71        while pos < len && depth > 0 {
72            if bytes[pos] == b'{' {
73                depth += 1;
74            } else if bytes[pos] == b'}' {
75                depth -= 1;
76            }
77            if depth > 0 {
78                pos += 1;
79            }
80        }
81        let decl_str = &css[decl_start..pos];
82        pos += 1; // skip '}'
83
84        if !selector.is_empty() {
85            let declarations = parse_style_attribute(decl_str);
86            rules.push(CssRule {
87                selector,
88                declarations,
89            });
90        }
91    }
92
93    rules
94}
95
96fn skip_whitespace_comments(css: &str, mut pos: usize) -> usize {
97    let bytes = css.as_bytes();
98    let len = bytes.len();
99
100    while pos < len {
101        if bytes[pos].is_ascii_whitespace() {
102            pos += 1;
103        } else if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'*' {
104            // Block comment
105            pos += 2;
106            while pos + 1 < len && !(bytes[pos] == b'*' && bytes[pos + 1] == b'/') {
107                pos += 1;
108            }
109            pos += 2;
110        } else {
111            break;
112        }
113    }
114    pos
115}
116
117/// A CSS rule (selector + declarations).
118#[derive(Debug, Clone)]
119pub struct CssRule {
120    pub selector: String,
121    pub declarations: Vec<CssDeclaration>,
122}
123
124/// Apply a set of CSS declarations to a ComputedStyle.
125pub fn apply_declarations(style: &mut ComputedStyle, declarations: &[CssDeclaration]) {
126    for decl in declarations {
127        apply_property(style, &decl.property, &decl.value);
128    }
129}
130
131/// Apply a single CSS property to a ComputedStyle.
132pub fn apply_property(style: &mut ComputedStyle, property: &str, value: &str) {
133    match property {
134        "display" => {
135            style.display = parse_display(value);
136        }
137        "position" => {
138            style.position = match value {
139                "static" => Position::Static,
140                "relative" => Position::Relative,
141                "absolute" => Position::Absolute,
142                "fixed" => Position::Fixed,
143                "sticky" => Position::Sticky,
144                _ => Position::Static,
145            };
146        }
147        "float" => {
148            style.float = match value {
149                "left" => Float::Left,
150                "right" => Float::Right,
151                "none" => Float::None,
152                _ => Float::None,
153            };
154        }
155        "clear" => {
156            style.clear = match value {
157                "left" => Clear::Left,
158                "right" => Clear::Right,
159                "both" => Clear::Both,
160                "none" => Clear::None,
161                _ => Clear::None,
162            };
163        }
164        "box-sizing" => {
165            style.box_sizing = match value {
166                "border-box" => BoxSizing::BorderBox,
167                "content-box" => BoxSizing::ContentBox,
168                _ => BoxSizing::ContentBox,
169            };
170        }
171        "width" => {
172            style.width = parse_length_percentage_auto(value);
173        }
174        "height" => {
175            style.height = parse_length_percentage_auto(value);
176        }
177        "min-width" => {
178            style.min_width = parse_length_percentage(value);
179        }
180        "min-height" => {
181            style.min_height = parse_length_percentage(value);
182        }
183        "max-width" => {
184            style.max_width = parse_length_percentage_none(value);
185        }
186        "max-height" => {
187            style.max_height = parse_length_percentage_none(value);
188        }
189        "margin" => {
190            let edges = parse_shorthand_edges(value);
191            style.margin_top = edges.0;
192            style.margin_right = edges.1;
193            style.margin_bottom = edges.2;
194            style.margin_left = edges.3;
195        }
196        "margin-top" => style.margin_top = parse_length_percentage_auto(value),
197        "margin-right" => style.margin_right = parse_length_percentage_auto(value),
198        "margin-bottom" => style.margin_bottom = parse_length_percentage_auto(value),
199        "margin-left" => style.margin_left = parse_length_percentage_auto(value),
200        "padding" => {
201            let edges = parse_shorthand_lp_edges(value);
202            style.padding_top = edges.0;
203            style.padding_right = edges.1;
204            style.padding_bottom = edges.2;
205            style.padding_left = edges.3;
206        }
207        "padding-top" => style.padding_top = parse_length_percentage(value),
208        "padding-right" => style.padding_right = parse_length_percentage(value),
209        "padding-bottom" => style.padding_bottom = parse_length_percentage(value),
210        "padding-left" => style.padding_left = parse_length_percentage(value),
211        "border-width" => {
212            let w = parse_px(value);
213            style.border_top_width = w;
214            style.border_right_width = w;
215            style.border_bottom_width = w;
216            style.border_left_width = w;
217        }
218        "border-top-width" => style.border_top_width = parse_px(value),
219        "border-right-width" => style.border_right_width = parse_px(value),
220        "border-bottom-width" => style.border_bottom_width = parse_px(value),
221        "border-left-width" => style.border_left_width = parse_px(value),
222        "border" => {
223            // Simplified: just extract width
224            let parts: Vec<&str> = value.split_whitespace().collect();
225            if let Some(first) = parts.first() {
226                let w = parse_px(first);
227                style.border_top_width = w;
228                style.border_right_width = w;
229                style.border_bottom_width = w;
230                style.border_left_width = w;
231            }
232        }
233        "top" => style.top = parse_length_percentage_auto(value),
234        "right" => style.right = parse_length_percentage_auto(value),
235        "bottom" => style.bottom = parse_length_percentage_auto(value),
236        "left" => style.left = parse_length_percentage_auto(value),
237        "overflow" => {
238            let v = parse_overflow(value);
239            style.overflow_x = v;
240            style.overflow_y = v;
241        }
242        "overflow-x" => style.overflow_x = parse_overflow(value),
243        "overflow-y" => style.overflow_y = parse_overflow(value),
244        "text-align" => {
245            style.text_align = match value {
246                "left" => TextAlign::Left,
247                "right" => TextAlign::Right,
248                "center" => TextAlign::Center,
249                "justify" => TextAlign::Justify,
250                _ => TextAlign::Left,
251            };
252        }
253        "line-height" => {
254            style.line_height = parse_px_or_number(value, 1.2);
255        }
256        // Flexbox
257        "flex-direction" => {
258            style.flex_direction = match value {
259                "row" => FlexDirection::Row,
260                "row-reverse" => FlexDirection::RowReverse,
261                "column" => FlexDirection::Column,
262                "column-reverse" => FlexDirection::ColumnReverse,
263                _ => FlexDirection::Row,
264            };
265        }
266        "flex-wrap" => {
267            style.flex_wrap = match value {
268                "nowrap" => FlexWrap::Nowrap,
269                "wrap" => FlexWrap::Wrap,
270                "wrap-reverse" => FlexWrap::WrapReverse,
271                _ => FlexWrap::Nowrap,
272            };
273        }
274        "flex-grow" => {
275            style.flex_grow = value.parse().unwrap_or(0.0);
276        }
277        "flex-shrink" => {
278            style.flex_shrink = value.parse().unwrap_or(1.0);
279        }
280        "flex-basis" => {
281            style.flex_basis = parse_length_percentage_auto(value);
282        }
283        "flex" => {
284            parse_flex_shorthand(style, value);
285        }
286        "align-items" => {
287            style.align_items = parse_align_items(value);
288        }
289        "align-self" => {
290            style.align_self = parse_align_self(value);
291        }
292        "align-content" => {
293            style.align_content = parse_align_content(value);
294        }
295        "justify-content" => {
296            style.justify_content = parse_justify_content(value);
297        }
298        "order" => {
299            style.order = value.parse().unwrap_or(0);
300        }
301        // Grid
302        "grid-template-columns" => {
303            style.grid_template_columns = parse_track_list(value);
304        }
305        "grid-template-rows" => {
306            style.grid_template_rows = parse_track_list(value);
307        }
308        "row-gap" | "grid-row-gap" => {
309            style.row_gap = parse_px(value);
310        }
311        "column-gap" | "grid-column-gap" => {
312            style.column_gap = parse_px(value);
313        }
314        "gap" | "grid-gap" => {
315            let parts: Vec<&str> = value.split_whitespace().collect();
316            style.row_gap = parse_px(parts.first().unwrap_or(&"0"));
317            style.column_gap = parse_px(parts.get(1).unwrap_or(parts.first().unwrap_or(&"0")));
318        }
319        "grid-row-start" => style.grid_row_start = parse_grid_placement(value),
320        "grid-row-end" => style.grid_row_end = parse_grid_placement(value),
321        "grid-column-start" => style.grid_column_start = parse_grid_placement(value),
322        "grid-column-end" => style.grid_column_end = parse_grid_placement(value),
323        "grid-auto-flow" => {
324            style.grid_auto_flow = match value {
325                "row" => GridAutoFlow::Row,
326                "column" => GridAutoFlow::Column,
327                "row dense" => GridAutoFlow::RowDense,
328                "column dense" => GridAutoFlow::ColumnDense,
329                _ => GridAutoFlow::Row,
330            };
331        }
332        // Table
333        "table-layout" => {
334            style.table_layout = match value {
335                "fixed" => TableLayout::Fixed,
336                "auto" => TableLayout::Auto,
337                _ => TableLayout::Auto,
338            };
339        }
340        "border-collapse" => {
341            style.border_collapse = match value {
342                "collapse" => BorderCollapse::Collapse,
343                "separate" => BorderCollapse::Separate,
344                _ => BorderCollapse::Separate,
345            };
346        }
347        "border-spacing" => {
348            style.border_spacing = parse_px(value);
349        }
350        "caption-side" => {
351            style.caption_side = match value {
352                "top" => CaptionSide::Top,
353                "bottom" => CaptionSide::Bottom,
354                _ => CaptionSide::Top,
355            };
356        }
357        _ => {
358            // Unknown property — ignore
359        }
360    }
361}
362
363// --- Value parsers ---
364
365fn parse_px(value: &str) -> f32 {
366    let value = value.trim();
367    if value == "0" {
368        return 0.0;
369    }
370    if let Some(px) = value.strip_suffix("px") {
371        px.trim().parse().unwrap_or(0.0)
372    } else {
373        value.parse().unwrap_or(0.0)
374    }
375}
376
377fn parse_px_or_number(value: &str, default: f32) -> f32 {
378    let value = value.trim();
379    if value == "normal" {
380        return default;
381    }
382    if let Some(px) = value.strip_suffix("px") {
383        px.trim().parse().unwrap_or(default)
384    } else {
385        value.parse().unwrap_or(default)
386    }
387}
388
389fn parse_length_percentage(value: &str) -> LengthPercentage {
390    let value = value.trim();
391    if value == "0" {
392        return LengthPercentage::Length(0.0);
393    }
394    if let Some(pct) = value.strip_suffix('%') {
395        LengthPercentage::Percentage(pct.trim().parse::<f32>().unwrap_or(0.0) / 100.0)
396    } else if let Some(px) = value.strip_suffix("px") {
397        LengthPercentage::Length(px.trim().parse().unwrap_or(0.0))
398    } else {
399        LengthPercentage::Length(value.parse().unwrap_or(0.0))
400    }
401}
402
403fn parse_length_percentage_auto(value: &str) -> LengthPercentageAuto {
404    let value = value.trim();
405    if value == "auto" {
406        return LengthPercentageAuto::Auto;
407    }
408    if value == "0" {
409        return LengthPercentageAuto::Length(0.0);
410    }
411    if let Some(pct) = value.strip_suffix('%') {
412        LengthPercentageAuto::Percentage(pct.trim().parse::<f32>().unwrap_or(0.0) / 100.0)
413    } else if let Some(px) = value.strip_suffix("px") {
414        LengthPercentageAuto::Length(px.trim().parse().unwrap_or(0.0))
415    } else {
416        LengthPercentageAuto::Length(value.parse().unwrap_or(0.0))
417    }
418}
419
420fn parse_length_percentage_none(value: &str) -> LengthPercentageNone {
421    let value = value.trim();
422    if value == "none" {
423        return LengthPercentageNone::None;
424    }
425    if value == "0" {
426        return LengthPercentageNone::Length(0.0);
427    }
428    if let Some(pct) = value.strip_suffix('%') {
429        LengthPercentageNone::Percentage(pct.trim().parse::<f32>().unwrap_or(0.0) / 100.0)
430    } else if let Some(px) = value.strip_suffix("px") {
431        LengthPercentageNone::Length(px.trim().parse().unwrap_or(0.0))
432    } else {
433        LengthPercentageNone::Length(value.parse().unwrap_or(0.0))
434    }
435}
436
437fn parse_display(value: &str) -> Display {
438    match value.trim() {
439        "block" => Display::BLOCK,
440        "inline" => Display::INLINE,
441        "inline-block" => Display::INLINE_BLOCK,
442        "flex" => Display::FLEX,
443        "inline-flex" => Display::INLINE_FLEX,
444        "grid" => Display::GRID,
445        "inline-grid" => Display::INLINE_GRID,
446        "table" => Display::TABLE,
447        "table-row" => Display::TABLE_ROW,
448        "table-cell" => Display::TABLE_CELL,
449        "table-row-group" => Display::TABLE_ROW_GROUP,
450        "table-column" => Display::TABLE_COLUMN,
451        "table-column-group" => Display::TABLE_COLUMN_GROUP,
452        "table-caption" => Display::TABLE_CAPTION,
453        "table-header-group" => Display::TABLE_HEADER_GROUP,
454        "table-footer-group" => Display::TABLE_FOOTER_GROUP,
455        "flow-root" => Display::FLOW_ROOT,
456        "none" => Display::NONE,
457        _ => Display::INLINE,
458    }
459}
460
461fn parse_overflow(value: &str) -> Overflow {
462    match value.trim() {
463        "visible" => Overflow::Visible,
464        "hidden" => Overflow::Hidden,
465        "scroll" => Overflow::Scroll,
466        "auto" => Overflow::Auto,
467        _ => Overflow::Visible,
468    }
469}
470
471fn parse_shorthand_edges(
472    value: &str,
473) -> (
474    LengthPercentageAuto,
475    LengthPercentageAuto,
476    LengthPercentageAuto,
477    LengthPercentageAuto,
478) {
479    let parts: Vec<&str> = value.split_whitespace().collect();
480    match parts.len() {
481        1 => {
482            let v = parse_length_percentage_auto(parts[0]);
483            (v, v, v, v)
484        }
485        2 => {
486            let vert = parse_length_percentage_auto(parts[0]);
487            let horiz = parse_length_percentage_auto(parts[1]);
488            (vert, horiz, vert, horiz)
489        }
490        3 => {
491            let top = parse_length_percentage_auto(parts[0]);
492            let horiz = parse_length_percentage_auto(parts[1]);
493            let bottom = parse_length_percentage_auto(parts[2]);
494            (top, horiz, bottom, horiz)
495        }
496        4 => (
497            parse_length_percentage_auto(parts[0]),
498            parse_length_percentage_auto(parts[1]),
499            parse_length_percentage_auto(parts[2]),
500            parse_length_percentage_auto(parts[3]),
501        ),
502        _ => (
503            LengthPercentageAuto::px(0.0),
504            LengthPercentageAuto::px(0.0),
505            LengthPercentageAuto::px(0.0),
506            LengthPercentageAuto::px(0.0),
507        ),
508    }
509}
510
511fn parse_shorthand_lp_edges(
512    value: &str,
513) -> (
514    LengthPercentage,
515    LengthPercentage,
516    LengthPercentage,
517    LengthPercentage,
518) {
519    let parts: Vec<&str> = value.split_whitespace().collect();
520    match parts.len() {
521        1 => {
522            let v = parse_length_percentage(parts[0]);
523            (v, v, v, v)
524        }
525        2 => {
526            let vert = parse_length_percentage(parts[0]);
527            let horiz = parse_length_percentage(parts[1]);
528            (vert, horiz, vert, horiz)
529        }
530        3 => {
531            let top = parse_length_percentage(parts[0]);
532            let horiz = parse_length_percentage(parts[1]);
533            let bottom = parse_length_percentage(parts[2]);
534            (top, horiz, bottom, horiz)
535        }
536        4 => (
537            parse_length_percentage(parts[0]),
538            parse_length_percentage(parts[1]),
539            parse_length_percentage(parts[2]),
540            parse_length_percentage(parts[3]),
541        ),
542        _ => (
543            LengthPercentage::Length(0.0),
544            LengthPercentage::Length(0.0),
545            LengthPercentage::Length(0.0),
546            LengthPercentage::Length(0.0),
547        ),
548    }
549}
550
551fn parse_flex_shorthand(style: &mut ComputedStyle, value: &str) {
552    let parts: Vec<&str> = value.split_whitespace().collect();
553    match parts.len() {
554        1 => {
555            if parts[0] == "none" {
556                style.flex_grow = 0.0;
557                style.flex_shrink = 0.0;
558                style.flex_basis = LengthPercentageAuto::Auto;
559            } else if parts[0] == "auto" {
560                style.flex_grow = 1.0;
561                style.flex_shrink = 1.0;
562                style.flex_basis = LengthPercentageAuto::Auto;
563            } else if let Ok(grow) = parts[0].parse::<f32>() {
564                style.flex_grow = grow;
565                style.flex_shrink = 1.0;
566                style.flex_basis = LengthPercentageAuto::px(0.0);
567            }
568        }
569        2 => {
570            style.flex_grow = parts[0].parse().unwrap_or(0.0);
571            if let Ok(shrink) = parts[1].parse::<f32>() {
572                style.flex_shrink = shrink;
573                style.flex_basis = LengthPercentageAuto::px(0.0);
574            } else {
575                style.flex_shrink = 1.0;
576                style.flex_basis = parse_length_percentage_auto(parts[1]);
577            }
578        }
579        3 => {
580            style.flex_grow = parts[0].parse().unwrap_or(0.0);
581            style.flex_shrink = parts[1].parse().unwrap_or(1.0);
582            style.flex_basis = parse_length_percentage_auto(parts[2]);
583        }
584        _ => {}
585    }
586}
587
588fn parse_align_items(value: &str) -> AlignItems {
589    match value.trim() {
590        "stretch" => AlignItems::Stretch,
591        "flex-start" | "start" => AlignItems::FlexStart,
592        "flex-end" | "end" => AlignItems::FlexEnd,
593        "center" => AlignItems::Center,
594        "baseline" => AlignItems::Baseline,
595        _ => AlignItems::Stretch,
596    }
597}
598
599fn parse_align_self(value: &str) -> AlignSelf {
600    match value.trim() {
601        "auto" => AlignSelf::Auto,
602        "stretch" => AlignSelf::Stretch,
603        "flex-start" | "start" => AlignSelf::FlexStart,
604        "flex-end" | "end" => AlignSelf::FlexEnd,
605        "center" => AlignSelf::Center,
606        "baseline" => AlignSelf::Baseline,
607        _ => AlignSelf::Auto,
608    }
609}
610
611fn parse_align_content(value: &str) -> AlignContent {
612    match value.trim() {
613        "stretch" => AlignContent::Stretch,
614        "flex-start" | "start" => AlignContent::FlexStart,
615        "flex-end" | "end" => AlignContent::FlexEnd,
616        "center" => AlignContent::Center,
617        "space-between" => AlignContent::SpaceBetween,
618        "space-around" => AlignContent::SpaceAround,
619        "space-evenly" => AlignContent::SpaceEvenly,
620        _ => AlignContent::Stretch,
621    }
622}
623
624fn parse_justify_content(value: &str) -> JustifyContent {
625    match value.trim() {
626        "flex-start" | "start" => JustifyContent::FlexStart,
627        "flex-end" | "end" => JustifyContent::FlexEnd,
628        "center" => JustifyContent::Center,
629        "space-between" => JustifyContent::SpaceBetween,
630        "space-around" => JustifyContent::SpaceAround,
631        "space-evenly" => JustifyContent::SpaceEvenly,
632        _ => JustifyContent::FlexStart,
633    }
634}
635
636fn parse_track_list(value: &str) -> Vec<TrackDefinition> {
637    let mut tracks = Vec::new();
638
639    // Simple tokenizer for track values
640    for token in split_track_tokens(value) {
641        let token = token.trim();
642        if token.is_empty() {
643            continue;
644        }
645        tracks.push(TrackDefinition::new(parse_track_sizing(token)));
646    }
647
648    tracks
649}
650
651fn split_track_tokens(value: &str) -> Vec<String> {
652    let mut tokens = Vec::new();
653    let mut current = String::new();
654    let mut paren_depth = 0;
655
656    for ch in value.chars() {
657        match ch {
658            '(' => {
659                paren_depth += 1;
660                current.push(ch);
661            }
662            ')' => {
663                paren_depth -= 1;
664                current.push(ch);
665            }
666            ' ' if paren_depth == 0 => {
667                if !current.is_empty() {
668                    tokens.push(current.clone());
669                    current.clear();
670                }
671            }
672            _ => {
673                current.push(ch);
674            }
675        }
676    }
677    if !current.is_empty() {
678        tokens.push(current);
679    }
680
681    tokens
682}
683
684fn parse_track_sizing(token: &str) -> TrackSizingFunction {
685    if let Some(fr) = token.strip_suffix("fr") {
686        TrackSizingFunction::Fr(fr.parse().unwrap_or(1.0))
687    } else if let Some(pct) = token.strip_suffix('%') {
688        TrackSizingFunction::Percentage(pct.parse::<f32>().unwrap_or(0.0) / 100.0)
689    } else if let Some(px) = token.strip_suffix("px") {
690        TrackSizingFunction::Length(px.parse().unwrap_or(0.0))
691    } else if token == "auto" {
692        TrackSizingFunction::Auto
693    } else if token == "min-content" {
694        TrackSizingFunction::MinContent
695    } else if token == "max-content" {
696        TrackSizingFunction::MaxContent
697    } else if token.starts_with("minmax(") {
698        parse_minmax(token)
699    } else if token.starts_with("fit-content(") {
700        let inner = &token["fit-content(".len()..token.len() - 1];
701        TrackSizingFunction::FitContent(parse_px(inner))
702    } else {
703        TrackSizingFunction::Length(token.parse().unwrap_or(0.0))
704    }
705}
706
707fn parse_minmax(token: &str) -> TrackSizingFunction {
708    let inner = &token["minmax(".len()..token.len() - 1];
709    if let Some((min, max)) = inner.split_once(',') {
710        TrackSizingFunction::MinMax(
711            Box::new(parse_track_sizing(min.trim())),
712            Box::new(parse_track_sizing(max.trim())),
713        )
714    } else {
715        TrackSizingFunction::Auto
716    }
717}
718
719fn parse_grid_placement(value: &str) -> GridPlacement {
720    let value = value.trim();
721    if value == "auto" {
722        return GridPlacement::Auto;
723    }
724    if let Some(span_val) = value.strip_prefix("span ") {
725        if let Ok(n) = span_val.trim().parse::<u32>() {
726            return GridPlacement::Span(n);
727        }
728    }
729    if let Ok(n) = value.parse::<i32>() {
730        return GridPlacement::Line(n);
731    }
732    GridPlacement::Named(value.to_string())
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn test_parse_style_attribute() {
741        let decls = parse_style_attribute("width: 100px; height: 50px; margin: 10px auto");
742        assert_eq!(decls.len(), 3);
743        assert_eq!(decls[0].property, "width");
744        assert_eq!(decls[0].value, "100px");
745    }
746
747    #[test]
748    fn test_apply_display() {
749        let mut style = ComputedStyle::default();
750        apply_property(&mut style, "display", "flex");
751        assert_eq!(style.display, Display::FLEX);
752    }
753
754    #[test]
755    fn test_apply_margin_shorthand() {
756        let mut style = ComputedStyle::default();
757        apply_property(&mut style, "margin", "10px 20px");
758        assert_eq!(style.margin_top, LengthPercentageAuto::px(10.0));
759        assert_eq!(style.margin_right, LengthPercentageAuto::px(20.0));
760        assert_eq!(style.margin_bottom, LengthPercentageAuto::px(10.0));
761        assert_eq!(style.margin_left, LengthPercentageAuto::px(20.0));
762    }
763
764    #[test]
765    fn test_parse_percentage() {
766        let lpa = parse_length_percentage_auto("50%");
767        assert_eq!(lpa, LengthPercentageAuto::Percentage(0.5));
768    }
769
770    #[test]
771    fn test_parse_stylesheet() {
772        let css = "div { width: 100px; height: 50px; } .box { display: flex; }";
773        let rules = parse_stylesheet(css);
774        assert_eq!(rules.len(), 2);
775        assert_eq!(rules[0].selector, "div");
776        assert_eq!(rules[0].declarations.len(), 2);
777        assert_eq!(rules[1].selector, ".box");
778    }
779
780    #[test]
781    fn test_parse_track_list() {
782        let tracks = parse_track_list("1fr 200px auto");
783        assert_eq!(tracks.len(), 3);
784        assert!(matches!(tracks[0].sizing, TrackSizingFunction::Fr(f) if (f - 1.0).abs() < 0.001));
785        assert!(
786            matches!(tracks[1].sizing, TrackSizingFunction::Length(v) if (v - 200.0).abs() < 0.001)
787        );
788        assert!(matches!(tracks[2].sizing, TrackSizingFunction::Auto));
789    }
790}