1use crate::ir::layout::LayoutConstraints;
6use crate::ir::style::{Color, StyleProperties};
7use crate::ir::theme::{
8 FontWeight, SpacingScale, StyleClass, Theme, ThemePalette, Typography, WidgetState,
9};
10use std::collections::HashMap;
11
12pub fn parse_theme(
14 name: String,
15 palette_attrs: &HashMap<String, String>,
16 typography_attrs: &HashMap<String, String>,
17 spacing_unit: Option<f32>,
18) -> Result<Theme, String> {
19 let palette = parse_palette(palette_attrs)?;
20 let typography = parse_typography(typography_attrs)?;
21 let spacing = SpacingScale {
22 unit: spacing_unit.unwrap_or(4.0),
23 };
24
25 let theme = Theme {
26 name,
27 palette,
28 typography,
29 spacing,
30 base_styles: HashMap::new(),
31 };
32
33 theme.validate()?;
34 Ok(theme)
35}
36
37pub fn parse_palette(attrs: &HashMap<String, String>) -> Result<ThemePalette, String> {
39 let get_color = |key: &str| -> Result<Color, String> {
40 let value = attrs
41 .get(key)
42 .ok_or_else(|| format!("Missing required palette color: {}", key))?;
43 Color::parse(value)
44 };
45
46 Ok(ThemePalette {
47 primary: get_color("primary")?,
48 secondary: get_color("secondary")?,
49 success: get_color("success")?,
50 warning: get_color("warning")?,
51 danger: get_color("danger")?,
52 background: get_color("background")?,
53 surface: get_color("surface")?,
54 text: get_color("text")?,
55 text_secondary: get_color("text_secondary")?,
56 })
57}
58
59pub fn parse_typography(attrs: &HashMap<String, String>) -> Result<Typography, String> {
61 let font_family = attrs
62 .get("font_family")
63 .cloned()
64 .unwrap_or_else(|| "sans-serif".to_string());
65
66 let font_size_base: f32 = attrs
67 .get("font_size_base")
68 .unwrap_or(&"16.0".to_string())
69 .parse()
70 .map_err(|_| "Invalid font_size_base")?;
71
72 let font_size_small: f32 = attrs
73 .get("font_size_small")
74 .unwrap_or(&"12.0".to_string())
75 .parse()
76 .map_err(|_| "Invalid font_size_small")?;
77
78 let font_size_large: f32 = attrs
79 .get("font_size_large")
80 .unwrap_or(&"20.0".to_string())
81 .parse()
82 .map_err(|_| "Invalid font_size_large")?;
83
84 let font_weight = match attrs.get("font_weight") {
85 Some(w) => FontWeight::parse(w)?,
86 None => FontWeight::Normal,
87 };
88
89 let line_height: f32 = attrs
90 .get("line_height")
91 .unwrap_or(&"1.5".to_string())
92 .parse()
93 .map_err(|_| "Invalid line_height")?;
94
95 Ok(Typography {
96 font_family,
97 font_size_base,
98 font_size_small,
99 font_size_large,
100 font_weight,
101 line_height,
102 })
103}
104
105pub fn parse_style_class(
107 name: String,
108 base_attrs: &HashMap<String, String>,
109 extends: Vec<String>,
110 state_variants: HashMap<WidgetState, StyleProperties>,
111 combined_state_variants: HashMap<crate::ir::theme::StateSelector, StyleProperties>,
112 layout: Option<LayoutConstraints>,
113) -> Result<StyleClass, String> {
114 let style = parse_style_properties_from_attrs(base_attrs)?;
115
116 let class = StyleClass {
117 name,
118 style,
119 layout,
120 extends,
121 state_variants,
122 combined_state_variants,
123 };
124
125 Ok(class)
126}
127
128pub fn parse_style_properties_from_attrs(
130 attrs: &HashMap<String, String>,
131) -> Result<StyleProperties, String> {
132 use crate::parser::style_parser::*;
133
134 let mut background = None;
135 let mut color = None;
136 let mut shadow = None;
137 let mut opacity = None;
138 let mut transform = None;
139
140 if let Some(value) = attrs.get("background") {
142 background = Some(parse_background_attr(value)?);
143 }
144
145 if let Some(value) = attrs.get("color") {
147 color = Some(parse_color_attr(value)?);
148 }
149
150 let border_width = attrs
152 .get("border_width")
153 .map(|v| parse_border_width(v))
154 .transpose()?;
155 let border_color = attrs
156 .get("border_color")
157 .map(|v| parse_border_color(v))
158 .transpose()?;
159 let border_radius = attrs
160 .get("border_radius")
161 .map(|v| parse_border_radius(v))
162 .transpose()?;
163 let border_style = attrs
164 .get("border_style")
165 .map(|v| parse_border_style(v))
166 .transpose()?;
167
168 let border = build_border(border_width, border_color, border_radius, border_style)?;
169
170 if let Some(value) = attrs.get("shadow") {
172 shadow = Some(parse_shadow_attr(value)?);
173 }
174
175 if let Some(value) = attrs.get("opacity") {
177 opacity = Some(parse_opacity(value)?);
178 }
179
180 if let Some(value) = attrs.get("transform") {
182 transform = Some(parse_transform(value)?);
183 }
184
185 build_style_properties(background, color, border, shadow, opacity, transform)
186}
187
188pub fn parse_layout_constraints(
190 attrs: &HashMap<String, String>,
191) -> Result<Option<LayoutConstraints>, String> {
192 use crate::parser::style_parser::*;
193
194 let mut constraints = LayoutConstraints::default();
195 let mut has_any = false;
196
197 if let Some(value) = attrs.get("width") {
199 constraints.width = Some(parse_length_attr(value)?);
200 has_any = true;
201 }
202
203 if let Some(value) = attrs.get("height") {
204 constraints.height = Some(parse_length_attr(value)?);
205 has_any = true;
206 }
207
208 if let Some(value) = attrs.get("min_width") {
210 constraints.min_width = Some(parse_constraint(value)?);
211 has_any = true;
212 }
213
214 if let Some(value) = attrs.get("max_width") {
215 constraints.max_width = Some(parse_constraint(value)?);
216 has_any = true;
217 }
218
219 if let Some(value) = attrs.get("min_height") {
220 constraints.min_height = Some(parse_constraint(value)?);
221 has_any = true;
222 }
223
224 if let Some(value) = attrs.get("max_height") {
225 constraints.max_height = Some(parse_constraint(value)?);
226 has_any = true;
227 }
228
229 if let Some(value) = attrs.get("padding") {
231 constraints.padding = Some(parse_padding_attr(value)?);
232 has_any = true;
233 }
234
235 if let Some(value) = attrs.get("spacing") {
236 constraints.spacing = Some(parse_spacing(value)?);
237 has_any = true;
238 }
239
240 if let Some(value) = attrs.get("align_items") {
242 constraints.align_items = Some(parse_alignment(value)?);
243 has_any = true;
244 }
245
246 if let Some(value) = attrs.get("justify_content") {
247 constraints.justify_content = Some(parse_justification(value)?);
248 has_any = true;
249 }
250
251 if let Some(value) = attrs.get("align_self") {
252 constraints.align_self = Some(parse_alignment(value)?);
253 has_any = true;
254 }
255
256 if let Some(value) = attrs.get("direction") {
258 constraints.direction = Some(crate::ir::layout::Direction::parse(value)?);
259 has_any = true;
260 }
261
262 if has_any {
263 constraints.validate()?;
264 Ok(Some(constraints))
265 } else {
266 Ok(None)
267 }
268}
269
270pub type StateVariantMaps = (
273 HashMap<WidgetState, StyleProperties>,
274 HashMap<crate::ir::theme::StateSelector, StyleProperties>,
275);
276
277pub fn parse_state_variants(attrs: &HashMap<String, String>) -> Result<StateVariantMaps, String> {
279 use crate::ir::theme::StateSelector;
280
281 let mut single_variants: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
282 let mut combined_variants: HashMap<StateSelector, HashMap<String, String>> = HashMap::new();
283
284 for (key, value) in attrs {
285 if let Some((prefix, attr_name)) = split_state_prefix(key) {
287 if let Some(states) = parse_combined_states(prefix) {
289 if states.len() == 1 {
290 single_variants
292 .entry(states[0])
293 .or_default()
294 .insert(attr_name.to_string(), value.to_string());
295 } else {
296 let selector = StateSelector::combined(states);
298 combined_variants
299 .entry(selector)
300 .or_default()
301 .insert(attr_name.to_string(), value.to_string());
302 }
303 } else {
304 return Err(format!("Invalid state prefix: {}", prefix));
305 }
306 }
307 }
308
309 let mut single_result = HashMap::new();
311 for (state, state_attrs) in single_variants {
312 let style = parse_style_properties_from_attrs(&state_attrs)?;
313 single_result.insert(state, style);
314 }
315
316 let mut combined_result = HashMap::new();
318 for (selector, state_attrs) in combined_variants {
319 let style = parse_style_properties_from_attrs(&state_attrs)?;
320 combined_result.insert(selector, style);
321 }
322
323 Ok((single_result, combined_result))
324}
325
326fn split_state_prefix(key: &str) -> Option<(&str, &str)> {
330 let colons: Vec<usize> = key.match_indices(':').map(|(i, _)| i).collect();
332
333 let last_colon = match colons.last() {
335 Some(&pos) => pos,
336 None => return None,
337 };
338 let attr_name = &key[last_colon + 1..];
339
340 let potential_states = &key[..last_colon];
343
344 let state_parts: Vec<&str> = potential_states.split(':').collect();
346
347 let all_valid_states = state_parts.iter().all(|&s| {
349 matches!(
350 s.trim().to_lowercase().as_str(),
351 "hover" | "focus" | "active" | "disabled"
352 )
353 });
354
355 if all_valid_states && !state_parts.is_empty() {
356 return Some((potential_states, attr_name));
358 }
359
360 None
361}
362
363fn parse_combined_states(prefix: &str) -> Option<Vec<WidgetState>> {
366 let parts: Vec<&str> = prefix.split(':').collect();
367 let mut states = Vec::new();
368
369 for part in parts {
370 if let Some(state) = WidgetState::from_prefix(part) {
371 if !states.contains(&state) {
373 states.push(state);
374 }
375 } else {
376 return None;
377 }
378 }
379
380 if states.is_empty() {
381 None
382 } else {
383 Some(states)
384 }
385}
386
387pub fn parse_theme_from_node(
389 node: roxmltree::Node,
390 _source: &str,
391) -> Result<Theme, crate::parser::error::ParseError> {
392 use crate::parser::error::{ParseError, ParseErrorKind};
393
394 let name = node
395 .attribute("name")
396 .map(|s| s.to_string())
397 .unwrap_or_else(|| "default".to_string());
398
399 let mut palette_attrs = HashMap::new();
400 let mut typography_attrs = HashMap::new();
401 let mut spacing_unit = None;
402
403 for child in node.children() {
405 if child.node_type() != roxmltree::NodeType::Element {
406 continue;
407 }
408
409 let tag = child.tag_name().name();
410
411 if tag == "palette" {
412 for attr in child.attributes() {
413 palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
414 }
415 } else if tag == "typography" {
416 for attr in child.attributes() {
417 typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
418 }
419 } else if tag == "spacing" {
420 if let Some(unit) = child.attribute("unit") {
421 spacing_unit = unit.parse::<f32>().ok();
422 }
423 }
424 }
425
426 let required_colors = ["primary", "secondary", "background", "text"];
428 for color in &required_colors {
429 if !palette_attrs.contains_key(*color) {
430 return Err(ParseError {
431 kind: ParseErrorKind::InvalidValue,
432 message: format!("Theme palette missing required color: {}", color),
433 span: crate::ir::Span::default(),
434 suggestion: None,
435 });
436 }
437 }
438
439 let theme =
441 parse_theme(name, &palette_attrs, &typography_attrs, spacing_unit).map_err(|e| {
442 ParseError {
443 kind: ParseErrorKind::InvalidValue,
444 message: format!("Failed to parse theme: {}", e),
445 span: crate::ir::Span::default(),
446 suggestion: None,
447 }
448 })?;
449
450 Ok(theme)
451}
452
453pub fn parse_style_class_from_node(
455 node: roxmltree::Node,
456 _source: &str,
457) -> Result<StyleClass, crate::parser::error::ParseError> {
458 use crate::parser::error::{ParseError, ParseErrorKind};
459
460 let name = node
461 .attribute("name")
462 .map(|s| s.to_string())
463 .unwrap_or_default();
464
465 if name.is_empty() {
466 return Err(ParseError {
467 kind: ParseErrorKind::InvalidValue,
468 message: "Style class must have a name".to_string(),
469 span: crate::ir::Span::default(),
470 suggestion: None,
471 });
472 }
473
474 let mut base_attrs = HashMap::new();
476 let mut extends = Vec::new();
477 let mut state_variants_raw: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
478 let mut combined_state_variants_raw: HashMap<
479 crate::ir::theme::StateSelector,
480 HashMap<String, String>,
481 > = HashMap::new();
482 let mut layout = None;
483
484 for attr in node.attributes() {
485 let key = attr.name();
486 let value = attr.value();
487
488 if key == "extends" {
490 extends = value.split_whitespace().map(|s| s.to_string()).collect();
491 continue;
492 }
493
494 if let Some((prefix, attr_name)) = split_state_prefix(key) {
496 if let Some(states) = parse_combined_states(prefix) {
498 if states.len() == 1 {
499 let state_attr = state_variants_raw.entry(states[0]).or_default();
501 state_attr.insert(attr_name.to_string(), value.to_string());
502 } else {
503 let selector = crate::ir::theme::StateSelector::combined(states);
505 let state_attr = combined_state_variants_raw.entry(selector).or_default();
506 state_attr.insert(attr_name.to_string(), value.to_string());
507 }
508 } else {
509 return Err(ParseError {
510 kind: ParseErrorKind::InvalidValue,
511 message: format!("Invalid state prefix: {}", prefix),
512 span: crate::ir::Span::default(),
513 suggestion: None,
514 });
515 }
516 continue;
517 }
518
519 let layout_attr_names = [
521 "width",
522 "height",
523 "min_width",
524 "max_width",
525 "min_height",
526 "max_height",
527 "padding",
528 "spacing",
529 "align_items",
530 "justify_content",
531 "align_self",
532 "direction",
533 ];
534
535 if layout_attr_names.contains(&key) {
536 base_attrs.insert(key.to_string(), value.to_string());
537 continue;
538 }
539
540 base_attrs.insert(key.to_string(), value.to_string());
542 }
543
544 for child in node.children() {
546 if child.node_type() != roxmltree::NodeType::Element {
547 continue;
548 }
549
550 let tag = child.tag_name().name();
551
552 if let Some(state) = WidgetState::from_prefix(tag) {
554 let state_attr = state_variants_raw.entry(state).or_default();
555 for attr in child.attributes() {
556 state_attr.insert(attr.name().to_string(), attr.value().to_string());
557 }
558 continue;
559 }
560
561 if tag == "base" {
563 for attr in child.attributes() {
564 base_attrs.insert(attr.name().to_string(), attr.value().to_string());
565 }
566 continue;
567 }
568
569 if tag == "layout" {
571 let mut layout_attrs = HashMap::new();
572 for attr in child.attributes() {
573 layout_attrs.insert(attr.name().to_string(), attr.value().to_string());
574 }
575 layout = parse_layout_constraints(&layout_attrs).map_err(|e| ParseError {
576 kind: ParseErrorKind::InvalidValue,
577 message: format!("Failed to parse layout: {}", e),
578 span: crate::ir::Span::default(),
579 suggestion: None,
580 })?;
581 continue;
582 }
583 }
584
585 if base_attrs.keys().any(|k| {
587 matches!(
588 k.as_str(),
589 "width"
590 | "height"
591 | "min_width"
592 | "max_width"
593 | "min_height"
594 | "max_height"
595 | "padding"
596 | "spacing"
597 | "align_items"
598 | "justify_content"
599 | "align_self"
600 | "direction"
601 )
602 }) {
603 layout = parse_layout_constraints(&base_attrs).map_err(|e| ParseError {
604 kind: ParseErrorKind::InvalidValue,
605 message: format!("Failed to parse layout: {}", e),
606 span: crate::ir::Span::default(),
607 suggestion: None,
608 })?;
609
610 let layout_keys: Vec<String> = base_attrs
612 .keys()
613 .filter(|k| {
614 matches!(
615 k.as_str(),
616 "width"
617 | "height"
618 | "min_width"
619 | "max_width"
620 | "min_height"
621 | "max_height"
622 | "padding"
623 | "spacing"
624 | "align_items"
625 | "justify_content"
626 | "align_self"
627 | "direction"
628 )
629 })
630 .cloned()
631 .collect();
632
633 for key in layout_keys {
634 base_attrs.remove(&key);
635 }
636 }
637
638 let mut state_variants = HashMap::new();
640 for (state, state_attrs) in state_variants_raw {
641 let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
642 kind: ParseErrorKind::InvalidValue,
643 message: format!("Failed to parse state variant for {:?}: {}", state, e),
644 span: crate::ir::Span::default(),
645 suggestion: None,
646 })?;
647 state_variants.insert(state, style);
648 }
649
650 let mut combined_state_variants = HashMap::new();
652 for (selector, state_attrs) in combined_state_variants_raw {
653 let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
654 kind: ParseErrorKind::InvalidValue,
655 message: format!(
656 "Failed to parse combined state variant for {:?}: {}",
657 selector, e
658 ),
659 span: crate::ir::Span::default(),
660 suggestion: None,
661 })?;
662 combined_state_variants.insert(selector, style);
663 }
664
665 let class = parse_style_class(
667 name,
668 &base_attrs,
669 extends,
670 state_variants,
671 combined_state_variants,
672 layout,
673 )
674 .map_err(|e| ParseError {
675 kind: ParseErrorKind::InvalidValue,
676 message: format!("Failed to parse style class: {}", e),
677 span: crate::ir::Span::default(),
678 suggestion: None,
679 })?;
680
681 Ok(class)
682}