1use crate::scene::*;
2
3pub fn parse_color(s: &str) -> Option<Color> {
4 let s = s.trim();
5
6 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 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 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 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" => {
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 "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 "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" => {
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" => {
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" => {
284 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" => {
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 "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 "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" => {
409 style.overflow = match val {
410 "hidden" => Overflow::Hidden,
411 "scroll" | "auto" => Overflow::Scroll,
412 _ => Overflow::Visible,
413 };
414 }
415
416 "z-index" => {
418 if let Ok(v) = val.parse::<i32>() {
419 style.z_index = v;
420 }
421 }
422
423 "transition" => {
425 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, };
432 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" => {
456 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 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 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 _ => {} }
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#[derive(Debug, Clone)]
550pub struct CssRule {
551 pub selector: Selector,
552 pub declarations: Vec<(String, String)>,
553 pub specificity: u32,
554}
555
556#[derive(Debug, Clone)]
558pub enum SelectorPart {
559 Tag(String),
560 Class(String),
561 Id(String),
562 Universal,
563}
564
565#[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#[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 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 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 if !self.segments.last().unwrap().matches_node(node) {
634 return false;
635 }
636 if self.segments.len() == 1 {
637 return true;
638 }
639 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 while pos < bytes.len() && (bytes[pos] as char).is_whitespace() {
674 pos += 1;
675 }
676 if pos >= bytes.len() {
677 break;
678 }
679
680 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 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; if selector_str.starts_with("@keyframes") {
702 let anim_name = selector_str
703 .strip_prefix("@keyframes")
704 .unwrap()
705 .trim()
706 .to_string();
707 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 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 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 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 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 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(¤t, parts);
820 current = String::new();
821 }
822 current.push(c);
823 }
824
825 if !current.is_empty() {
826 flush_selector_part(¤t, 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 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 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 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 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 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 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 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 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 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, current_iteration: 0.0,
998 keyframes: kf_anim.keyframes.clone(),
999 });
1000 }
1001 }
1002 }
1003}