use crate::ir::layout::LayoutConstraints;
use crate::ir::style::{Color, StyleProperties};
use crate::ir::theme::{
FontWeight, SpacingScale, StyleClass, Theme, ThemeDocument, ThemeError, ThemeErrorKind,
ThemePalette, Typography, WidgetState,
};
use std::collections::HashMap;
pub fn parse_theme_document(xml: &str) -> Result<ThemeDocument, ThemeError> {
let doc = roxmltree::Document::parse(xml).map_err(|e| ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: format!("THEME_003: Failed to parse XML: {}", e),
})?;
let root = doc.root().first_child().ok_or_else(|| ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: "THEME_003: No root element found".to_string(),
})?;
if root.tag_name().name() != "dampen" {
return Err(ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: "THEME_003: Root element must be <dampen>".to_string(),
});
}
let mut themes = HashMap::new();
let mut default_theme = None;
let mut follow_system = true;
for child in root.children() {
if child.node_type() != roxmltree::NodeType::Element {
continue;
}
let tag = child.tag_name().name();
match tag {
"themes" => {
for grandchild in child.children() {
if grandchild.node_type() != roxmltree::NodeType::Element {
continue;
}
if grandchild.tag_name().name() == "theme" {
let theme = parse_theme_from_node_simple(grandchild)?;
if themes.contains_key(&theme.name) {
return Err(ThemeError {
kind: ThemeErrorKind::DuplicateThemeName,
message: format!(
"THEME_005: Duplicate theme name: '{}'",
theme.name
),
});
}
themes.insert(theme.name.clone(), theme);
}
}
}
"default_theme" => {
if let Some(name) = child.attribute("name") {
default_theme = Some(name.to_string());
}
}
"follow_system" => {
if let Some(enabled) = child.attribute("enabled") {
follow_system = enabled.parse::<bool>().unwrap_or(true);
}
}
_ => {}
}
}
let document = ThemeDocument {
themes,
default_theme,
follow_system,
};
document.validate()?;
Ok(document)
}
fn parse_theme_from_node_simple(node: roxmltree::Node) -> Result<Theme, ThemeError> {
let name = node
.attribute("name")
.map(|s| s.to_string())
.unwrap_or_else(|| "default".to_string());
let extends = node.attribute("extends").map(|s| s.to_string());
let mut palette_attrs = HashMap::new();
let mut typography_attrs = HashMap::new();
let mut spacing_unit = None;
for child in node.children() {
if child.node_type() != roxmltree::NodeType::Element {
continue;
}
let tag = child.tag_name().name();
match tag {
"palette" => {
for attr in child.attributes() {
palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
}
"typography" => {
for attr in child.attributes() {
typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
}
"spacing" => {
if let Some(unit) = child.attribute("unit") {
spacing_unit = unit.parse::<f32>().ok();
}
}
_ => {}
}
}
let palette = parse_palette(&palette_attrs).map_err(|e| ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: format!("THEME_003: Invalid palette: {}", e),
})?;
let typography = parse_typography(&typography_attrs).map_err(|e| ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: format!("THEME_003: Invalid typography: {}", e),
})?;
let spacing = SpacingScale { unit: spacing_unit };
spacing.validate().map_err(|e| ThemeError {
kind: ThemeErrorKind::MissingPaletteColor,
message: format!("THEME_003: Invalid spacing: {}", e),
})?;
let theme = Theme {
name,
palette,
typography,
spacing,
base_styles: HashMap::new(),
extends,
};
Ok(theme)
}
pub fn parse_theme(
name: String,
palette_attrs: &HashMap<String, String>,
typography_attrs: &HashMap<String, String>,
spacing_unit: Option<f32>,
extends: Option<String>,
) -> Result<Theme, String> {
let palette = parse_palette(palette_attrs)?;
let typography = parse_typography(typography_attrs)?;
let spacing = SpacingScale { unit: spacing_unit };
let theme = Theme {
name,
palette,
typography,
spacing,
base_styles: HashMap::new(),
extends: extends.clone(),
};
theme.validate(extends.is_some())?;
Ok(theme)
}
pub fn parse_palette(attrs: &HashMap<String, String>) -> Result<ThemePalette, String> {
let get_color = |key: &str| -> Result<Option<Color>, String> {
if let Some(value) = attrs.get(key) {
Ok(Some(Color::parse(value)?))
} else {
Ok(None)
}
};
Ok(ThemePalette {
primary: get_color("primary")?,
secondary: get_color("secondary")?,
success: get_color("success")?,
warning: get_color("warning")?,
danger: get_color("danger")?,
background: get_color("background")?,
surface: get_color("surface")?,
text: get_color("text")?,
text_secondary: get_color("text_secondary")?,
})
}
pub fn parse_typography(attrs: &HashMap<String, String>) -> Result<Typography, String> {
let font_family = attrs.get("font_family").cloned();
let font_size_base = if let Some(s) = attrs.get("font_size_base") {
Some(s.parse().map_err(|_| "Invalid font_size_base")?)
} else {
None
};
let font_size_small = if let Some(s) = attrs.get("font_size_small") {
Some(s.parse().map_err(|_| "Invalid font_size_small")?)
} else {
None
};
let font_size_large = if let Some(s) = attrs.get("font_size_large") {
Some(s.parse().map_err(|_| "Invalid font_size_large")?)
} else {
None
};
let font_weight = match attrs.get("font_weight") {
Some(w) => FontWeight::parse(w)?,
None => FontWeight::Normal,
};
let line_height = if let Some(s) = attrs.get("line_height") {
Some(s.parse().map_err(|_| "Invalid line_height")?)
} else {
None
};
Ok(Typography {
font_family,
font_size_base,
font_size_small,
font_size_large,
font_weight,
line_height,
})
}
pub fn parse_style_class(
name: String,
base_attrs: &HashMap<String, String>,
extends: Vec<String>,
state_variants: HashMap<WidgetState, StyleProperties>,
combined_state_variants: HashMap<crate::ir::theme::StateSelector, StyleProperties>,
layout: Option<LayoutConstraints>,
) -> Result<StyleClass, String> {
let style = parse_style_properties_from_attrs(base_attrs)?;
let class = StyleClass {
name,
style,
layout,
extends,
state_variants,
combined_state_variants,
};
Ok(class)
}
pub fn parse_style_properties_from_attrs(
attrs: &HashMap<String, String>,
) -> Result<StyleProperties, String> {
use crate::parser::style_parser::*;
let mut background = None;
let mut color = None;
let mut shadow = None;
let mut opacity = None;
let mut transform = None;
if let Some(value) = attrs.get("background") {
background = Some(parse_background_attr(value)?);
}
if let Some(value) = attrs.get("color") {
color = Some(parse_color_attr(value)?);
}
let border_width = attrs
.get("border_width")
.map(|v| parse_border_width(v))
.transpose()?;
let border_color = attrs
.get("border_color")
.map(|v| parse_border_color(v))
.transpose()?;
let border_radius = attrs
.get("border_radius")
.map(|v| parse_border_radius(v))
.transpose()?;
let border_style = attrs
.get("border_style")
.map(|v| parse_border_style(v))
.transpose()?;
let border = build_border(border_width, border_color, border_radius, border_style)?;
if let Some(value) = attrs.get("shadow") {
shadow = Some(parse_shadow_attr(value)?);
}
if let Some(value) = attrs.get("opacity") {
opacity = Some(parse_opacity(value)?);
}
if let Some(value) = attrs.get("transform") {
transform = Some(parse_transform(value)?);
}
build_style_properties(background, color, border, shadow, opacity, transform)
}
pub fn parse_layout_constraints(
attrs: &HashMap<String, String>,
) -> Result<Option<LayoutConstraints>, String> {
use crate::parser::style_parser::*;
let mut constraints = LayoutConstraints::default();
let mut has_any = false;
if let Some(value) = attrs.get("width") {
constraints.width = Some(parse_length_attr(value)?);
has_any = true;
}
if let Some(value) = attrs.get("height") {
constraints.height = Some(parse_length_attr(value)?);
has_any = true;
}
if let Some(value) = attrs.get("min_width") {
constraints.min_width = Some(parse_constraint(value)?);
has_any = true;
}
if let Some(value) = attrs.get("max_width") {
constraints.max_width = Some(parse_constraint(value)?);
has_any = true;
}
if let Some(value) = attrs.get("min_height") {
constraints.min_height = Some(parse_constraint(value)?);
has_any = true;
}
if let Some(value) = attrs.get("max_height") {
constraints.max_height = Some(parse_constraint(value)?);
has_any = true;
}
if let Some(value) = attrs.get("padding") {
constraints.padding = Some(parse_padding_attr(value)?);
has_any = true;
}
if let Some(value) = attrs.get("spacing") {
constraints.spacing = Some(parse_spacing(value)?);
has_any = true;
}
if let Some(value) = attrs.get("align_items") {
constraints.align_items = Some(parse_alignment(value)?);
has_any = true;
}
if let Some(value) = attrs.get("justify_content") {
constraints.justify_content = Some(parse_justification(value)?);
has_any = true;
}
if let Some(value) = attrs.get("align_self") {
constraints.align_self = Some(parse_alignment(value)?);
has_any = true;
}
if let Some(value) = attrs.get("direction") {
constraints.direction = Some(crate::ir::layout::Direction::parse(value)?);
has_any = true;
}
if has_any {
constraints.validate()?;
Ok(Some(constraints))
} else {
Ok(None)
}
}
pub type StateVariantMaps = (
HashMap<WidgetState, StyleProperties>,
HashMap<crate::ir::theme::StateSelector, StyleProperties>,
);
pub fn parse_state_variants(attrs: &HashMap<String, String>) -> Result<StateVariantMaps, String> {
use crate::ir::theme::StateSelector;
let mut single_variants: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
let mut combined_variants: HashMap<StateSelector, HashMap<String, String>> = HashMap::new();
for (key, value) in attrs {
if let Some((prefix, attr_name)) = split_state_prefix(key) {
if let Some(states) = parse_combined_states(prefix) {
if states.len() == 1 {
single_variants
.entry(states[0])
.or_default()
.insert(attr_name.to_string(), value.to_string());
} else {
let selector = StateSelector::combined(states);
combined_variants
.entry(selector)
.or_default()
.insert(attr_name.to_string(), value.to_string());
}
} else {
return Err(format!("Invalid state prefix: {}", prefix));
}
}
}
let mut single_result = HashMap::new();
for (state, state_attrs) in single_variants {
let style = parse_style_properties_from_attrs(&state_attrs)?;
single_result.insert(state, style);
}
let mut combined_result = HashMap::new();
for (selector, state_attrs) in combined_variants {
let style = parse_style_properties_from_attrs(&state_attrs)?;
combined_result.insert(selector, style);
}
Ok((single_result, combined_result))
}
fn split_state_prefix(key: &str) -> Option<(&str, &str)> {
let colons: Vec<usize> = key.match_indices(':').map(|(i, _)| i).collect();
let last_colon = match colons.last() {
Some(&pos) => pos,
None => return None,
};
let attr_name = &key[last_colon + 1..];
let potential_states = &key[..last_colon];
let state_parts: Vec<&str> = potential_states.split(':').collect();
let all_valid_states = state_parts.iter().all(|&s| {
matches!(
s.trim().to_lowercase().as_str(),
"hover" | "focus" | "active" | "disabled"
)
});
if all_valid_states && !state_parts.is_empty() {
return Some((potential_states, attr_name));
}
None
}
fn parse_combined_states(prefix: &str) -> Option<Vec<WidgetState>> {
let parts: Vec<&str> = prefix.split(':').collect();
let mut states = Vec::new();
for part in parts {
if let Some(state) = WidgetState::from_prefix(part) {
if !states.contains(&state) {
states.push(state);
}
} else {
return None;
}
}
if states.is_empty() {
None
} else {
Some(states)
}
}
pub fn parse_theme_from_node(
node: roxmltree::Node,
_source: &str,
) -> Result<Theme, crate::parser::error::ParseError> {
use crate::parser::error::{ParseError, ParseErrorKind};
let name = node
.attribute("name")
.map(|s| s.to_string())
.unwrap_or_else(|| "default".to_string());
let extends = node.attribute("extends").map(|s| s.to_string());
let mut palette_attrs = HashMap::new();
let mut typography_attrs = HashMap::new();
let mut spacing_unit = None;
for child in node.children() {
if child.node_type() != roxmltree::NodeType::Element {
continue;
}
let tag = child.tag_name().name();
if tag == "palette" {
for attr in child.attributes() {
palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
} else if tag == "typography" {
for attr in child.attributes() {
typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
} else if tag == "spacing"
&& let Some(unit) = child.attribute("unit")
{
spacing_unit = unit.parse::<f32>().ok();
}
}
let theme = parse_theme(
name,
&palette_attrs,
&typography_attrs,
spacing_unit,
extends,
)
.map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Failed to parse theme: {}", e),
span: crate::ir::Span::default(),
suggestion: None,
})?;
Ok(theme)
}
pub fn parse_style_class_from_node(
node: roxmltree::Node,
_source: &str,
) -> Result<StyleClass, crate::parser::error::ParseError> {
use crate::parser::error::{ParseError, ParseErrorKind};
let name = node
.attribute("name")
.map(|s| s.to_string())
.unwrap_or_default();
if name.is_empty() {
return Err(ParseError {
kind: ParseErrorKind::InvalidValue,
message: "Style class must have a name".to_string(),
span: crate::ir::Span::default(),
suggestion: None,
});
}
let mut base_attrs = HashMap::new();
let mut extends = Vec::new();
let mut state_variants_raw: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
let mut combined_state_variants_raw: HashMap<
crate::ir::theme::StateSelector,
HashMap<String, String>,
> = HashMap::new();
let mut layout = None;
for attr in node.attributes() {
let key = attr.name();
let value = attr.value();
if key == "extends" {
extends = value.split_whitespace().map(|s| s.to_string()).collect();
continue;
}
if let Some((prefix, attr_name)) = split_state_prefix(key) {
if let Some(states) = parse_combined_states(prefix) {
if states.len() == 1 {
let state_attr = state_variants_raw.entry(states[0]).or_default();
state_attr.insert(attr_name.to_string(), value.to_string());
} else {
let selector = crate::ir::theme::StateSelector::combined(states);
let state_attr = combined_state_variants_raw.entry(selector).or_default();
state_attr.insert(attr_name.to_string(), value.to_string());
}
} else {
return Err(ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Invalid state prefix: {}", prefix),
span: crate::ir::Span::default(),
suggestion: None,
});
}
continue;
}
let layout_attr_names = [
"width",
"height",
"min_width",
"max_width",
"min_height",
"max_height",
"padding",
"spacing",
"align_items",
"justify_content",
"align_self",
"direction",
];
if layout_attr_names.contains(&key) {
base_attrs.insert(key.to_string(), value.to_string());
continue;
}
base_attrs.insert(key.to_string(), value.to_string());
}
for child in node.children() {
if child.node_type() != roxmltree::NodeType::Element {
continue;
}
let tag = child.tag_name().name();
if let Some(state) = WidgetState::from_prefix(tag) {
let state_attr = state_variants_raw.entry(state).or_default();
for attr in child.attributes() {
state_attr.insert(attr.name().to_string(), attr.value().to_string());
}
continue;
}
if tag == "base" {
for attr in child.attributes() {
base_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
continue;
}
if tag == "layout" {
let mut layout_attrs = HashMap::new();
for attr in child.attributes() {
layout_attrs.insert(attr.name().to_string(), attr.value().to_string());
}
layout = parse_layout_constraints(&layout_attrs).map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Failed to parse layout: {}", e),
span: crate::ir::Span::default(),
suggestion: None,
})?;
continue;
}
}
if base_attrs.keys().any(|k| {
matches!(
k.as_str(),
"width"
| "height"
| "min_width"
| "max_width"
| "min_height"
| "max_height"
| "padding"
| "spacing"
| "align_items"
| "justify_content"
| "align_self"
| "direction"
)
}) {
layout = parse_layout_constraints(&base_attrs).map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Failed to parse layout: {}", e),
span: crate::ir::Span::default(),
suggestion: None,
})?;
let layout_keys: Vec<String> = base_attrs
.keys()
.filter(|k| {
matches!(
k.as_str(),
"width"
| "height"
| "min_width"
| "max_width"
| "min_height"
| "max_height"
| "padding"
| "spacing"
| "align_items"
| "justify_content"
| "align_self"
| "direction"
)
})
.cloned()
.collect();
for key in layout_keys {
base_attrs.remove(&key);
}
}
let mut state_variants = HashMap::new();
for (state, state_attrs) in state_variants_raw {
let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Failed to parse state variant for {:?}: {}", state, e),
span: crate::ir::Span::default(),
suggestion: None,
})?;
state_variants.insert(state, style);
}
let mut combined_state_variants = HashMap::new();
for (selector, state_attrs) in combined_state_variants_raw {
let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!(
"Failed to parse combined state variant for {:?}: {}",
selector, e
),
span: crate::ir::Span::default(),
suggestion: None,
})?;
combined_state_variants.insert(selector, style);
}
let class = parse_style_class(
name,
&base_attrs,
extends,
state_variants,
combined_state_variants,
layout,
)
.map_err(|e| ParseError {
kind: ParseErrorKind::InvalidValue,
message: format!("Failed to parse style class: {}", e),
span: crate::ir::Span::default(),
suggestion: None,
})?;
Ok(class)
}