1pub mod attribute_standard;
2pub mod error;
3pub mod gradient;
4pub mod lexer;
5pub mod style_parser;
6pub mod theme_parser;
7
8use crate::expr::tokenize_binding_expr;
9use crate::expr::{BindingExpr, Expr, LiteralExpr};
10use crate::ir::style::StyleProperties;
11use crate::ir::theme::WidgetState;
12use crate::ir::{
13 AttributeValue, Breakpoint, DampenDocument, EventBinding, EventKind, InterpolatedPart,
14 SchemaVersion, Span, WidgetKind, WidgetNode,
15};
16use crate::parser::error::{ParseError, ParseErrorKind};
17use roxmltree::{Document, Node, NodeType};
18use std::collections::HashMap;
19
20pub const MAX_SUPPORTED_VERSION: SchemaVersion = SchemaVersion { major: 1, minor: 0 };
25
26pub fn parse_version_string(version_str: &str, span: Span) -> Result<SchemaVersion, ParseError> {
45 let trimmed = version_str.trim();
46
47 if trimmed.is_empty() {
49 return Err(ParseError {
50 kind: ParseErrorKind::InvalidValue,
51 message: "Version attribute cannot be empty".to_string(),
52 span,
53 suggestion: Some("Use format: version=\"1.0\"".to_string()),
54 });
55 }
56
57 let parts: Vec<&str> = trimmed.split('.').collect();
59 if parts.len() != 2 {
60 return Err(ParseError {
61 kind: ParseErrorKind::InvalidValue,
62 message: format!(
63 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
64 trimmed
65 ),
66 span,
67 suggestion: Some("Use format: version=\"1.0\"".to_string()),
68 });
69 }
70
71 let major = parts[0].parse::<u16>().map_err(|_| ParseError {
73 kind: ParseErrorKind::InvalidValue,
74 message: format!(
75 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
76 trimmed
77 ),
78 span,
79 suggestion: Some("Use format: version=\"1.0\"".to_string()),
80 })?;
81
82 let minor = parts[1].parse::<u16>().map_err(|_| ParseError {
84 kind: ParseErrorKind::InvalidValue,
85 message: format!(
86 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
87 trimmed
88 ),
89 span,
90 suggestion: Some("Use format: version=\"1.0\"".to_string()),
91 })?;
92
93 Ok(SchemaVersion { major, minor })
94}
95
96pub fn validate_version_supported(version: &SchemaVersion, span: Span) -> Result<(), ParseError> {
108 if (version.major, version.minor) > (MAX_SUPPORTED_VERSION.major, MAX_SUPPORTED_VERSION.minor) {
109 return Err(ParseError {
110 kind: ParseErrorKind::UnsupportedVersion,
111 message: format!(
112 "Schema version {}.{} is not supported. Maximum supported version: {}.{}",
113 version.major,
114 version.minor,
115 MAX_SUPPORTED_VERSION.major,
116 MAX_SUPPORTED_VERSION.minor
117 ),
118 span,
119 suggestion: Some(format!(
120 "Upgrade dampen-core to support v{}.{}, or use version=\"{}.{}\"",
121 version.major,
122 version.minor,
123 MAX_SUPPORTED_VERSION.major,
124 MAX_SUPPORTED_VERSION.minor
125 )),
126 });
127 }
128 Ok(())
129}
130
131#[derive(Debug, Clone, PartialEq)]
135pub struct ValidationWarning {
136 pub widget_kind: WidgetKind,
138 pub declared_version: SchemaVersion,
140 pub required_version: SchemaVersion,
142 pub span: Span,
144}
145
146impl ValidationWarning {
147 pub fn format_message(&self) -> String {
149 format!(
150 "Widget '{}' requires schema v{}.{} but document declares v{}.{}",
151 widget_kind_name(&self.widget_kind),
152 self.required_version.major,
153 self.required_version.minor,
154 self.declared_version.major,
155 self.declared_version.minor
156 )
157 }
158
159 pub fn suggestion(&self) -> String {
161 format!(
162 "Update to <dampen version=\"{}.{}\"> or remove this widget",
163 self.required_version.major, self.required_version.minor
164 )
165 }
166}
167
168fn widget_kind_name(kind: &WidgetKind) -> String {
170 match kind {
171 WidgetKind::Column => "column".to_string(),
172 WidgetKind::Row => "row".to_string(),
173 WidgetKind::Container => "container".to_string(),
174 WidgetKind::Scrollable => "scrollable".to_string(),
175 WidgetKind::Stack => "stack".to_string(),
176 WidgetKind::Text => "text".to_string(),
177 WidgetKind::Image => "image".to_string(),
178 WidgetKind::Svg => "svg".to_string(),
179 WidgetKind::Button => "button".to_string(),
180 WidgetKind::TextInput => "text_input".to_string(),
181 WidgetKind::Checkbox => "checkbox".to_string(),
182 WidgetKind::Slider => "slider".to_string(),
183 WidgetKind::PickList => "pick_list".to_string(),
184 WidgetKind::Toggler => "toggler".to_string(),
185 WidgetKind::Space => "space".to_string(),
186 WidgetKind::Rule => "rule".to_string(),
187 WidgetKind::Radio => "radio".to_string(),
188 WidgetKind::ComboBox => "combobox".to_string(),
189 WidgetKind::ProgressBar => "progress_bar".to_string(),
190 WidgetKind::Tooltip => "tooltip".to_string(),
191 WidgetKind::Grid => "grid".to_string(),
192 WidgetKind::Canvas => "canvas".to_string(),
193 WidgetKind::Float => "float".to_string(),
194 WidgetKind::For => "for".to_string(),
195 WidgetKind::If => "if".to_string(),
196 WidgetKind::Custom(name) => name.clone(),
197 }
198}
199
200pub fn validate_widget_versions(document: &DampenDocument) -> Vec<ValidationWarning> {
225 let mut warnings = Vec::new();
226 validate_widget_tree(&document.root, &document.version, &mut warnings);
227 warnings
228}
229
230fn validate_widget_tree(
232 node: &WidgetNode,
233 doc_version: &SchemaVersion,
234 warnings: &mut Vec<ValidationWarning>,
235) {
236 let min_version = node.kind.minimum_version();
237
238 if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
240 warnings.push(ValidationWarning {
241 widget_kind: node.kind.clone(),
242 declared_version: *doc_version,
243 required_version: min_version,
244 span: node.span,
245 });
246 }
247
248 for child in &node.children {
250 validate_widget_tree(child, doc_version, warnings);
251 }
252}
253
254pub fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
286 let doc = Document::parse(xml).map_err(|e| ParseError {
288 kind: ParseErrorKind::XmlSyntax,
289 message: e.to_string(),
290 span: Span::new(0, 0, 1, 1),
291 suggestion: None,
292 })?;
293
294 let root = doc.root().first_child().ok_or_else(|| ParseError {
296 kind: ParseErrorKind::XmlSyntax,
297 message: "No root element found".to_string(),
298 span: Span::new(0, 0, 1, 1),
299 suggestion: None,
300 })?;
301
302 let root_tag = root.tag_name().name();
304
305 if root_tag == "dampen" {
306 parse_dampen_document(root, xml)
308 } else {
309 let root_widget = parse_node(root, xml)?;
312
313 Ok(DampenDocument {
314 version: SchemaVersion::default(),
315 root: root_widget,
316 themes: HashMap::new(),
317 style_classes: HashMap::new(),
318 global_theme: None,
319 follow_system: true,
320 })
321 }
322}
323
324fn validate_widget_attributes(
326 kind: &WidgetKind,
327 attributes: &std::collections::HashMap<String, AttributeValue>,
328 span: Span,
329) -> Result<(), ParseError> {
330 match kind {
331 WidgetKind::ComboBox | WidgetKind::PickList => {
332 require_non_empty_attribute(
333 kind,
334 "options",
335 attributes,
336 span,
337 "Add a comma-separated list: options=\"Option1,Option2\"",
338 )?;
339 }
340 WidgetKind::Canvas => {
341 require_attribute(
342 kind,
343 "width",
344 attributes,
345 span,
346 "Add width attribute: width=\"400\"",
347 )?;
348 require_attribute(
349 kind,
350 "height",
351 attributes,
352 span,
353 "Add height attribute: height=\"200\"",
354 )?;
355 require_attribute(
356 kind,
357 "program",
358 attributes,
359 span,
360 "Add program attribute: program=\"{{chart}}\"",
361 )?;
362
363 validate_numeric_range(kind, "width", attributes, span, 50..=4000)?;
364 validate_numeric_range(kind, "height", attributes, span, 50..=4000)?;
365 }
366 WidgetKind::Grid => {
367 require_attribute(
368 kind,
369 "columns",
370 attributes,
371 span,
372 "Add columns attribute: columns=\"5\"",
373 )?;
374 validate_numeric_range(kind, "columns", attributes, span, 1..=20)?;
375 }
376 WidgetKind::Tooltip => {
377 require_attribute(
378 kind,
379 "message",
380 attributes,
381 span,
382 "Add message attribute: message=\"Help text\"",
383 )?;
384 }
385 WidgetKind::For => {
386 require_attribute(
387 kind,
388 "each",
389 attributes,
390 span,
391 "Add each attribute: each=\"item\"",
392 )?;
393 require_attribute(
394 kind,
395 "in",
396 attributes,
397 span,
398 "Add in attribute: in=\"{items}\"",
399 )?;
400 }
401 _ => {}
402 }
403 Ok(())
404}
405
406fn require_attribute(
408 kind: &WidgetKind,
409 attr_name: &str,
410 attributes: &HashMap<String, AttributeValue>,
411 span: Span,
412 suggestion: &str,
413) -> Result<(), ParseError> {
414 if !attributes.contains_key(attr_name) {
415 return Err(ParseError {
416 kind: ParseErrorKind::MissingAttribute,
417 message: format!("{:?} widget requires '{}' attribute", kind, attr_name),
418 span,
419 suggestion: Some(suggestion.to_string()),
420 });
421 }
422 Ok(())
423}
424
425fn require_non_empty_attribute(
427 kind: &WidgetKind,
428 attr_name: &str,
429 attributes: &HashMap<String, AttributeValue>,
430 span: Span,
431 suggestion: &str,
432) -> Result<(), ParseError> {
433 match attributes.get(attr_name) {
434 Some(AttributeValue::Static(value)) if !value.trim().is_empty() => Ok(()),
435 _ => Err(ParseError {
436 kind: ParseErrorKind::MissingAttribute,
437 message: format!(
438 "{:?} widget requires '{}' attribute to be non-empty",
439 kind, attr_name
440 ),
441 span,
442 suggestion: Some(suggestion.to_string()),
443 }),
444 }
445}
446
447fn validate_numeric_range<T: PartialOrd + std::fmt::Display + std::str::FromStr>(
449 kind: &WidgetKind,
450 attr_name: &str,
451 attributes: &HashMap<String, AttributeValue>,
452 span: Span,
453 range: std::ops::RangeInclusive<T>,
454) -> Result<(), ParseError> {
455 if let Some(AttributeValue::Static(value_str)) = attributes.get(attr_name)
456 && let Ok(value) = value_str.parse::<T>()
457 && !range.contains(&value)
458 {
459 return Err(ParseError {
460 kind: ParseErrorKind::InvalidValue,
461 message: format!(
462 "{} for {:?} {} must be between {} and {}, found {}",
463 attr_name,
464 kind,
465 attr_name,
466 range.start(),
467 range.end(),
468 value
469 ),
470 span,
471 suggestion: Some(format!(
472 "Use {} value between {} and {}",
473 attr_name,
474 range.start(),
475 range.end()
476 )),
477 });
478 }
479 Ok(())
480}
481
482fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
484 if children.is_empty() {
485 return Err(ParseError {
486 kind: ParseErrorKind::InvalidValue,
487 message: "Tooltip widget must have exactly one child widget".to_string(),
488 span,
489 suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
490 });
491 }
492 if children.len() > 1 {
493 return Err(ParseError {
494 kind: ParseErrorKind::InvalidValue,
495 message: format!(
496 "Tooltip widget must have exactly one child, found {}",
497 children.len()
498 ),
499 span,
500 suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
501 });
502 }
503 Ok(())
504}
505
506fn validate_canvas_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
508 if !children.is_empty() {
509 return Err(ParseError {
510 kind: ParseErrorKind::InvalidValue,
511 message: format!(
512 "Canvas widget cannot have children, found {}",
513 children.len()
514 ),
515 span,
516 suggestion: Some("Canvas is a leaf widget - remove child elements".to_string()),
517 });
518 }
519 Ok(())
520}
521
522fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
524 if node.node_type() != NodeType::Element {
526 return Err(ParseError {
527 kind: ParseErrorKind::XmlSyntax,
528 message: "Expected element node".to_string(),
529 span: Span::new(0, 0, 1, 1),
530 suggestion: None,
531 });
532 }
533
534 let tag_name = node.tag_name().name();
536 let kind = match tag_name {
537 "column" => WidgetKind::Column,
538 "row" => WidgetKind::Row,
539 "container" => WidgetKind::Container,
540 "scrollable" => WidgetKind::Scrollable,
541 "stack" => WidgetKind::Stack,
542 "text" => WidgetKind::Text,
543 "image" => WidgetKind::Image,
544 "svg" => WidgetKind::Svg,
545 "button" => WidgetKind::Button,
546 "text_input" => WidgetKind::TextInput,
547 "checkbox" => WidgetKind::Checkbox,
548 "slider" => WidgetKind::Slider,
549 "pick_list" => WidgetKind::PickList,
550 "toggler" => WidgetKind::Toggler,
551 "space" => WidgetKind::Space,
552 "rule" => WidgetKind::Rule,
553 "radio" => WidgetKind::Radio,
554 "combobox" => WidgetKind::ComboBox,
555 "progress_bar" => WidgetKind::ProgressBar,
556 "tooltip" => WidgetKind::Tooltip,
557 "grid" => WidgetKind::Grid,
558 "canvas" => WidgetKind::Canvas,
559 "float" => WidgetKind::Float,
560 "for" => WidgetKind::For,
561 "if" => WidgetKind::If,
562 unknown => {
563 return Err(ParseError {
564 kind: ParseErrorKind::UnknownWidget,
565 message: format!("Unknown widget: {}", unknown),
566 span: get_span(node, source),
567 suggestion: Some(format!(
568 "Valid widgets are: {}",
569 WidgetKind::all_standard().join(", ")
570 )),
571 });
572 }
573 };
574
575 let mut attributes = std::collections::HashMap::new();
577 let mut breakpoint_attributes: HashMap<Breakpoint, HashMap<String, AttributeValue>> =
578 HashMap::new();
579 let mut inline_state_variants: HashMap<WidgetState, HashMap<String, AttributeValue>> =
580 HashMap::new();
581 let mut events = Vec::new();
582 let mut id = None;
583
584 for attr in node.attributes() {
585 let name = if let Some(ns) = attr.namespace() {
587 if ns.starts_with("urn:dampen:state") {
589 let prefix = node
591 .namespaces()
592 .find(|n| n.uri() == ns)
593 .and_then(|n| n.name())
594 .unwrap_or("");
595 format!("{}:{}", prefix, attr.name())
596 } else {
597 attr.name().to_string()
598 }
599 } else {
600 attr.name().to_string()
601 };
602 let value = attr.value();
603
604 if name == "id" {
606 id = Some(value.to_string());
607 continue;
608 }
609
610 if name.starts_with("on_") {
612 let event_kind = match name.as_str() {
613 "on_click" => Some(EventKind::Click),
614 "on_press" => Some(EventKind::Press),
615 "on_release" => Some(EventKind::Release),
616 "on_change" => Some(EventKind::Change),
617 "on_input" => Some(EventKind::Input),
618 "on_submit" => Some(EventKind::Submit),
619 "on_select" => Some(EventKind::Select),
620 "on_toggle" => Some(EventKind::Toggle),
621 "on_scroll" => Some(EventKind::Scroll),
622 _ => None,
623 };
624
625 if let Some(event) = event_kind {
626 let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
629 let handler = value[..colon_pos].to_string();
630 let param_str = &value[colon_pos + 1..];
631
632 if param_str.starts_with('\'')
634 && param_str.ends_with('\'')
635 && param_str.len() >= 2
636 {
637 let quoted_value = ¶m_str[1..param_str.len() - 1];
638 let expr = BindingExpr {
640 expr: Expr::Literal(LiteralExpr::String(quoted_value.to_string())),
641 span: Span::new(
642 colon_pos + 1,
643 colon_pos + 1 + param_str.len(),
644 1,
645 colon_pos as u32 + 1,
646 ),
647 };
648 (handler, Some(expr))
649 } else {
650 let param_clean = param_str.trim_matches('{').trim_matches('}');
652
653 match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
655 Ok(expr) => (handler, Some(expr)),
656 Err(_) => {
657 (value.to_string(), None)
659 }
660 }
661 }
662 } else {
663 (value.to_string(), None)
664 };
665
666 events.push(EventBinding {
667 event,
668 handler: handler_name,
669 param,
670 span: get_span(node, source),
671 });
672 continue;
673 }
674 }
675
676 if let Some((prefix, attr_name)) = name.split_once('-')
679 && let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix)
680 {
681 let attr_value = parse_attribute_value(value, get_span(node, source))?;
682 breakpoint_attributes
683 .entry(breakpoint)
684 .or_default()
685 .insert(attr_name.to_string(), attr_value);
686 continue;
687 }
688
689 if let Some((state_prefix, attr_name)) = name.split_once(':')
690 && let Some(state) = WidgetState::from_prefix(state_prefix)
691 {
692 let attr_value = parse_attribute_value(value, get_span(node, source))?;
693 inline_state_variants
694 .entry(state)
695 .or_default()
696 .insert(attr_name.to_string(), attr_value);
697 continue;
698 }
699 let attr_value = parse_attribute_value(value, get_span(node, source))?;
704 attributes.insert(name.to_string(), attr_value);
705 }
706
707 let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
709 class_attr
710 .split_whitespace()
711 .map(|s| s.to_string())
712 .collect()
713 } else {
714 Vec::new()
715 };
716
717 let theme_ref = attributes.get("theme").cloned();
719
720 let mut children = Vec::new();
722 for child in node.children() {
723 if child.node_type() == NodeType::Element {
724 children.push(parse_node(child, source)?);
725 }
726 }
727
728 if kind == WidgetKind::Tooltip {
730 validate_tooltip_children(&children, get_span(node, source))?;
731 }
732
733 if kind == WidgetKind::Canvas {
735 validate_canvas_children(&children, get_span(node, source))?;
736 }
737
738 let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
740 kind: ParseErrorKind::InvalidValue,
741 message: e,
742 span: get_span(node, source),
743 suggestion: None,
744 })?;
745 let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
746 kind: ParseErrorKind::InvalidValue,
747 message: e,
748 span: get_span(node, source),
749 suggestion: None,
750 })?;
751
752 let _attr_warnings = attribute_standard::normalize_attributes(&kind, &mut attributes);
754 validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
758
759 let mut final_state_variants: HashMap<WidgetState, StyleProperties> = HashMap::new();
762 for (state, state_attrs) in inline_state_variants {
763 if let Some(state_style) = parse_style_attributes(&state_attrs).map_err(|e| ParseError {
764 kind: ParseErrorKind::InvalidValue,
765 message: format!("Invalid style in {:?} state: {}", state, e),
766 span: get_span(node, source),
767 suggestion: None,
768 })? {
769 final_state_variants.insert(state, state_style);
770 }
771 }
772
773 Ok(WidgetNode {
774 kind,
775 id,
776 attributes,
777 events,
778 children,
779 span: get_span(node, source),
780 style,
781 layout,
782 theme_ref,
783 classes,
784 breakpoint_attributes,
785 inline_state_variants: final_state_variants,
786 })
787}
788
789fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
791 let mut themes = HashMap::new();
792 let mut style_classes = HashMap::new();
793 let mut root_widget = None;
794 let mut global_theme = None;
795 let mut follow_system = true;
796
797 let span = get_span(root, source);
799 let version = if let Some(version_attr) = root.attribute("version") {
800 let parsed = parse_version_string(version_attr, span)?;
801 validate_version_supported(&parsed, span)?;
802 parsed
803 } else {
804 SchemaVersion::default()
806 };
807
808 for child in root.children() {
810 if child.node_type() != NodeType::Element {
811 continue;
812 }
813
814 let tag_name = child.tag_name().name();
815
816 match tag_name {
817 "themes" => {
818 for theme_node in child.children() {
820 if theme_node.node_type() == NodeType::Element
821 && theme_node.tag_name().name() == "theme"
822 {
823 let theme =
824 crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
825 let name = theme_node
826 .attribute("name")
827 .map(|s| s.to_string())
828 .unwrap_or_else(|| "default".to_string());
829 themes.insert(name, theme);
830 }
831 }
832 }
833 "style_classes" | "classes" | "styles" => {
834 for class_node in child.children() {
836 if class_node.node_type() == NodeType::Element {
837 let tag = class_node.tag_name().name();
838 if tag == "class" || tag == "style" {
839 let class = crate::parser::theme_parser::parse_style_class_from_node(
840 class_node, source,
841 )?;
842 style_classes.insert(class.name.clone(), class);
843 }
844 }
845 }
846 }
847 "global_theme" | "default_theme" => {
848 if let Some(theme_name) = child.attribute("name") {
850 global_theme = Some(theme_name.to_string());
851 }
852 }
853 "follow_system" => {
854 if let Some(enabled) = child.attribute("enabled") {
855 follow_system = enabled.parse::<bool>().unwrap_or(true);
856 }
857 }
858 _ => {
859 if root_widget.is_some() {
861 return Err(ParseError {
862 kind: ParseErrorKind::XmlSyntax,
863 message: "Multiple root widgets found in <dampen>".to_string(),
864 span: get_span(child, source),
865 suggestion: Some("Only one root widget is allowed".to_string()),
866 });
867 }
868 root_widget = Some(parse_node(child, source)?);
869 }
870 }
871 }
872
873 let root_widget = if let Some(w) = root_widget {
875 w
876 } else if !themes.is_empty() || !style_classes.is_empty() {
877 WidgetNode::default()
879 } else {
880 return Err(ParseError {
881 kind: ParseErrorKind::XmlSyntax,
882 message: "No root widget found in <dampen>".to_string(),
883 span: get_span(root, source),
884 suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
885 });
886 };
887
888 Ok(DampenDocument {
889 version,
890 root: root_widget,
891 themes,
892 style_classes,
893 global_theme,
894 follow_system,
895 })
896}
897
898pub fn parse_comma_separated(value: &str) -> Vec<String> {
900 value
901 .split(',')
902 .map(|s| s.trim().to_string())
903 .filter(|s| !s.is_empty())
904 .collect()
905}
906
907pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
909where
910 T: std::str::FromStr + std::fmt::Display,
911{
912 let normalized = value.trim().to_lowercase();
913 for variant in valid_variants.iter() {
914 if variant.to_lowercase() == normalized {
915 return T::from_str(variant).map_err(|_| {
916 format!(
917 "Failed to parse '{}' as {}",
918 variant,
919 std::any::type_name::<T>()
920 )
921 });
922 }
923 }
924 Err(format!(
925 "Invalid value '{}'. Valid options: {}",
926 value,
927 valid_variants.join(", ")
928 ))
929}
930
931fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
933 if value.contains('{') && value.contains('}') {
935 let mut parts = Vec::new();
937 let mut remaining = value;
938
939 while let Some(start_pos) = remaining.find('{') {
940 if start_pos > 0 {
942 parts.push(InterpolatedPart::Literal(
943 remaining[..start_pos].to_string(),
944 ));
945 }
946
947 if let Some(end_pos) = remaining[start_pos..].find('}') {
949 let expr_start = start_pos + 1;
950 let expr_end = start_pos + end_pos;
951 let expr_str = &remaining[expr_start..expr_end];
952
953 let binding_expr = tokenize_binding_expr(
955 expr_str,
956 span.start + expr_start,
957 span.line,
958 span.column + expr_start as u32,
959 )
960 .map_err(|e| ParseError {
961 kind: ParseErrorKind::InvalidExpression,
962 message: format!("Invalid expression: {}", e),
963 span: Span::new(
964 span.start + expr_start,
965 span.start + expr_end,
966 span.line,
967 span.column + expr_start as u32,
968 ),
969 suggestion: None,
970 })?;
971
972 parts.push(InterpolatedPart::Binding(binding_expr));
973
974 remaining = &remaining[expr_end + 1..];
976 } else {
977 parts.push(InterpolatedPart::Literal(remaining.to_string()));
979 break;
980 }
981 }
982
983 if !remaining.is_empty() {
985 parts.push(InterpolatedPart::Literal(remaining.to_string()));
986 }
987
988 if parts.len() == 1 {
991 match &parts[0] {
992 InterpolatedPart::Binding(expr) => {
993 return Ok(AttributeValue::Binding(expr.clone()));
994 }
995 InterpolatedPart::Literal(lit) => {
996 return Ok(AttributeValue::Static(lit.clone()));
997 }
998 }
999 } else {
1000 return Ok(AttributeValue::Interpolated(parts));
1001 }
1002 }
1003
1004 Ok(AttributeValue::Static(value.to_string()))
1006}
1007
1008fn get_span(node: Node, source: &str) -> Span {
1010 let range = node.range();
1011
1012 let (line, col) = calculate_line_col(source, range.start);
1014
1015 Span {
1016 start: range.start,
1017 end: range.end,
1018 line,
1019 column: col,
1020 }
1021}
1022
1023fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
1027 if offset == 0 {
1028 return (1, 1);
1029 }
1030
1031 let mut line = 1;
1032 let mut col = 1;
1033
1034 for (i, c) in source.char_indices().take(offset.saturating_add(1)) {
1035 if i >= offset {
1036 break;
1037 }
1038 if c == '\n' {
1039 line += 1;
1040 col = 1;
1041 } else {
1042 col += 1;
1043 }
1044 }
1045
1046 (line, col)
1047}
1048
1049fn parse_layout_attributes(
1051 kind: &WidgetKind,
1052 attributes: &HashMap<String, AttributeValue>,
1053) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
1054 use crate::ir::layout::LayoutConstraints;
1055 use crate::parser::style_parser::{
1056 parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
1057 parse_length_attr, parse_padding_attr, parse_spacing,
1058 };
1059
1060 let mut layout = LayoutConstraints::default();
1061 let mut has_any = false;
1062
1063 if let Some(AttributeValue::Static(value)) = attributes.get("width") {
1065 layout.width = Some(parse_length_attr(value)?);
1066 has_any = true;
1067 }
1068
1069 if let Some(AttributeValue::Static(value)) = attributes.get("height") {
1071 layout.height = Some(parse_length_attr(value)?);
1072 has_any = true;
1073 }
1074
1075 if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
1077 layout.min_width = Some(parse_constraint(value)?);
1078 has_any = true;
1079 }
1080
1081 if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
1082 layout.max_width = Some(parse_constraint(value)?);
1083 has_any = true;
1084 }
1085
1086 if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
1087 layout.min_height = Some(parse_constraint(value)?);
1088 has_any = true;
1089 }
1090
1091 if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
1092 layout.max_height = Some(parse_constraint(value)?);
1093 has_any = true;
1094 }
1095
1096 if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
1098 layout.padding = Some(parse_padding_attr(value)?);
1099 has_any = true;
1100 }
1101
1102 if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
1104 layout.spacing = Some(parse_spacing(value)?);
1105 has_any = true;
1106 }
1107
1108 if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
1110 layout.align_items = Some(parse_alignment(value)?);
1111 has_any = true;
1112 }
1113
1114 if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
1115 layout.justify_content = Some(parse_justification(value)?);
1116 has_any = true;
1117 }
1118
1119 if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
1120 layout.align_self = Some(parse_alignment(value)?);
1121 has_any = true;
1122 }
1123
1124 if let Some(AttributeValue::Static(value)) = attributes.get("align_x") {
1126 layout.align_x = Some(parse_alignment(value)?);
1127 has_any = true;
1128 }
1129
1130 if let Some(AttributeValue::Static(value)) = attributes.get("align_y") {
1131 layout.align_y = Some(parse_alignment(value)?);
1132 has_any = true;
1133 }
1134
1135 if let Some(AttributeValue::Static(value)) = attributes.get("align") {
1137 let alignment = parse_alignment(value)?;
1138 layout.align_items = Some(alignment);
1139 layout.justify_content = Some(match alignment {
1140 crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
1141 crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
1142 crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
1143 crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
1144 });
1145 has_any = true;
1146 }
1147
1148 if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
1150 layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
1151 has_any = true;
1152 }
1153
1154 if !matches!(kind, WidgetKind::Tooltip)
1156 && let Some(AttributeValue::Static(value)) = attributes.get("position")
1157 {
1158 layout.position = Some(crate::ir::layout::Position::parse(value)?);
1159 has_any = true;
1160 }
1161
1162 if let Some(AttributeValue::Static(value)) = attributes.get("top") {
1164 layout.top = Some(parse_float_attr(value, "top")?);
1165 has_any = true;
1166 }
1167
1168 if let Some(AttributeValue::Static(value)) = attributes.get("right") {
1169 layout.right = Some(parse_float_attr(value, "right")?);
1170 has_any = true;
1171 }
1172
1173 if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
1174 layout.bottom = Some(parse_float_attr(value, "bottom")?);
1175 has_any = true;
1176 }
1177
1178 if let Some(AttributeValue::Static(value)) = attributes.get("left") {
1179 layout.left = Some(parse_float_attr(value, "left")?);
1180 has_any = true;
1181 }
1182
1183 if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
1185 layout.z_index = Some(parse_int_attr(value, "z_index")?);
1186 has_any = true;
1187 }
1188
1189 if has_any {
1191 layout
1192 .validate()
1193 .map_err(|e| format!("Layout validation failed: {}", e))?;
1194 Ok(Some(layout))
1195 } else {
1196 Ok(None)
1197 }
1198}
1199
1200fn parse_style_attributes(
1202 attributes: &HashMap<String, AttributeValue>,
1203) -> Result<Option<crate::ir::style::StyleProperties>, String> {
1204 use crate::parser::style_parser::{
1205 build_border, build_style_properties, parse_background_attr, parse_border_color,
1206 parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
1207 parse_opacity, parse_shadow_attr, parse_transform,
1208 };
1209
1210 let mut background = None;
1211 let mut color = None;
1212 let mut border_width = None;
1213 let mut border_color = None;
1214 let mut border_radius = None;
1215 let mut border_style = None;
1216 let mut shadow = None;
1217 let mut opacity = None;
1218 let mut transform = None;
1219 let mut has_any = false;
1220
1221 if let Some(AttributeValue::Static(value)) = attributes.get("background") {
1223 background = Some(parse_background_attr(value)?);
1224 has_any = true;
1225 }
1226
1227 if let Some(AttributeValue::Static(value)) = attributes.get("color") {
1229 color = Some(parse_color_attr(value)?);
1230 has_any = true;
1231 }
1232
1233 if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
1235 border_width = Some(parse_border_width(value)?);
1236 has_any = true;
1237 }
1238
1239 if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
1240 border_color = Some(parse_border_color(value)?);
1241 has_any = true;
1242 }
1243
1244 if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
1245 border_radius = Some(parse_border_radius(value)?);
1246 has_any = true;
1247 }
1248
1249 if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
1250 border_style = Some(parse_border_style(value)?);
1251 has_any = true;
1252 }
1253
1254 if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
1256 shadow = Some(parse_shadow_attr(value)?);
1257 has_any = true;
1258 }
1259
1260 if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
1262 opacity = Some(parse_opacity(value)?);
1263 has_any = true;
1264 }
1265
1266 if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
1268 transform = Some(parse_transform(value)?);
1269 has_any = true;
1270 }
1271
1272 if has_any {
1273 let border = build_border(border_width, border_color, border_radius, border_style)?;
1274 let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
1275 Ok(Some(style))
1276 } else {
1277 Ok(None)
1278 }
1279}
1280
1281pub fn validate_no_circular_dependencies(
1294 _file_path: &std::path::Path,
1295 _visited: &mut std::collections::HashSet<std::path::PathBuf>,
1296) -> Result<(), ParseError> {
1297 Ok(())
1298}
1299
1300#[cfg(test)]
1301mod circular_dependency_tests {
1302 use super::*;
1303 use std::collections::HashSet;
1304 use std::path::PathBuf;
1305
1306 #[test]
1307 fn test_no_circular_dependencies_without_includes() {
1308 let file_path = PathBuf::from("test.dampen");
1310 let mut visited = HashSet::new();
1311
1312 let result = validate_no_circular_dependencies(&file_path, &mut visited);
1313 assert!(
1314 result.is_ok(),
1315 "Single file should have no circular dependencies"
1316 );
1317 }
1318
1319 }
1325
1326#[cfg(test)]
1327mod inline_state_styles_tests {
1328 use super::*;
1329 use crate::ir::theme::WidgetState;
1330
1331 #[test]
1332 fn test_parse_single_state_attribute() {
1333 let xml = r##"
1336 <dampen version="1.0" xmlns:hover="urn:dampen:state:hover">
1337 <button label="Click" hover:background="#ff0000" />
1338 </dampen>
1339 "##;
1340
1341 let result = parse(xml);
1342 assert!(result.is_ok(), "Should parse valid XML with hover state");
1343
1344 let doc = result.unwrap();
1345 let button = &doc.root;
1346
1347 assert!(
1349 button
1350 .inline_state_variants
1351 .contains_key(&WidgetState::Hover),
1352 "Should have hover state variant"
1353 );
1354
1355 let hover_style = button
1356 .inline_state_variants
1357 .get(&WidgetState::Hover)
1358 .unwrap();
1359
1360 assert!(
1362 hover_style.background.is_some(),
1363 "Hover state should have background"
1364 );
1365 }
1366
1367 #[test]
1368 fn test_parse_multiple_state_attributes() {
1369 let xml = r##"
1372 <dampen version="1.0"
1373 xmlns:hover="urn:dampen:state:hover"
1374 xmlns:active="urn:dampen:state:active"
1375 xmlns:disabled="urn:dampen:state:disabled">
1376 <button
1377 label="Click"
1378 hover:background="#ff0000"
1379 active:background="#00ff00"
1380 disabled:opacity="0.5"
1381 />
1382 </dampen>
1383 "##;
1384
1385 let result = parse(xml);
1386 assert!(
1387 result.is_ok(),
1388 "Should parse valid XML with multiple states"
1389 );
1390
1391 let doc = result.unwrap();
1392 let button = &doc.root;
1393
1394 assert!(
1396 button
1397 .inline_state_variants
1398 .contains_key(&WidgetState::Hover),
1399 "Should have hover state"
1400 );
1401 assert!(
1402 button
1403 .inline_state_variants
1404 .contains_key(&WidgetState::Active),
1405 "Should have active state"
1406 );
1407 assert!(
1408 button
1409 .inline_state_variants
1410 .contains_key(&WidgetState::Disabled),
1411 "Should have disabled state"
1412 );
1413
1414 let hover_style = button
1416 .inline_state_variants
1417 .get(&WidgetState::Hover)
1418 .unwrap();
1419 assert!(
1420 hover_style.background.is_some(),
1421 "Hover state should have background"
1422 );
1423
1424 let active_style = button
1426 .inline_state_variants
1427 .get(&WidgetState::Active)
1428 .unwrap();
1429 assert!(
1430 active_style.background.is_some(),
1431 "Active state should have background"
1432 );
1433
1434 let disabled_style = button
1436 .inline_state_variants
1437 .get(&WidgetState::Disabled)
1438 .unwrap();
1439 assert!(
1440 disabled_style.opacity.is_some(),
1441 "Disabled state should have opacity"
1442 );
1443 }
1444
1445 #[test]
1446 fn test_parse_invalid_state_prefix() {
1447 let xml = r##"
1449 <dampen version="1.0" xmlns:unknown="urn:dampen:state:unknown">
1450 <button label="Click" unknown:background="#ff0000" />
1451 </dampen>
1452 "##;
1453
1454 let result = parse(xml);
1455 assert!(
1456 result.is_ok(),
1457 "Should parse with warning for invalid state"
1458 );
1459
1460 let doc = result.unwrap();
1461 let button = &doc.root;
1462
1463 assert!(
1465 button.inline_state_variants.is_empty(),
1466 "Should have no state variants for invalid prefix"
1467 );
1468
1469 assert!(
1471 button.attributes.contains_key("unknown:background"),
1472 "Invalid state prefix should be treated as regular attribute"
1473 );
1474 }
1475}