1use crate::ir::layout::LayoutConstraints;
6use crate::ir::style::{Color, StyleProperties};
7use crate::ir::theme::{
8 FontWeight, SpacingScale, StyleClass, Theme, ThemeDocument, ThemeError, ThemeErrorKind,
9 ThemePalette, Typography, WidgetState,
10};
11use std::collections::HashMap;
12
13pub fn parse_theme_document(xml: &str) -> Result<ThemeDocument, ThemeError> {
15 let doc = roxmltree::Document::parse(xml).map_err(|e| ThemeError {
16 kind: ThemeErrorKind::MissingPaletteColor,
17 message: format!("THEME_003: Failed to parse XML: {}", e),
18 })?;
19
20 let root = doc.root().first_child().ok_or_else(|| ThemeError {
22 kind: ThemeErrorKind::MissingPaletteColor,
23 message: "THEME_003: No root element found".to_string(),
24 })?;
25
26 if root.tag_name().name() != "dampen" {
28 return Err(ThemeError {
29 kind: ThemeErrorKind::MissingPaletteColor,
30 message: "THEME_003: Root element must be <dampen>".to_string(),
31 });
32 }
33
34 let mut themes = HashMap::new();
35 let mut default_theme = None;
36 let mut follow_system = true;
37
38 for child in root.children() {
40 if child.node_type() != roxmltree::NodeType::Element {
41 continue;
42 }
43
44 let tag = child.tag_name().name();
45
46 match tag {
47 "themes" => {
48 for grandchild in child.children() {
50 if grandchild.node_type() != roxmltree::NodeType::Element {
51 continue;
52 }
53
54 if grandchild.tag_name().name() == "theme" {
55 let theme = parse_theme_from_node_simple(grandchild)?;
56 if themes.contains_key(&theme.name) {
57 return Err(ThemeError {
58 kind: ThemeErrorKind::DuplicateThemeName,
59 message: format!(
60 "THEME_005: Duplicate theme name: '{}'",
61 theme.name
62 ),
63 });
64 }
65 themes.insert(theme.name.clone(), theme);
66 }
67 }
68 }
69 "default_theme" => {
70 if let Some(name) = child.attribute("name") {
71 default_theme = Some(name.to_string());
72 }
73 }
74 "follow_system" => {
75 if let Some(enabled) = child.attribute("enabled") {
76 follow_system = enabled.parse::<bool>().unwrap_or(true);
77 }
78 }
79 _ => {}
80 }
81 }
82
83 let document = ThemeDocument {
84 themes,
85 default_theme,
86 follow_system,
87 };
88
89 document.validate()?;
90 Ok(document)
91}
92
93fn parse_theme_from_node_simple(node: roxmltree::Node) -> Result<Theme, ThemeError> {
95 let name = node
96 .attribute("name")
97 .map(|s| s.to_string())
98 .unwrap_or_else(|| "default".to_string());
99
100 let extends = node.attribute("extends").map(|s| s.to_string());
101
102 let mut palette_attrs = HashMap::new();
103 let mut typography_attrs = HashMap::new();
104 let mut spacing_unit = None;
105
106 for child in node.children() {
108 if child.node_type() != roxmltree::NodeType::Element {
109 continue;
110 }
111
112 let tag = child.tag_name().name();
113
114 match tag {
115 "palette" => {
116 for attr in child.attributes() {
117 palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
118 }
119 }
120 "typography" => {
121 for attr in child.attributes() {
122 typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
123 }
124 }
125 "spacing" => {
126 if let Some(unit) = child.attribute("unit") {
127 spacing_unit = unit.parse::<f32>().ok();
128 }
129 }
130 _ => {}
131 }
132 }
133
134 let palette = parse_palette(&palette_attrs).map_err(|e| ThemeError {
135 kind: ThemeErrorKind::MissingPaletteColor,
136 message: format!("THEME_003: Invalid palette: {}", e),
137 })?;
138
139 let typography = parse_typography(&typography_attrs).map_err(|e| ThemeError {
140 kind: ThemeErrorKind::MissingPaletteColor,
141 message: format!("THEME_003: Invalid typography: {}", e),
142 })?;
143
144 let spacing = SpacingScale { unit: spacing_unit };
145
146 spacing.validate().map_err(|e| ThemeError {
147 kind: ThemeErrorKind::MissingPaletteColor,
148 message: format!("THEME_003: Invalid spacing: {}", e),
149 })?;
150
151 let theme = Theme {
152 name,
153 palette,
154 typography,
155 spacing,
156 base_styles: HashMap::new(),
157 extends,
158 };
159
160 Ok(theme)
161}
162
163pub fn parse_theme(
165 name: String,
166 palette_attrs: &HashMap<String, String>,
167 typography_attrs: &HashMap<String, String>,
168 spacing_unit: Option<f32>,
169 extends: Option<String>,
170) -> Result<Theme, String> {
171 let palette = parse_palette(palette_attrs)?;
172 let typography = parse_typography(typography_attrs)?;
173 let spacing = SpacingScale { unit: spacing_unit };
174
175 let theme = Theme {
176 name,
177 palette,
178 typography,
179 spacing,
180 base_styles: HashMap::new(),
181 extends: extends.clone(),
182 };
183
184 theme.validate(extends.is_some())?;
185 Ok(theme)
186}
187
188pub fn parse_palette(attrs: &HashMap<String, String>) -> Result<ThemePalette, String> {
190 let get_color = |key: &str| -> Result<Option<Color>, String> {
191 if let Some(value) = attrs.get(key) {
192 Ok(Some(Color::parse(value)?))
193 } else {
194 Ok(None)
195 }
196 };
197
198 Ok(ThemePalette {
199 primary: get_color("primary")?,
200 secondary: get_color("secondary")?,
201 success: get_color("success")?,
202 warning: get_color("warning")?,
203 danger: get_color("danger")?,
204 background: get_color("background")?,
205 surface: get_color("surface")?,
206 text: get_color("text")?,
207 text_secondary: get_color("text_secondary")?,
208 })
209}
210
211pub fn parse_typography(attrs: &HashMap<String, String>) -> Result<Typography, String> {
213 let font_family = attrs.get("font_family").cloned();
214
215 let font_size_base = if let Some(s) = attrs.get("font_size_base") {
216 Some(s.parse().map_err(|_| "Invalid font_size_base")?)
217 } else {
218 None
219 };
220
221 let font_size_small = if let Some(s) = attrs.get("font_size_small") {
222 Some(s.parse().map_err(|_| "Invalid font_size_small")?)
223 } else {
224 None
225 };
226
227 let font_size_large = if let Some(s) = attrs.get("font_size_large") {
228 Some(s.parse().map_err(|_| "Invalid font_size_large")?)
229 } else {
230 None
231 };
232
233 let font_weight = match attrs.get("font_weight") {
234 Some(w) => FontWeight::parse(w)?,
235 None => FontWeight::Normal,
236 };
237
238 let line_height = if let Some(s) = attrs.get("line_height") {
239 Some(s.parse().map_err(|_| "Invalid line_height")?)
240 } else {
241 None
242 };
243
244 Ok(Typography {
245 font_family,
246 font_size_base,
247 font_size_small,
248 font_size_large,
249 font_weight,
250 line_height,
251 })
252}
253
254pub fn parse_style_class(
256 name: String,
257 base_attrs: &HashMap<String, String>,
258 extends: Vec<String>,
259 state_variants: HashMap<WidgetState, StyleProperties>,
260 combined_state_variants: HashMap<crate::ir::theme::StateSelector, StyleProperties>,
261 layout: Option<LayoutConstraints>,
262) -> Result<StyleClass, String> {
263 let style = parse_style_properties_from_attrs(base_attrs)?;
264
265 let class = StyleClass {
266 name,
267 style,
268 layout,
269 extends,
270 state_variants,
271 combined_state_variants,
272 };
273
274 Ok(class)
275}
276
277pub fn parse_style_properties_from_attrs(
279 attrs: &HashMap<String, String>,
280) -> Result<StyleProperties, String> {
281 use crate::parser::style_parser::*;
282
283 let mut background = None;
284 let mut color = None;
285 let mut shadow = None;
286 let mut opacity = None;
287 let mut transform = None;
288
289 if let Some(value) = attrs.get("background") {
291 background = Some(parse_background_attr(value)?);
292 }
293
294 if let Some(value) = attrs.get("color") {
296 color = Some(parse_color_attr(value)?);
297 }
298
299 let border_width = attrs
301 .get("border_width")
302 .map(|v| parse_border_width(v))
303 .transpose()?;
304 let border_color = attrs
305 .get("border_color")
306 .map(|v| parse_border_color(v))
307 .transpose()?;
308 let border_radius = attrs
309 .get("border_radius")
310 .map(|v| parse_border_radius(v))
311 .transpose()?;
312 let border_style = attrs
313 .get("border_style")
314 .map(|v| parse_border_style(v))
315 .transpose()?;
316
317 let border = build_border(border_width, border_color, border_radius, border_style)?;
318
319 if let Some(value) = attrs.get("shadow") {
321 shadow = Some(parse_shadow_attr(value)?);
322 }
323
324 if let Some(value) = attrs.get("opacity") {
326 opacity = Some(parse_opacity(value)?);
327 }
328
329 if let Some(value) = attrs.get("transform") {
331 transform = Some(parse_transform(value)?);
332 }
333
334 build_style_properties(background, color, border, shadow, opacity, transform)
335}
336
337pub fn parse_layout_constraints(
339 attrs: &HashMap<String, String>,
340) -> Result<Option<LayoutConstraints>, String> {
341 use crate::parser::style_parser::*;
342
343 let mut constraints = LayoutConstraints::default();
344 let mut has_any = false;
345
346 if let Some(value) = attrs.get("width") {
348 constraints.width = Some(parse_length_attr(value)?);
349 has_any = true;
350 }
351
352 if let Some(value) = attrs.get("height") {
353 constraints.height = Some(parse_length_attr(value)?);
354 has_any = true;
355 }
356
357 if let Some(value) = attrs.get("min_width") {
359 constraints.min_width = Some(parse_constraint(value)?);
360 has_any = true;
361 }
362
363 if let Some(value) = attrs.get("max_width") {
364 constraints.max_width = Some(parse_constraint(value)?);
365 has_any = true;
366 }
367
368 if let Some(value) = attrs.get("min_height") {
369 constraints.min_height = Some(parse_constraint(value)?);
370 has_any = true;
371 }
372
373 if let Some(value) = attrs.get("max_height") {
374 constraints.max_height = Some(parse_constraint(value)?);
375 has_any = true;
376 }
377
378 if let Some(value) = attrs.get("padding") {
380 constraints.padding = Some(parse_padding_attr(value)?);
381 has_any = true;
382 }
383
384 if let Some(value) = attrs.get("spacing") {
385 constraints.spacing = Some(parse_spacing(value)?);
386 has_any = true;
387 }
388
389 if let Some(value) = attrs.get("align_items") {
391 constraints.align_items = Some(parse_alignment(value)?);
392 has_any = true;
393 }
394
395 if let Some(value) = attrs.get("justify_content") {
396 constraints.justify_content = Some(parse_justification(value)?);
397 has_any = true;
398 }
399
400 if let Some(value) = attrs.get("align_self") {
401 constraints.align_self = Some(parse_alignment(value)?);
402 has_any = true;
403 }
404
405 if let Some(value) = attrs.get("direction") {
407 constraints.direction = Some(crate::ir::layout::Direction::parse(value)?);
408 has_any = true;
409 }
410
411 if has_any {
412 constraints.validate()?;
413 Ok(Some(constraints))
414 } else {
415 Ok(None)
416 }
417}
418
419pub type StateVariantMaps = (
422 HashMap<WidgetState, StyleProperties>,
423 HashMap<crate::ir::theme::StateSelector, StyleProperties>,
424);
425
426pub fn parse_state_variants(attrs: &HashMap<String, String>) -> Result<StateVariantMaps, String> {
428 use crate::ir::theme::StateSelector;
429
430 let mut single_variants: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
431 let mut combined_variants: HashMap<StateSelector, HashMap<String, String>> = HashMap::new();
432
433 for (key, value) in attrs {
434 if let Some((prefix, attr_name)) = split_state_prefix(key) {
436 if let Some(states) = parse_combined_states(prefix) {
438 if states.len() == 1 {
439 single_variants
441 .entry(states[0])
442 .or_default()
443 .insert(attr_name.to_string(), value.to_string());
444 } else {
445 let selector = StateSelector::combined(states);
447 combined_variants
448 .entry(selector)
449 .or_default()
450 .insert(attr_name.to_string(), value.to_string());
451 }
452 } else {
453 return Err(format!("Invalid state prefix: {}", prefix));
454 }
455 }
456 }
457
458 let mut single_result = HashMap::new();
460 for (state, state_attrs) in single_variants {
461 let style = parse_style_properties_from_attrs(&state_attrs)?;
462 single_result.insert(state, style);
463 }
464
465 let mut combined_result = HashMap::new();
467 for (selector, state_attrs) in combined_variants {
468 let style = parse_style_properties_from_attrs(&state_attrs)?;
469 combined_result.insert(selector, style);
470 }
471
472 Ok((single_result, combined_result))
473}
474
475fn split_state_prefix(key: &str) -> Option<(&str, &str)> {
479 let colons: Vec<usize> = key.match_indices(':').map(|(i, _)| i).collect();
481
482 let last_colon = match colons.last() {
484 Some(&pos) => pos,
485 None => return None,
486 };
487 let attr_name = &key[last_colon + 1..];
488
489 let potential_states = &key[..last_colon];
492
493 let state_parts: Vec<&str> = potential_states.split(':').collect();
495
496 let all_valid_states = state_parts.iter().all(|&s| {
498 matches!(
499 s.trim().to_lowercase().as_str(),
500 "hover" | "focus" | "active" | "disabled"
501 )
502 });
503
504 if all_valid_states && !state_parts.is_empty() {
505 return Some((potential_states, attr_name));
507 }
508
509 None
510}
511
512fn parse_combined_states(prefix: &str) -> Option<Vec<WidgetState>> {
515 let parts: Vec<&str> = prefix.split(':').collect();
516 let mut states = Vec::new();
517
518 for part in parts {
519 if let Some(state) = WidgetState::from_prefix(part) {
520 if !states.contains(&state) {
522 states.push(state);
523 }
524 } else {
525 return None;
526 }
527 }
528
529 if states.is_empty() {
530 None
531 } else {
532 Some(states)
533 }
534}
535
536pub fn parse_theme_from_node(
538 node: roxmltree::Node,
539 _source: &str,
540) -> Result<Theme, crate::parser::error::ParseError> {
541 use crate::parser::error::{ParseError, ParseErrorKind};
542
543 let name = node
544 .attribute("name")
545 .map(|s| s.to_string())
546 .unwrap_or_else(|| "default".to_string());
547
548 let extends = node.attribute("extends").map(|s| s.to_string());
549
550 let mut palette_attrs = HashMap::new();
551 let mut typography_attrs = HashMap::new();
552 let mut spacing_unit = None;
553
554 for child in node.children() {
556 if child.node_type() != roxmltree::NodeType::Element {
557 continue;
558 }
559
560 let tag = child.tag_name().name();
561
562 if tag == "palette" {
563 for attr in child.attributes() {
564 palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
565 }
566 } else if tag == "typography" {
567 for attr in child.attributes() {
568 typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
569 }
570 } else if tag == "spacing" {
571 if let Some(unit) = child.attribute("unit") {
572 spacing_unit = unit.parse::<f32>().ok();
573 }
574 }
575 }
576
577 let theme = parse_theme(
579 name,
580 &palette_attrs,
581 &typography_attrs,
582 spacing_unit,
583 extends,
584 )
585 .map_err(|e| ParseError {
586 kind: ParseErrorKind::InvalidValue,
587 message: format!("Failed to parse theme: {}", e),
588 span: crate::ir::Span::default(),
589 suggestion: None,
590 })?;
591
592 Ok(theme)
593}
594
595pub fn parse_style_class_from_node(
597 node: roxmltree::Node,
598 _source: &str,
599) -> Result<StyleClass, crate::parser::error::ParseError> {
600 use crate::parser::error::{ParseError, ParseErrorKind};
601
602 let name = node
603 .attribute("name")
604 .map(|s| s.to_string())
605 .unwrap_or_default();
606
607 if name.is_empty() {
608 return Err(ParseError {
609 kind: ParseErrorKind::InvalidValue,
610 message: "Style class must have a name".to_string(),
611 span: crate::ir::Span::default(),
612 suggestion: None,
613 });
614 }
615
616 let mut base_attrs = HashMap::new();
618 let mut extends = Vec::new();
619 let mut state_variants_raw: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
620 let mut combined_state_variants_raw: HashMap<
621 crate::ir::theme::StateSelector,
622 HashMap<String, String>,
623 > = HashMap::new();
624 let mut layout = None;
625
626 for attr in node.attributes() {
627 let key = attr.name();
628 let value = attr.value();
629
630 if key == "extends" {
632 extends = value.split_whitespace().map(|s| s.to_string()).collect();
633 continue;
634 }
635
636 if let Some((prefix, attr_name)) = split_state_prefix(key) {
638 if let Some(states) = parse_combined_states(prefix) {
640 if states.len() == 1 {
641 let state_attr = state_variants_raw.entry(states[0]).or_default();
643 state_attr.insert(attr_name.to_string(), value.to_string());
644 } else {
645 let selector = crate::ir::theme::StateSelector::combined(states);
647 let state_attr = combined_state_variants_raw.entry(selector).or_default();
648 state_attr.insert(attr_name.to_string(), value.to_string());
649 }
650 } else {
651 return Err(ParseError {
652 kind: ParseErrorKind::InvalidValue,
653 message: format!("Invalid state prefix: {}", prefix),
654 span: crate::ir::Span::default(),
655 suggestion: None,
656 });
657 }
658 continue;
659 }
660
661 let layout_attr_names = [
663 "width",
664 "height",
665 "min_width",
666 "max_width",
667 "min_height",
668 "max_height",
669 "padding",
670 "spacing",
671 "align_items",
672 "justify_content",
673 "align_self",
674 "direction",
675 ];
676
677 if layout_attr_names.contains(&key) {
678 base_attrs.insert(key.to_string(), value.to_string());
679 continue;
680 }
681
682 base_attrs.insert(key.to_string(), value.to_string());
684 }
685
686 for child in node.children() {
688 if child.node_type() != roxmltree::NodeType::Element {
689 continue;
690 }
691
692 let tag = child.tag_name().name();
693
694 if let Some(state) = WidgetState::from_prefix(tag) {
696 let state_attr = state_variants_raw.entry(state).or_default();
697 for attr in child.attributes() {
698 state_attr.insert(attr.name().to_string(), attr.value().to_string());
699 }
700 continue;
701 }
702
703 if tag == "base" {
705 for attr in child.attributes() {
706 base_attrs.insert(attr.name().to_string(), attr.value().to_string());
707 }
708 continue;
709 }
710
711 if tag == "layout" {
713 let mut layout_attrs = HashMap::new();
714 for attr in child.attributes() {
715 layout_attrs.insert(attr.name().to_string(), attr.value().to_string());
716 }
717 layout = parse_layout_constraints(&layout_attrs).map_err(|e| ParseError {
718 kind: ParseErrorKind::InvalidValue,
719 message: format!("Failed to parse layout: {}", e),
720 span: crate::ir::Span::default(),
721 suggestion: None,
722 })?;
723 continue;
724 }
725 }
726
727 if base_attrs.keys().any(|k| {
729 matches!(
730 k.as_str(),
731 "width"
732 | "height"
733 | "min_width"
734 | "max_width"
735 | "min_height"
736 | "max_height"
737 | "padding"
738 | "spacing"
739 | "align_items"
740 | "justify_content"
741 | "align_self"
742 | "direction"
743 )
744 }) {
745 layout = parse_layout_constraints(&base_attrs).map_err(|e| ParseError {
746 kind: ParseErrorKind::InvalidValue,
747 message: format!("Failed to parse layout: {}", e),
748 span: crate::ir::Span::default(),
749 suggestion: None,
750 })?;
751
752 let layout_keys: Vec<String> = base_attrs
754 .keys()
755 .filter(|k| {
756 matches!(
757 k.as_str(),
758 "width"
759 | "height"
760 | "min_width"
761 | "max_width"
762 | "min_height"
763 | "max_height"
764 | "padding"
765 | "spacing"
766 | "align_items"
767 | "justify_content"
768 | "align_self"
769 | "direction"
770 )
771 })
772 .cloned()
773 .collect();
774
775 for key in layout_keys {
776 base_attrs.remove(&key);
777 }
778 }
779
780 let mut state_variants = HashMap::new();
782 for (state, state_attrs) in state_variants_raw {
783 let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
784 kind: ParseErrorKind::InvalidValue,
785 message: format!("Failed to parse state variant for {:?}: {}", state, e),
786 span: crate::ir::Span::default(),
787 suggestion: None,
788 })?;
789 state_variants.insert(state, style);
790 }
791
792 let mut combined_state_variants = HashMap::new();
794 for (selector, state_attrs) in combined_state_variants_raw {
795 let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
796 kind: ParseErrorKind::InvalidValue,
797 message: format!(
798 "Failed to parse combined state variant for {:?}: {}",
799 selector, e
800 ),
801 span: crate::ir::Span::default(),
802 suggestion: None,
803 })?;
804 combined_state_variants.insert(selector, style);
805 }
806
807 let class = parse_style_class(
809 name,
810 &base_attrs,
811 extends,
812 state_variants,
813 combined_state_variants,
814 layout,
815 )
816 .map_err(|e| ParseError {
817 kind: ParseErrorKind::InvalidValue,
818 message: format!("Failed to parse style class: {}", e),
819 span: crate::ir::Span::default(),
820 suggestion: None,
821 })?;
822
823 Ok(class)
824}