Skip to main content

foundry_runtime/
css.rs

1use crate::scene::*;
2
3pub fn parse_color(s: &str) -> Option<Color> {
4    let s = s.trim();
5
6    // Named colors
7    match s.to_lowercase().as_str() {
8        "transparent" => return Some(Color::TRANSPARENT),
9        "black" => return Some(Color::BLACK),
10        "white" => return Some(Color::WHITE),
11        "red" => return Some(Color::from_rgba(255, 0, 0, 1.0)),
12        "green" => return Some(Color::from_rgba(0, 128, 0, 1.0)),
13        "blue" => return Some(Color::from_rgba(0, 0, 255, 1.0)),
14        "yellow" => return Some(Color::from_rgba(255, 255, 0, 1.0)),
15        "orange" => return Some(Color::from_rgba(255, 165, 0, 1.0)),
16        "purple" => return Some(Color::from_rgba(128, 0, 128, 1.0)),
17        "pink" => return Some(Color::from_rgba(255, 192, 203, 1.0)),
18        "gray" | "grey" => return Some(Color::from_rgba(128, 128, 128, 1.0)),
19        "lightgray" | "lightgrey" => return Some(Color::from_rgba(211, 211, 211, 1.0)),
20        "darkgray" | "darkgrey" => return Some(Color::from_rgba(169, 169, 169, 1.0)),
21        "cyan" => return Some(Color::from_rgba(0, 255, 255, 1.0)),
22        "magenta" => return Some(Color::from_rgba(255, 0, 255, 1.0)),
23        "brown" => return Some(Color::from_rgba(165, 42, 42, 1.0)),
24        "navy" => return Some(Color::from_rgba(0, 0, 128, 1.0)),
25        "teal" => return Some(Color::from_rgba(0, 128, 128, 1.0)),
26        "coral" => return Some(Color::from_rgba(255, 127, 80, 1.0)),
27        "salmon" => return Some(Color::from_rgba(250, 128, 114, 1.0)),
28        "gold" => return Some(Color::from_rgba(255, 215, 0, 1.0)),
29        "silver" => return Some(Color::from_rgba(192, 192, 192, 1.0)),
30        "olive" => return Some(Color::from_rgba(128, 128, 0, 1.0)),
31        "maroon" => return Some(Color::from_rgba(128, 0, 0, 1.0)),
32        "lime" => return Some(Color::from_rgba(0, 255, 0, 1.0)),
33        "aqua" => return Some(Color::from_rgba(0, 255, 255, 1.0)),
34        "indigo" => return Some(Color::from_rgba(75, 0, 130, 1.0)),
35        "violet" => return Some(Color::from_rgba(238, 130, 238, 1.0)),
36        "tomato" => return Some(Color::from_rgba(255, 99, 71, 1.0)),
37        "skyblue" => return Some(Color::from_rgba(135, 206, 235, 1.0)),
38        "steelblue" => return Some(Color::from_rgba(70, 130, 180, 1.0)),
39        "slategray" | "slategrey" => return Some(Color::from_rgba(112, 128, 144, 1.0)),
40        "whitesmoke" => return Some(Color::from_rgba(245, 245, 245, 1.0)),
41        "aliceblue" => return Some(Color::from_rgba(240, 248, 255, 1.0)),
42        "dodgerblue" => return Some(Color::from_rgba(30, 144, 255, 1.0)),
43        "crimson" => return Some(Color::from_rgba(220, 20, 60, 1.0)),
44        "darkblue" => return Some(Color::from_rgba(0, 0, 139, 1.0)),
45        "darkgreen" => return Some(Color::from_rgba(0, 100, 0, 1.0)),
46        "darkred" => return Some(Color::from_rgba(139, 0, 0, 1.0)),
47        _ => {}
48    }
49
50    // #RGB, #RRGGBB, #RRGGBBAA
51    if let Some(hex) = s.strip_prefix('#') {
52        match hex.len() {
53            3 => {
54                let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
55                let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
56                let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
57                return Some(Color::from_rgba(r, g, b, 1.0));
58            }
59            6 => {
60                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
61                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
62                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
63                return Some(Color::from_rgba(r, g, b, 1.0));
64            }
65            8 => {
66                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
67                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
68                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
69                let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
70                return Some(Color::from_rgba(r, g, b, a as f32 / 255.0));
71            }
72            _ => return None,
73        }
74    }
75
76    // rgb(r, g, b) and rgba(r, g, b, a)
77    if s.starts_with("rgb") {
78        let inner = s
79            .trim_start_matches("rgba(")
80            .trim_start_matches("rgb(")
81            .trim_end_matches(')');
82        let parts: Vec<&str> = inner.split(',').collect();
83        if parts.len() >= 3 {
84            let r = parts[0].trim().parse::<u8>().ok()?;
85            let g = parts[1].trim().parse::<u8>().ok()?;
86            let b = parts[2].trim().parse::<u8>().ok()?;
87            let a = if parts.len() == 4 {
88                parts[3].trim().parse::<f32>().unwrap_or(1.0)
89            } else {
90                1.0
91            };
92            return Some(Color::from_rgba(r, g, b, a));
93        }
94    }
95
96    None
97}
98
99pub fn parse_length(s: &str) -> Option<SizeValue> {
100    let s = s.trim();
101    if s == "auto" {
102        return Some(SizeValue::Auto);
103    }
104    if s == "0" {
105        return Some(SizeValue::Px(0.0));
106    }
107    if let Some(v) = s.strip_suffix("px") {
108        return v.trim().parse::<f32>().ok().map(SizeValue::Px);
109    }
110    if let Some(v) = s.strip_suffix('%') {
111        return v.trim().parse::<f32>().ok().map(SizeValue::Percent);
112    }
113    if let Some(v) = s.strip_suffix("rem") {
114        return v.trim().parse::<f32>().ok().map(SizeValue::Rem);
115    }
116    if let Some(v) = s.strip_suffix("em") {
117        return v.trim().parse::<f32>().ok().map(SizeValue::Em);
118    }
119    if let Some(v) = s.strip_suffix("vh") {
120        return v.trim().parse::<f32>().ok().map(SizeValue::Vh);
121    }
122    if let Some(v) = s.strip_suffix("vw") {
123        return v.trim().parse::<f32>().ok().map(SizeValue::Vw);
124    }
125    // Plain number -> px
126    s.parse::<f32>().ok().map(SizeValue::Px)
127}
128
129pub fn apply_property(style: &mut ResolvedStyle, property: &str, value: &str) {
130    let prop = property.trim().to_lowercase();
131    let val = value.trim();
132
133    match prop.as_str() {
134        // Display
135        "display" => {
136            style.display = match val {
137                "flex" => Display::Flex,
138                "block" => Display::Block,
139                "inline" => Display::Inline,
140                "none" => Display::None,
141                _ => style.display,
142            };
143        }
144
145        // Flexbox
146        "flex-direction" => {
147            style.flex_direction = match val {
148                "row" => FlexDirection::Row,
149                "column" => FlexDirection::Column,
150                "row-reverse" => FlexDirection::RowReverse,
151                "column-reverse" => FlexDirection::ColumnReverse,
152                _ => style.flex_direction,
153            };
154        }
155        "justify-content" => {
156            style.justify_content = match val {
157                "flex-start" | "start" => JustifyContent::Start,
158                "center" => JustifyContent::Center,
159                "flex-end" | "end" => JustifyContent::End,
160                "space-between" => JustifyContent::SpaceBetween,
161                "space-around" => JustifyContent::SpaceAround,
162                "space-evenly" => JustifyContent::SpaceEvenly,
163                _ => style.justify_content,
164            };
165        }
166        "align-items" => {
167            style.align_items = match val {
168                "stretch" => AlignItems::Stretch,
169                "flex-start" | "start" => AlignItems::Start,
170                "center" => AlignItems::Center,
171                "flex-end" | "end" => AlignItems::End,
172                _ => style.align_items,
173            };
174        }
175        "flex-wrap" => {
176            style.flex_wrap = match val {
177                "wrap" => FlexWrap::Wrap,
178                "nowrap" => FlexWrap::NoWrap,
179                _ => style.flex_wrap,
180            };
181        }
182        "flex-grow" => {
183            if let Ok(v) = val.parse::<f32>() {
184                style.flex_grow = v;
185            }
186        }
187        "flex-shrink" => {
188            if let Ok(v) = val.parse::<f32>() {
189                style.flex_shrink = v;
190            }
191        }
192        "gap" => {
193            if let Some(SizeValue::Px(v)) = parse_length(val) {
194                style.gap = v;
195            }
196        }
197
198        // Sizing
199        "width" => {
200            if let Some(v) = parse_length(val) {
201                style.width = v;
202            }
203        }
204        "height" => {
205            if let Some(v) = parse_length(val) {
206                style.height = v;
207            }
208        }
209        "min-width" => {
210            if let Some(v) = parse_length(val) {
211                style.min_width = v;
212            }
213        }
214        "min-height" => {
215            if let Some(v) = parse_length(val) {
216                style.min_height = v;
217            }
218        }
219        "max-width" => {
220            if let Some(v) = parse_length(val) {
221                style.max_width = v;
222            }
223        }
224        "max-height" => {
225            if let Some(v) = parse_length(val) {
226                style.max_height = v;
227            }
228        }
229
230        // Margin
231        "margin" => {
232            let parts = parse_box_shorthand(val);
233            style.margin = parts;
234        }
235        "margin-top" => {
236            if let Some(SizeValue::Px(v)) = parse_length(val) {
237                style.margin[0] = v;
238            }
239        }
240        "margin-right" => {
241            if let Some(SizeValue::Px(v)) = parse_length(val) {
242                style.margin[1] = v;
243            }
244        }
245        "margin-bottom" => {
246            if let Some(SizeValue::Px(v)) = parse_length(val) {
247                style.margin[2] = v;
248            }
249        }
250        "margin-left" => {
251            if let Some(SizeValue::Px(v)) = parse_length(val) {
252                style.margin[3] = v;
253            }
254        }
255
256        // Padding
257        "padding" => {
258            let parts = parse_box_shorthand(val);
259            style.padding = parts;
260        }
261        "padding-top" => {
262            if let Some(SizeValue::Px(v)) = parse_length(val) {
263                style.padding[0] = v;
264            }
265        }
266        "padding-right" => {
267            if let Some(SizeValue::Px(v)) = parse_length(val) {
268                style.padding[1] = v;
269            }
270        }
271        "padding-bottom" => {
272            if let Some(SizeValue::Px(v)) = parse_length(val) {
273                style.padding[2] = v;
274            }
275        }
276        "padding-left" => {
277            if let Some(SizeValue::Px(v)) = parse_length(val) {
278                style.padding[3] = v;
279            }
280        }
281
282        // Border
283        "border" => {
284            // border: 1px solid #000
285            let parts: Vec<&str> = val.split_whitespace().collect();
286            if let Some(first) = parts.first() {
287                if let Some(SizeValue::Px(w)) = parse_length(first) {
288                    style.border_width = [w; 4];
289                }
290            }
291            if let Some(color_str) = parts.last() {
292                if let Some(c) = parse_color(color_str) {
293                    style.border_color = c;
294                }
295            }
296        }
297        "border-width" => {
298            if let Some(SizeValue::Px(v)) = parse_length(val) {
299                style.border_width = [v; 4];
300            }
301        }
302        "border-color" => {
303            if let Some(c) = parse_color(val) {
304                style.border_color = c;
305            }
306        }
307        "border-radius" => {
308            let parts: Vec<&str> = val.split_whitespace().collect();
309            match parts.len() {
310                1 => {
311                    if let Some(SizeValue::Px(v)) = parse_length(parts[0]) {
312                        style.border_radius = [v; 4];
313                    }
314                }
315                4 => {
316                    for (i, p) in parts.iter().enumerate() {
317                        if let Some(SizeValue::Px(v)) = parse_length(p) {
318                            style.border_radius[i] = v;
319                        }
320                    }
321                }
322                _ => {}
323            }
324        }
325
326        // Position
327        "position" => {
328            style.position = match val {
329                "relative" => Position::Relative,
330                "absolute" => Position::Absolute,
331                "fixed" => Position::Fixed,
332                _ => style.position,
333            };
334        }
335        "top" => {
336            if let Some(v) = parse_length(val) {
337                style.top = v;
338            }
339        }
340        "left" => {
341            if let Some(v) = parse_length(val) {
342                style.left = v;
343            }
344        }
345        "right" => {
346            if let Some(v) = parse_length(val) {
347                style.right = v;
348            }
349        }
350        "bottom" => {
351            if let Some(v) = parse_length(val) {
352                style.bottom = v;
353            }
354        }
355
356        // Visual
357        "background-color" | "background" => {
358            if let Some(c) = parse_color(val) {
359                style.background_color = c;
360            }
361        }
362        "color" => {
363            if let Some(c) = parse_color(val) {
364                style.color = c;
365            }
366        }
367        "opacity" => {
368            if let Ok(v) = val.parse::<f32>() {
369                style.opacity = v;
370            }
371        }
372
373        // Text
374        "font-size" => {
375            if let Some(SizeValue::Px(v)) = parse_length(val) {
376                style.font_size = v;
377            }
378        }
379        "font-weight" => {
380            style.font_weight = match val {
381                "bold" => 700,
382                "normal" => 400,
383                "lighter" => 300,
384                "bolder" => 800,
385                _ => val.parse::<u16>().unwrap_or(400),
386            };
387        }
388        "font-family" => {
389            style.font_family = val.trim_matches(|c| c == '\'' || c == '"').to_string();
390        }
391        "text-align" => {
392            style.text_align = match val {
393                "left" => TextAlign::Left,
394                "center" => TextAlign::Center,
395                "right" => TextAlign::Right,
396                _ => style.text_align,
397            };
398        }
399        "line-height" => {
400            if let Ok(v) = val.parse::<f32>() {
401                style.line_height = v;
402            } else if let Some(SizeValue::Px(v)) = parse_length(val) {
403                style.line_height = v / style.font_size;
404            }
405        }
406
407        // Overflow
408        "overflow" => {
409            style.overflow = match val {
410                "hidden" => Overflow::Hidden,
411                "scroll" | "auto" => Overflow::Scroll,
412                _ => Overflow::Visible,
413            };
414        }
415
416        // Z-index
417        "z-index" => {
418            if let Ok(v) = val.parse::<i32>() {
419                style.z_index = v;
420            }
421        }
422
423        // Transitions
424        "transition" => {
425            // transition: all 0.3s, transition: background-color 0.2s
426            let parts: Vec<&str> = val.split_whitespace().collect();
427            if parts.len() >= 2 {
428                style.transition_property = match parts[0] {
429                    "all" => crate::scene::TransitionProperty::All,
430                    _ => crate::scene::TransitionProperty::All, // treat any property as "all" for v0.1
431                };
432                // Parse duration
433                let dur = parts[1];
434                if let Some(s) = dur.strip_suffix('s') {
435                    if let Some(ms) = s.strip_suffix('m') {
436                        style.transition_duration = ms.parse::<f32>().unwrap_or(0.0) / 1000.0;
437                    } else {
438                        style.transition_duration = s.parse::<f32>().unwrap_or(0.0);
439                    }
440                }
441            }
442        }
443        "transition-duration" => {
444            let dur = val.trim();
445            if let Some(s) = dur.strip_suffix('s') {
446                if let Some(ms) = s.strip_suffix('m') {
447                    style.transition_duration = ms.parse::<f32>().unwrap_or(0.0) / 1000.0;
448                } else {
449                    style.transition_duration = s.parse::<f32>().unwrap_or(0.0);
450                }
451            }
452        }
453
454        // Animation
455        "animation" => {
456            // animation: name duration [delay] [iteration-count] [direction]
457            // e.g. "fadeIn 0.5s", "pulse 2s infinite", "slide 1s 0.2s infinite alternate"
458            let parts: Vec<&str> = val.split_whitespace().collect();
459            if !parts.is_empty() {
460                style.animation_name = parts[0].to_string();
461            }
462            if parts.len() >= 2 {
463                style.animation_duration = parse_time(parts[1]);
464            }
465            // Check remaining parts for delay, iteration, direction
466            for &part in parts.iter().skip(2) {
467                if part == "infinite" {
468                    style.animation_iteration_count = f32::INFINITY;
469                } else if part == "alternate" {
470                    style.animation_direction = crate::scene::AnimationDirection::Alternate;
471                } else if part == "reverse" {
472                    style.animation_direction = crate::scene::AnimationDirection::Reverse;
473                } else if part.ends_with('s') {
474                    // Could be delay
475                    let t = parse_time(part);
476                    if t > 0.0 {
477                        style.animation_delay = t;
478                    }
479                } else if let Ok(n) = part.parse::<f32>() {
480                    style.animation_iteration_count = n;
481                }
482            }
483        }
484        "animation-name" => {
485            style.animation_name = val.trim().to_string();
486        }
487        "animation-duration" => {
488            style.animation_duration = parse_time(val.trim());
489        }
490        "animation-delay" => {
491            style.animation_delay = parse_time(val.trim());
492        }
493        "animation-iteration-count" => {
494            if val.trim() == "infinite" {
495                style.animation_iteration_count = f32::INFINITY;
496            } else if let Ok(n) = val.trim().parse::<f32>() {
497                style.animation_iteration_count = n;
498            }
499        }
500
501        _ => {} // Unsupported property - ignore
502    }
503}
504
505fn parse_time(s: &str) -> f32 {
506    if let Some(ms) = s.strip_suffix("ms") {
507        ms.parse::<f32>().unwrap_or(0.0) / 1000.0
508    } else if let Some(secs) = s.strip_suffix('s') {
509        secs.parse::<f32>().unwrap_or(0.0)
510    } else {
511        0.0
512    }
513}
514
515fn parse_box_shorthand(val: &str) -> [f32; 4] {
516    let parts: Vec<f32> = val
517        .split_whitespace()
518        .map(|p| {
519            if let Some(SizeValue::Px(v)) = parse_length(p) {
520                v
521            } else {
522                0.0
523            }
524        })
525        .collect();
526
527    match parts.len() {
528        1 => [parts[0]; 4],
529        2 => [parts[0], parts[1], parts[0], parts[1]],
530        3 => [parts[0], parts[1], parts[2], parts[1]],
531        4 => [parts[0], parts[1], parts[2], parts[3]],
532        _ => [0.0; 4],
533    }
534}
535
536pub fn parse_inline_style(style_str: &str, target: &mut ResolvedStyle) {
537    for declaration in style_str.split(';') {
538        let declaration = declaration.trim();
539        if declaration.is_empty() {
540            continue;
541        }
542        if let Some((prop, val)) = declaration.split_once(':') {
543            apply_property(target, prop, val);
544        }
545    }
546}
547
548// CSS rule: selector + declarations
549#[derive(Debug, Clone)]
550pub struct CssRule {
551    pub selector: Selector,
552    pub declarations: Vec<(String, String)>,
553    pub specificity: u32,
554}
555
556/// A single selector atom (e.g., "div", ".card", "#main")
557#[derive(Debug, Clone)]
558pub enum SelectorPart {
559    Tag(String),
560    Class(String),
561    Id(String),
562    Universal,
563}
564
565/// A segment is one space-separated part of a selector.
566/// e.g., "div.btn.active" is ONE segment with parts [Tag("div"), Class("btn"), Class("active")]
567/// All parts in a segment must match the SAME element.
568#[derive(Debug, Clone)]
569pub struct SelectorSegment {
570    pub parts: Vec<SelectorPart>,
571}
572
573impl SelectorSegment {
574    pub fn matches_node(&self, node: &crate::scene::SceneNode) -> bool {
575        self.parts.iter().all(|part| match part {
576            SelectorPart::Tag(tag) => node.tag == *tag,
577            SelectorPart::Class(class) => node.classes.contains(class),
578            SelectorPart::Id(id) => node.element_id.as_deref() == Some(id.as_str()),
579            SelectorPart::Universal => true,
580        })
581    }
582}
583
584/// A full selector: chain of segments (ancestor > ... > target) + optional :hover
585/// e.g. ".card h1:hover" = segments=[Segment([Class("card")]), Segment([Tag("h1")])], hover=true
586#[derive(Debug, Clone)]
587pub struct Selector {
588    pub segments: Vec<SelectorSegment>,
589    pub hover: bool,
590}
591
592impl Selector {
593    pub fn is_hover(&self) -> bool {
594        self.hover
595    }
596
597    pub fn specificity(&self) -> u32 {
598        let mut s: u32 = 0;
599        for seg in &self.segments {
600            for part in &seg.parts {
601                s += match part {
602                    SelectorPart::Id(_) => 100,
603                    SelectorPart::Class(_) => 10,
604                    SelectorPart::Tag(_) => 1,
605                    SelectorPart::Universal => 0,
606                };
607            }
608        }
609        if self.hover {
610            s += 10;
611        }
612        s
613    }
614
615    /// Target-only match, ignores ancestor chain.
616    pub fn matches(&self, node: &crate::scene::SceneNode) -> bool {
617        if self.segments.is_empty() {
618            return false;
619        }
620        self.segments.last().unwrap().matches_node(node)
621    }
622
623    /// Full match: target segment + ancestor chain
624    pub fn matches_with_ancestors(
625        &self,
626        node: &crate::scene::SceneNode,
627        scene: &crate::scene::SceneGraph,
628    ) -> bool {
629        if self.segments.is_empty() {
630            return false;
631        }
632        // Last segment must match target node
633        if !self.segments.last().unwrap().matches_node(node) {
634            return false;
635        }
636        if self.segments.len() == 1 {
637            return true;
638        }
639        // Walk up ancestors to match remaining segments
640        let ancestor_segs = &self.segments[..self.segments.len() - 1];
641        let mut seg_idx = ancestor_segs.len();
642        let mut current = node.parent;
643        while seg_idx > 0 {
644            if let Some(parent_id) = current {
645                let parent = scene.get(parent_id);
646                if ancestor_segs[seg_idx - 1].matches_node(parent) {
647                    seg_idx -= 1;
648                }
649                current = parent.parent;
650            } else {
651                return false;
652            }
653        }
654        true
655    }
656}
657
658pub fn parse_stylesheet(css: &str) -> Vec<CssRule> {
659    let (rules, _) = parse_stylesheet_with_keyframes(css);
660    rules
661}
662
663pub fn parse_stylesheet_with_keyframes(
664    css: &str,
665) -> (Vec<CssRule>, Vec<crate::scene::KeyframeAnimation>) {
666    let mut rules = Vec::new();
667    let mut keyframes = Vec::new();
668    let mut pos = 0;
669    let bytes = css.as_bytes();
670
671    while pos < bytes.len() {
672        // Skip whitespace and comments
673        while pos < bytes.len() && (bytes[pos] as char).is_whitespace() {
674            pos += 1;
675        }
676        if pos >= bytes.len() {
677            break;
678        }
679
680        // Skip comments
681        if pos + 1 < bytes.len() && bytes[pos] == b'/' && bytes[pos + 1] == b'*' {
682            if let Some(end) = css[pos..].find("*/") {
683                pos += end + 2;
684                continue;
685            }
686            break;
687        }
688
689        // Find selector (everything before '{')
690        let selector_start = pos;
691        while pos < bytes.len() && bytes[pos] != b'{' {
692            pos += 1;
693        }
694        if pos >= bytes.len() {
695            break;
696        }
697        let selector_str = css[selector_start..pos].trim();
698        pos += 1; // skip '{'
699
700        // Handle @keyframes blocks
701        if selector_str.starts_with("@keyframes") {
702            let anim_name = selector_str
703                .strip_prefix("@keyframes")
704                .unwrap()
705                .trim()
706                .to_string();
707            // Find the end of the @keyframes block (nested braces)
708            let kf_start = pos;
709            let mut brace_depth = 1;
710            while pos < bytes.len() && brace_depth > 0 {
711                if bytes[pos] == b'{' {
712                    brace_depth += 1;
713                } else if bytes[pos] == b'}' {
714                    brace_depth -= 1;
715                }
716                pos += 1;
717            }
718            let kf_body = &css[kf_start..pos - 1];
719            let kf_frames = parse_keyframes(kf_body);
720            keyframes.push(crate::scene::KeyframeAnimation {
721                name: anim_name,
722                keyframes: kf_frames,
723            });
724            continue;
725        }
726
727        // Find declarations (everything before '}')
728        let decl_start = pos;
729        let mut brace_depth = 1;
730        while pos < bytes.len() && brace_depth > 0 {
731            if bytes[pos] == b'{' {
732                brace_depth += 1;
733            } else if bytes[pos] == b'}' {
734                brace_depth -= 1;
735            }
736            pos += 1;
737        }
738        let decl_str = &css[decl_start..pos - 1];
739
740        // Parse selector: each space-separated token is a segment (ancestor chain)
741        // Each token can be compound: "div.btn.active" = one segment with multiple parts
742        let selectors: Vec<Selector> = selector_str
743            .split(',')
744            .filter_map(|s| {
745                let s = s.trim();
746                if s.is_empty() {
747                    return None;
748                }
749                let mut segments = Vec::new();
750                let mut hover = false;
751
752                for token in s.split_whitespace() {
753                    let (base, is_hover) = if let Some(stripped) = token.strip_suffix(":hover") {
754                        (stripped, true)
755                    } else {
756                        (token, false)
757                    };
758                    if is_hover {
759                        hover = true;
760                    }
761
762                    // Parse one token into a segment (may have multiple parts)
763                    let mut parts = Vec::new();
764                    parse_selector_token(base, &mut parts);
765
766                    if !parts.is_empty() {
767                        segments.push(SelectorSegment { parts });
768                    }
769                }
770
771                if segments.is_empty() {
772                    return None;
773                }
774                Some(Selector { segments, hover })
775            })
776            .collect();
777
778        // Parse declarations
779        let declarations: Vec<(String, String)> = decl_str
780            .split(';')
781            .filter_map(|d| {
782                let d = d.trim();
783                if d.is_empty() {
784                    return None;
785                }
786                let (prop, val) = d.split_once(':')?;
787                Some((prop.trim().to_string(), val.trim().to_string()))
788            })
789            .collect();
790
791        for selector in selectors {
792            let specificity = selector.specificity();
793            rules.push(CssRule {
794                selector,
795                declarations: declarations.clone(),
796                specificity,
797            });
798        }
799    }
800
801    // Sort by specificity (lower first, so higher specificity overwrites)
802    rules.sort_by_key(|r| r.specificity);
803    (rules, keyframes)
804}
805
806fn parse_selector_token(token: &str, parts: &mut Vec<SelectorPart>) {
807    if token == "*" {
808        parts.push(SelectorPart::Universal);
809        return;
810    }
811    if token.is_empty() {
812        return;
813    }
814
815    let mut current = String::new();
816
817    for c in token.chars() {
818        if (c == '.' || c == '#') && !current.is_empty() {
819            flush_selector_part(&current, parts);
820            current = String::new();
821        }
822        current.push(c);
823    }
824
825    if !current.is_empty() {
826        flush_selector_part(&current, parts);
827    }
828}
829
830fn flush_selector_part(s: &str, parts: &mut Vec<SelectorPart>) {
831    if let Some(id) = s.strip_prefix('#') {
832        parts.push(SelectorPart::Id(id.to_string()));
833    } else if let Some(class) = s.strip_prefix('.') {
834        parts.push(SelectorPart::Class(class.to_string()));
835    } else {
836        parts.push(SelectorPart::Tag(s.to_string()));
837    }
838}
839
840fn parse_keyframes(body: &str) -> Vec<crate::scene::Keyframe> {
841    let mut frames = Vec::new();
842    let mut pos = 0;
843    let bytes = body.as_bytes();
844
845    while pos < bytes.len() {
846        while pos < bytes.len() && (bytes[pos] as char).is_whitespace() {
847            pos += 1;
848        }
849        if pos >= bytes.len() {
850            break;
851        }
852
853        // Find percentage/from/to (before '{')
854        let start = pos;
855        while pos < bytes.len() && bytes[pos] != b'{' {
856            pos += 1;
857        }
858        if pos >= bytes.len() {
859            break;
860        }
861        let pct_str = body[start..pos].trim();
862        pos += 1;
863
864        // Find declarations (before '}')
865        let decl_start = pos;
866        let mut depth = 1;
867        while pos < bytes.len() && depth > 0 {
868            if bytes[pos] == b'{' {
869                depth += 1;
870            } else if bytes[pos] == b'}' {
871                depth -= 1;
872            }
873            pos += 1;
874        }
875        let decl_str = &body[decl_start..pos - 1];
876
877        let percent = match pct_str {
878            "from" => 0.0,
879            "to" => 1.0,
880            s => s
881                .strip_suffix('%')
882                .and_then(|p| p.trim().parse::<f32>().ok())
883                .map(|p| p / 100.0)
884                .unwrap_or(0.0),
885        };
886
887        let mut style = ResolvedStyle::default();
888        parse_inline_style(decl_str, &mut style);
889
890        frames.push(crate::scene::Keyframe { percent, style });
891    }
892
893    frames.sort_by(|a, b| a.percent.partial_cmp(&b.percent).unwrap());
894    frames
895}
896
897pub fn apply_styles(scene: &mut SceneGraph, rules: &[CssRule]) {
898    let node_count = scene.node_count();
899    for i in 0..node_count {
900        let node_id = NodeId(i);
901
902        // Apply matching non-hover rules in specificity order
903        for rule in rules {
904            if rule.selector.is_hover() {
905                continue;
906            }
907            let matches = if rule.selector.segments.len() > 1 {
908                rule.selector
909                    .matches_with_ancestors(scene.get(node_id), scene)
910            } else {
911                rule.selector.matches(scene.get(node_id))
912            };
913            if matches {
914                for (prop, val) in &rule.declarations {
915                    apply_property(&mut scene.nodes[i].style, prop, val);
916                }
917            }
918        }
919
920        // Apply inline styles (highest specificity)
921        let inline_style = scene.get(node_id).attributes.get("style").cloned();
922        if let Some(style_str) = inline_style {
923            parse_inline_style(&style_str, &mut scene.nodes[i].style);
924        }
925
926        // Inherit color and font properties from parent
927        if let Some(parent_id) = scene.get(node_id).parent {
928            let parent_color = scene.get(parent_id).style.color;
929            let parent_font = scene.get(parent_id).style.font_family.clone();
930
931            let node_has_color = scene.nodes[i]
932                .attributes
933                .get("style")
934                .map(|s| {
935                    // Check for standalone "color:" not "background-color:"
936                    s.split(';').any(|decl| {
937                        let prop = decl.split(':').next().unwrap_or("").trim();
938                        prop == "color"
939                    })
940                })
941                .unwrap_or(false);
942            let node_has_class_color = rules.iter().any(|r| {
943                r.selector.matches(&scene.nodes[i])
944                    && r.declarations.iter().any(|(p, _)| p == "color")
945            });
946
947            if !node_has_color && !node_has_class_color {
948                scene.nodes[i].style.color = parent_color;
949            }
950
951            if scene.nodes[i].style.font_family.is_empty() {
952                scene.nodes[i].style.font_family = parent_font;
953            }
954        }
955
956        // Collect hover styles: start from the node's resolved style, apply hover rules on top
957        let hover_rules: Vec<&CssRule> = rules
958            .iter()
959            .filter(|r| {
960                r.selector.is_hover()
961                    && if r.selector.segments.len() > 1 {
962                        r.selector.matches_with_ancestors(&scene.nodes[i], scene)
963                    } else {
964                        r.selector.matches(&scene.nodes[i])
965                    }
966            })
967            .collect();
968
969        if !hover_rules.is_empty() {
970            let mut hover = scene.nodes[i].style.clone();
971            for rule in &hover_rules {
972                for (prop, val) in &rule.declarations {
973                    apply_property(&mut hover, prop, val);
974                }
975            }
976            scene.nodes[i].hover_style = Some(hover);
977            scene.nodes[i].base_style = Some(scene.nodes[i].style.clone());
978        }
979
980        // Store image src
981        let img_src = scene.nodes[i].attributes.get("src").cloned();
982        if scene.nodes[i].kind == crate::scene::ElementKind::Img {
983            scene.nodes[i].image_src = img_src;
984        }
985
986        // Set up CSS animation if node has animation-name
987        let anim_name = scene.nodes[i].style.animation_name.clone();
988        if !anim_name.is_empty() {
989            if let Some(kf_anim) = scene.keyframes.iter().find(|k| k.name == anim_name) {
990                scene.nodes[i].animation = Some(crate::scene::AnimationState {
991                    animation_name: anim_name,
992                    duration: scene.nodes[i].style.animation_duration,
993                    delay: scene.nodes[i].style.animation_delay,
994                    iteration_count: scene.nodes[i].style.animation_iteration_count,
995                    direction: scene.nodes[i].style.animation_direction,
996                    start_time: None, // Started on first frame
997                    current_iteration: 0.0,
998                    keyframes: kf_anim.keyframes.clone(),
999                });
1000            }
1001        }
1002    }
1003}