dampen_core/parser/
theme_parser.rs

1//! Theme and style class parsing
2//!
3//! This module provides parsers for theme definitions and style classes.
4
5use 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
12/// Parse a theme definition from XML attributes
13pub 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
37/// Parse theme palette from attributes
38pub 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
59/// Parse typography from attributes
60pub 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
105/// Parse a style class definition
106pub 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
128/// Parse style properties from a map of attributes
129pub 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    // Parse background
141    if let Some(value) = attrs.get("background") {
142        background = Some(parse_background_attr(value)?);
143    }
144
145    // Parse color
146    if let Some(value) = attrs.get("color") {
147        color = Some(parse_color_attr(value)?);
148    }
149
150    // Parse border properties
151    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    // Parse shadow
171    if let Some(value) = attrs.get("shadow") {
172        shadow = Some(parse_shadow_attr(value)?);
173    }
174
175    // Parse opacity
176    if let Some(value) = attrs.get("opacity") {
177        opacity = Some(parse_opacity(value)?);
178    }
179
180    // Parse transform
181    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
188/// Parse layout constraints from attributes
189pub 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    // Parse sizing
198    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    // Parse constraints
209    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    // Parse layout
230    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    // Parse alignment
241    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    // Parse direction
257    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
270/// Parse state-prefixed attributes into state variants
271/// Type alias for state variant maps
272pub type StateVariantMaps = (
273    HashMap<WidgetState, StyleProperties>,
274    HashMap<crate::ir::theme::StateSelector, StyleProperties>,
275);
276
277/// Returns both single and combined state variants
278pub 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        // Check if key has state prefix
286        if let Some((prefix, attr_name)) = split_state_prefix(key) {
287            // Try to parse as combined states first
288            if let Some(states) = parse_combined_states(prefix) {
289                if states.len() == 1 {
290                    // Single state
291                    single_variants
292                        .entry(states[0])
293                        .or_default()
294                        .insert(attr_name.to_string(), value.to_string());
295                } else {
296                    // Combined states
297                    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    // Parse each single state's properties
310    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    // Parse each combined state's properties
317    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
326/// Split a state-prefixed attribute name
327/// e.g., "hover:background" -> Some(("hover", "background"))
328/// Also handles combined states: "hover:active:background" -> Some(("hover:active", "background"))
329fn split_state_prefix(key: &str) -> Option<(&str, &str)> {
330    // Find all colons
331    let colons: Vec<usize> = key.match_indices(':').map(|(i, _)| i).collect();
332
333    // The attribute name is after the last colon
334    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    // Check if what comes after the last colon looks like an attribute name
341    // (not a state name like "hover", "active", etc.)
342    let potential_states = &key[..last_colon];
343
344    // Split potential states by ':'
345    let state_parts: Vec<&str> = potential_states.split(':').collect();
346
347    // Verify all parts except the last are valid state names
348    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 the combined state prefix and attribute name
357        return Some((potential_states, attr_name));
358    }
359
360    None
361}
362
363/// Parse combined state prefix into individual states
364/// e.g., "hover:active" -> vec![WidgetState::Hover, WidgetState::Active]
365fn 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            // Avoid duplicates
372            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
387/// Parse a theme node from XML
388pub 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    // Parse child elements
404    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    // Validate required palette colors
427    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    // Parse using existing function
440    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
453/// Parse a style class node from XML
454pub 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    // Collect all attributes
475    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        // Check for extends
489        if key == "extends" {
490            extends = value.split_whitespace().map(|s| s.to_string()).collect();
491            continue;
492        }
493
494        // Check for state variants (prefixed attributes)
495        if let Some((prefix, attr_name)) = split_state_prefix(key) {
496            // Try to parse as combined states
497            if let Some(states) = parse_combined_states(prefix) {
498                if states.len() == 1 {
499                    // Single state
500                    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                    // Combined states
504                    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        // Check for layout attributes
520        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        // Regular style attribute
541        base_attrs.insert(key.to_string(), value.to_string());
542    }
543
544    // Parse child elements for state variants and base styles
545    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        // Handle state variant child elements
553        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        // Handle base element
562        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        // Handle layout child element
570        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    // Parse layout if any layout attributes present
586    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        // Remove layout attributes from base_attrs
611        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    // Parse state variants into StyleProperties
639    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    // Parse combined state variants into StyleProperties
651    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    // Parse using existing function
666    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}