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::Custom(name) => name.clone(),
196 }
197}
198
199pub fn validate_widget_versions(document: &DampenDocument) -> Vec<ValidationWarning> {
224 let mut warnings = Vec::new();
225 validate_widget_tree(&document.root, &document.version, &mut warnings);
226 warnings
227}
228
229fn validate_widget_tree(
231 node: &WidgetNode,
232 doc_version: &SchemaVersion,
233 warnings: &mut Vec<ValidationWarning>,
234) {
235 let min_version = node.kind.minimum_version();
236
237 if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
239 warnings.push(ValidationWarning {
240 widget_kind: node.kind.clone(),
241 declared_version: *doc_version,
242 required_version: min_version,
243 span: node.span,
244 });
245 }
246
247 for child in &node.children {
249 validate_widget_tree(child, doc_version, warnings);
250 }
251}
252
253pub fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
285 let doc = Document::parse(xml).map_err(|e| ParseError {
287 kind: ParseErrorKind::XmlSyntax,
288 message: e.to_string(),
289 span: Span::new(0, 0, 1, 1),
290 suggestion: None,
291 })?;
292
293 let root = doc.root().first_child().ok_or_else(|| ParseError {
295 kind: ParseErrorKind::XmlSyntax,
296 message: "No root element found".to_string(),
297 span: Span::new(0, 0, 1, 1),
298 suggestion: None,
299 })?;
300
301 let root_tag = root.tag_name().name();
303
304 if root_tag == "dampen" {
305 parse_dampen_document(root, xml)
307 } else {
308 let root_widget = parse_node(root, xml)?;
311
312 Ok(DampenDocument {
313 version: SchemaVersion::default(),
314 root: root_widget,
315 themes: HashMap::new(),
316 style_classes: HashMap::new(),
317 global_theme: None,
318 })
319 }
320}
321
322fn validate_widget_attributes(
324 kind: &WidgetKind,
325 attributes: &std::collections::HashMap<String, AttributeValue>,
326 span: Span,
327) -> Result<(), ParseError> {
328 match kind {
329 WidgetKind::ComboBox | WidgetKind::PickList => {
330 require_non_empty_attribute(
331 kind,
332 "options",
333 attributes,
334 span,
335 "Add a comma-separated list: options=\"Option1,Option2\"",
336 )?;
337 }
338 WidgetKind::Canvas => {
339 require_attribute(
340 kind,
341 "width",
342 attributes,
343 span,
344 "Add width attribute: width=\"400\"",
345 )?;
346 require_attribute(
347 kind,
348 "height",
349 attributes,
350 span,
351 "Add height attribute: height=\"200\"",
352 )?;
353 require_attribute(
354 kind,
355 "program",
356 attributes,
357 span,
358 "Add program attribute: program=\"{{chart}}\"",
359 )?;
360
361 validate_numeric_range(kind, "width", attributes, span, 50..=4000)?;
362 validate_numeric_range(kind, "height", attributes, span, 50..=4000)?;
363 }
364 WidgetKind::Grid => {
365 require_attribute(
366 kind,
367 "columns",
368 attributes,
369 span,
370 "Add columns attribute: columns=\"5\"",
371 )?;
372 validate_numeric_range(kind, "columns", attributes, span, 1..=20)?;
373 }
374 WidgetKind::Tooltip => {
375 require_attribute(
376 kind,
377 "message",
378 attributes,
379 span,
380 "Add message attribute: message=\"Help text\"",
381 )?;
382 }
383 WidgetKind::For => {
384 require_attribute(
385 kind,
386 "each",
387 attributes,
388 span,
389 "Add each attribute: each=\"item\"",
390 )?;
391 require_attribute(
392 kind,
393 "in",
394 attributes,
395 span,
396 "Add in attribute: in=\"{items}\"",
397 )?;
398 }
399 _ => {}
400 }
401 Ok(())
402}
403
404fn require_attribute(
406 kind: &WidgetKind,
407 attr_name: &str,
408 attributes: &HashMap<String, AttributeValue>,
409 span: Span,
410 suggestion: &str,
411) -> Result<(), ParseError> {
412 if !attributes.contains_key(attr_name) {
413 return Err(ParseError {
414 kind: ParseErrorKind::MissingAttribute,
415 message: format!("{:?} widget requires '{}' attribute", kind, attr_name),
416 span,
417 suggestion: Some(suggestion.to_string()),
418 });
419 }
420 Ok(())
421}
422
423fn require_non_empty_attribute(
425 kind: &WidgetKind,
426 attr_name: &str,
427 attributes: &HashMap<String, AttributeValue>,
428 span: Span,
429 suggestion: &str,
430) -> Result<(), ParseError> {
431 match attributes.get(attr_name) {
432 Some(AttributeValue::Static(value)) if !value.trim().is_empty() => Ok(()),
433 _ => Err(ParseError {
434 kind: ParseErrorKind::MissingAttribute,
435 message: format!(
436 "{:?} widget requires '{}' attribute to be non-empty",
437 kind, attr_name
438 ),
439 span,
440 suggestion: Some(suggestion.to_string()),
441 }),
442 }
443}
444
445fn validate_numeric_range<T: PartialOrd + std::fmt::Display + std::str::FromStr>(
447 kind: &WidgetKind,
448 attr_name: &str,
449 attributes: &HashMap<String, AttributeValue>,
450 span: Span,
451 range: std::ops::RangeInclusive<T>,
452) -> Result<(), ParseError> {
453 if let Some(AttributeValue::Static(value_str)) = attributes.get(attr_name) {
454 if let Ok(value) = value_str.parse::<T>() {
455 if !range.contains(&value) {
456 return Err(ParseError {
457 kind: ParseErrorKind::InvalidValue,
458 message: format!(
459 "{} for {:?} {} must be between {} and {}, found {}",
460 attr_name,
461 kind,
462 attr_name,
463 range.start(),
464 range.end(),
465 value
466 ),
467 span,
468 suggestion: Some(format!(
469 "Use {} value between {} and {}",
470 attr_name,
471 range.start(),
472 range.end()
473 )),
474 });
475 }
476 }
477 }
478 Ok(())
479}
480
481fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
483 if children.is_empty() {
484 return Err(ParseError {
485 kind: ParseErrorKind::InvalidValue,
486 message: "Tooltip widget must have exactly one child widget".to_string(),
487 span,
488 suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
489 });
490 }
491 if children.len() > 1 {
492 return Err(ParseError {
493 kind: ParseErrorKind::InvalidValue,
494 message: format!(
495 "Tooltip widget must have exactly one child, found {}",
496 children.len()
497 ),
498 span,
499 suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
500 });
501 }
502 Ok(())
503}
504
505fn validate_canvas_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
507 if !children.is_empty() {
508 return Err(ParseError {
509 kind: ParseErrorKind::InvalidValue,
510 message: format!(
511 "Canvas widget cannot have children, found {}",
512 children.len()
513 ),
514 span,
515 suggestion: Some("Canvas is a leaf widget - remove child elements".to_string()),
516 });
517 }
518 Ok(())
519}
520
521fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
523 if node.node_type() != NodeType::Element {
525 return Err(ParseError {
526 kind: ParseErrorKind::XmlSyntax,
527 message: "Expected element node".to_string(),
528 span: Span::new(0, 0, 1, 1),
529 suggestion: None,
530 });
531 }
532
533 let tag_name = node.tag_name().name();
535 let kind = match tag_name {
536 "column" => WidgetKind::Column,
537 "row" => WidgetKind::Row,
538 "container" => WidgetKind::Container,
539 "scrollable" => WidgetKind::Scrollable,
540 "stack" => WidgetKind::Stack,
541 "text" => WidgetKind::Text,
542 "image" => WidgetKind::Image,
543 "svg" => WidgetKind::Svg,
544 "button" => WidgetKind::Button,
545 "text_input" => WidgetKind::TextInput,
546 "checkbox" => WidgetKind::Checkbox,
547 "slider" => WidgetKind::Slider,
548 "pick_list" => WidgetKind::PickList,
549 "toggler" => WidgetKind::Toggler,
550 "space" => WidgetKind::Space,
551 "rule" => WidgetKind::Rule,
552 "radio" => WidgetKind::Radio,
553 "combobox" => WidgetKind::ComboBox,
554 "progress_bar" => WidgetKind::ProgressBar,
555 "tooltip" => WidgetKind::Tooltip,
556 "grid" => WidgetKind::Grid,
557 "canvas" => WidgetKind::Canvas,
558 "float" => WidgetKind::Float,
559 "for" => WidgetKind::For,
560 _ => {
561 return Err(ParseError {
562 kind: ParseErrorKind::UnknownWidget,
563 message: format!("Unknown widget: <{}>", tag_name),
564 span: get_span(node, source),
565 suggestion: Some("Did you mean one of the standard widgets?".to_string()),
566 });
567 }
568 };
569
570 let mut attributes = std::collections::HashMap::new();
572 let mut breakpoint_attributes: HashMap<Breakpoint, HashMap<String, AttributeValue>> =
573 HashMap::new();
574 let mut inline_state_variants: HashMap<WidgetState, HashMap<String, AttributeValue>> =
575 HashMap::new();
576 let mut events = Vec::new();
577 let mut id = None;
578
579 for attr in node.attributes() {
580 let name = if let Some(ns) = attr.namespace() {
582 if ns.starts_with("urn:dampen:state") {
584 let prefix = node
586 .namespaces()
587 .find(|n| n.uri() == ns)
588 .and_then(|n| n.name())
589 .unwrap_or("");
590 format!("{}:{}", prefix, attr.name())
591 } else {
592 attr.name().to_string()
593 }
594 } else {
595 attr.name().to_string()
596 };
597 let value = attr.value();
598
599 if name == "id" {
601 id = Some(value.to_string());
602 continue;
603 }
604
605 if name.starts_with("on_") {
607 let event_kind = match name.as_str() {
608 "on_click" => Some(EventKind::Click),
609 "on_press" => Some(EventKind::Press),
610 "on_release" => Some(EventKind::Release),
611 "on_change" => Some(EventKind::Change),
612 "on_input" => Some(EventKind::Input),
613 "on_submit" => Some(EventKind::Submit),
614 "on_select" => Some(EventKind::Select),
615 "on_toggle" => Some(EventKind::Toggle),
616 "on_scroll" => Some(EventKind::Scroll),
617 _ => None,
618 };
619
620 if let Some(event) = event_kind {
621 let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
624 let handler = value[..colon_pos].to_string();
625 let param_str = &value[colon_pos + 1..];
626
627 if param_str.starts_with('\'')
629 && param_str.ends_with('\'')
630 && param_str.len() >= 2
631 {
632 let quoted_value = ¶m_str[1..param_str.len() - 1];
633 let expr = BindingExpr {
635 expr: Expr::Literal(LiteralExpr::String(quoted_value.to_string())),
636 span: Span::new(
637 colon_pos + 1,
638 colon_pos + 1 + param_str.len(),
639 1,
640 colon_pos as u32 + 1,
641 ),
642 };
643 (handler, Some(expr))
644 } else {
645 let param_clean = param_str.trim_matches('{').trim_matches('}');
647
648 match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
650 Ok(expr) => (handler, Some(expr)),
651 Err(_) => {
652 (value.to_string(), None)
654 }
655 }
656 }
657 } else {
658 (value.to_string(), None)
659 };
660
661 events.push(EventBinding {
662 event,
663 handler: handler_name,
664 param,
665 span: get_span(node, source),
666 });
667 continue;
668 }
669 }
670
671 if let Some((prefix, attr_name)) = name.split_once('-') {
674 if let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix) {
675 let attr_value = parse_attribute_value(value, get_span(node, source))?;
676 breakpoint_attributes
677 .entry(breakpoint)
678 .or_default()
679 .insert(attr_name.to_string(), attr_value);
680 continue;
681 }
682 }
683
684 if let Some((state_prefix, attr_name)) = name.split_once(':') {
685 if let Some(state) = WidgetState::from_prefix(state_prefix) {
686 let attr_value = parse_attribute_value(value, get_span(node, source))?;
687 inline_state_variants
688 .entry(state)
689 .or_default()
690 .insert(attr_name.to_string(), attr_value);
691 continue;
692 }
693 }
696
697 let attr_value = parse_attribute_value(value, get_span(node, source))?;
699 attributes.insert(name.to_string(), attr_value);
700 }
701
702 let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
704 class_attr
705 .split_whitespace()
706 .map(|s| s.to_string())
707 .collect()
708 } else {
709 Vec::new()
710 };
711
712 let theme_ref = attributes.get("theme").cloned();
714
715 let mut children = Vec::new();
717 for child in node.children() {
718 if child.node_type() == NodeType::Element {
719 children.push(parse_node(child, source)?);
720 }
721 }
722
723 if kind == WidgetKind::Tooltip {
725 validate_tooltip_children(&children, get_span(node, source))?;
726 }
727
728 if kind == WidgetKind::Canvas {
730 validate_canvas_children(&children, get_span(node, source))?;
731 }
732
733 let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
735 kind: ParseErrorKind::InvalidValue,
736 message: e,
737 span: get_span(node, source),
738 suggestion: None,
739 })?;
740 let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
741 kind: ParseErrorKind::InvalidValue,
742 message: e,
743 span: get_span(node, source),
744 suggestion: None,
745 })?;
746
747 let _attr_warnings = attribute_standard::normalize_attributes(&kind, &mut attributes);
749 validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
753
754 let mut final_state_variants: HashMap<WidgetState, StyleProperties> = HashMap::new();
757 for (state, state_attrs) in inline_state_variants {
758 if let Some(state_style) = parse_style_attributes(&state_attrs).map_err(|e| ParseError {
759 kind: ParseErrorKind::InvalidValue,
760 message: format!("Invalid style in {:?} state: {}", state, e),
761 span: get_span(node, source),
762 suggestion: None,
763 })? {
764 final_state_variants.insert(state, state_style);
765 }
766 }
767
768 Ok(WidgetNode {
769 kind,
770 id,
771 attributes,
772 events,
773 children,
774 span: get_span(node, source),
775 style,
776 layout,
777 theme_ref,
778 classes,
779 breakpoint_attributes,
780 inline_state_variants: final_state_variants,
781 })
782}
783
784fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
786 let mut themes = HashMap::new();
787 let mut style_classes = HashMap::new();
788 let mut root_widget = None;
789 let mut global_theme = None;
790
791 let span = get_span(root, source);
793 let version = if let Some(version_attr) = root.attribute("version") {
794 let parsed = parse_version_string(version_attr, span)?;
795 validate_version_supported(&parsed, span)?;
796 parsed
797 } else {
798 SchemaVersion::default()
800 };
801
802 for child in root.children() {
804 if child.node_type() != NodeType::Element {
805 continue;
806 }
807
808 let tag_name = child.tag_name().name();
809
810 match tag_name {
811 "themes" => {
812 for theme_node in child.children() {
814 if theme_node.node_type() == NodeType::Element
815 && theme_node.tag_name().name() == "theme"
816 {
817 let theme =
818 crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
819 let name = theme_node
820 .attribute("name")
821 .map(|s| s.to_string())
822 .unwrap_or_else(|| "default".to_string());
823 themes.insert(name, theme);
824 }
825 }
826 }
827 "style_classes" | "classes" | "styles" => {
828 for class_node in child.children() {
830 if class_node.node_type() == NodeType::Element {
831 let tag = class_node.tag_name().name();
832 if tag == "class" || tag == "style" {
833 let class = crate::parser::theme_parser::parse_style_class_from_node(
834 class_node, source,
835 )?;
836 style_classes.insert(class.name.clone(), class);
837 }
838 }
839 }
840 }
841 "global_theme" => {
842 if let Some(theme_name) = child.attribute("name") {
844 global_theme = Some(theme_name.to_string());
845 }
846 }
847 _ => {
848 if root_widget.is_some() {
850 return Err(ParseError {
851 kind: ParseErrorKind::XmlSyntax,
852 message: "Multiple root widgets found in <dampen>".to_string(),
853 span: get_span(child, source),
854 suggestion: Some("Only one root widget is allowed".to_string()),
855 });
856 }
857 root_widget = Some(parse_node(child, source)?);
858 }
859 }
860 }
861
862 let root_widget = root_widget.ok_or_else(|| ParseError {
864 kind: ParseErrorKind::XmlSyntax,
865 message: "No root widget found in <dampen>".to_string(),
866 span: get_span(root, source),
867 suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
868 })?;
869
870 Ok(DampenDocument {
871 version,
872 root: root_widget,
873 themes,
874 style_classes,
875 global_theme,
876 })
877}
878
879pub fn parse_comma_separated(value: &str) -> Vec<String> {
881 value
882 .split(',')
883 .map(|s| s.trim().to_string())
884 .filter(|s| !s.is_empty())
885 .collect()
886}
887
888pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
890where
891 T: std::str::FromStr + std::fmt::Display,
892{
893 let normalized = value.trim().to_lowercase();
894 for variant in valid_variants.iter() {
895 if variant.to_lowercase() == normalized {
896 return T::from_str(variant).map_err(|_| {
897 format!(
898 "Failed to parse '{}' as {}",
899 variant,
900 std::any::type_name::<T>()
901 )
902 });
903 }
904 }
905 Err(format!(
906 "Invalid value '{}'. Valid options: {}",
907 value,
908 valid_variants.join(", ")
909 ))
910}
911
912fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
914 if value.contains('{') && value.contains('}') {
916 let mut parts = Vec::new();
918 let mut remaining = value;
919
920 while let Some(start_pos) = remaining.find('{') {
921 if start_pos > 0 {
923 parts.push(InterpolatedPart::Literal(
924 remaining[..start_pos].to_string(),
925 ));
926 }
927
928 if let Some(end_pos) = remaining[start_pos..].find('}') {
930 let expr_start = start_pos + 1;
931 let expr_end = start_pos + end_pos;
932 let expr_str = &remaining[expr_start..expr_end];
933
934 let binding_expr = tokenize_binding_expr(
936 expr_str,
937 span.start + expr_start,
938 span.line,
939 span.column + expr_start as u32,
940 )
941 .map_err(|e| ParseError {
942 kind: ParseErrorKind::InvalidExpression,
943 message: format!("Invalid expression: {}", e),
944 span: Span::new(
945 span.start + expr_start,
946 span.start + expr_end,
947 span.line,
948 span.column + expr_start as u32,
949 ),
950 suggestion: None,
951 })?;
952
953 parts.push(InterpolatedPart::Binding(binding_expr));
954
955 remaining = &remaining[expr_end + 1..];
957 } else {
958 parts.push(InterpolatedPart::Literal(remaining.to_string()));
960 break;
961 }
962 }
963
964 if !remaining.is_empty() {
966 parts.push(InterpolatedPart::Literal(remaining.to_string()));
967 }
968
969 if parts.len() == 1 {
972 match &parts[0] {
973 InterpolatedPart::Binding(expr) => {
974 return Ok(AttributeValue::Binding(expr.clone()));
975 }
976 InterpolatedPart::Literal(lit) => {
977 return Ok(AttributeValue::Static(lit.clone()));
978 }
979 }
980 } else {
981 return Ok(AttributeValue::Interpolated(parts));
982 }
983 }
984
985 Ok(AttributeValue::Static(value.to_string()))
987}
988
989fn get_span(node: Node, source: &str) -> Span {
991 let range = node.range();
992
993 let (line, col) = calculate_line_col(source, range.start);
995
996 Span {
997 start: range.start,
998 end: range.end,
999 line,
1000 column: col,
1001 }
1002}
1003
1004fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
1008 if offset == 0 {
1009 return (1, 1);
1010 }
1011
1012 let mut line = 1;
1013 let mut col = 1;
1014
1015 for (i, c) in source.char_indices().take(offset.saturating_add(1)) {
1016 if i >= offset {
1017 break;
1018 }
1019 if c == '\n' {
1020 line += 1;
1021 col = 1;
1022 } else {
1023 col += 1;
1024 }
1025 }
1026
1027 (line, col)
1028}
1029
1030fn parse_layout_attributes(
1032 kind: &WidgetKind,
1033 attributes: &HashMap<String, AttributeValue>,
1034) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
1035 use crate::ir::layout::LayoutConstraints;
1036 use crate::parser::style_parser::{
1037 parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
1038 parse_length_attr, parse_padding_attr, parse_spacing,
1039 };
1040
1041 let mut layout = LayoutConstraints::default();
1042 let mut has_any = false;
1043
1044 if let Some(AttributeValue::Static(value)) = attributes.get("width") {
1046 layout.width = Some(parse_length_attr(value)?);
1047 has_any = true;
1048 }
1049
1050 if let Some(AttributeValue::Static(value)) = attributes.get("height") {
1052 layout.height = Some(parse_length_attr(value)?);
1053 has_any = true;
1054 }
1055
1056 if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
1058 layout.min_width = Some(parse_constraint(value)?);
1059 has_any = true;
1060 }
1061
1062 if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
1063 layout.max_width = Some(parse_constraint(value)?);
1064 has_any = true;
1065 }
1066
1067 if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
1068 layout.min_height = Some(parse_constraint(value)?);
1069 has_any = true;
1070 }
1071
1072 if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
1073 layout.max_height = Some(parse_constraint(value)?);
1074 has_any = true;
1075 }
1076
1077 if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
1079 layout.padding = Some(parse_padding_attr(value)?);
1080 has_any = true;
1081 }
1082
1083 if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
1085 layout.spacing = Some(parse_spacing(value)?);
1086 has_any = true;
1087 }
1088
1089 if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
1091 layout.align_items = Some(parse_alignment(value)?);
1092 has_any = true;
1093 }
1094
1095 if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
1096 layout.justify_content = Some(parse_justification(value)?);
1097 has_any = true;
1098 }
1099
1100 if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
1101 layout.align_self = Some(parse_alignment(value)?);
1102 has_any = true;
1103 }
1104
1105 if let Some(AttributeValue::Static(value)) = attributes.get("align_x") {
1107 layout.align_x = Some(parse_alignment(value)?);
1108 has_any = true;
1109 }
1110
1111 if let Some(AttributeValue::Static(value)) = attributes.get("align_y") {
1112 layout.align_y = Some(parse_alignment(value)?);
1113 has_any = true;
1114 }
1115
1116 if let Some(AttributeValue::Static(value)) = attributes.get("align") {
1118 let alignment = parse_alignment(value)?;
1119 layout.align_items = Some(alignment);
1120 layout.justify_content = Some(match alignment {
1121 crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
1122 crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
1123 crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
1124 crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
1125 });
1126 has_any = true;
1127 }
1128
1129 if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
1131 layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
1132 has_any = true;
1133 }
1134
1135 if !matches!(kind, WidgetKind::Tooltip) {
1137 if let Some(AttributeValue::Static(value)) = attributes.get("position") {
1138 layout.position = Some(crate::ir::layout::Position::parse(value)?);
1139 has_any = true;
1140 }
1141 }
1142
1143 if let Some(AttributeValue::Static(value)) = attributes.get("top") {
1145 layout.top = Some(parse_float_attr(value, "top")?);
1146 has_any = true;
1147 }
1148
1149 if let Some(AttributeValue::Static(value)) = attributes.get("right") {
1150 layout.right = Some(parse_float_attr(value, "right")?);
1151 has_any = true;
1152 }
1153
1154 if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
1155 layout.bottom = Some(parse_float_attr(value, "bottom")?);
1156 has_any = true;
1157 }
1158
1159 if let Some(AttributeValue::Static(value)) = attributes.get("left") {
1160 layout.left = Some(parse_float_attr(value, "left")?);
1161 has_any = true;
1162 }
1163
1164 if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
1166 layout.z_index = Some(parse_int_attr(value, "z_index")?);
1167 has_any = true;
1168 }
1169
1170 if has_any {
1172 layout
1173 .validate()
1174 .map_err(|e| format!("Layout validation failed: {}", e))?;
1175 Ok(Some(layout))
1176 } else {
1177 Ok(None)
1178 }
1179}
1180
1181fn parse_style_attributes(
1183 attributes: &HashMap<String, AttributeValue>,
1184) -> Result<Option<crate::ir::style::StyleProperties>, String> {
1185 use crate::parser::style_parser::{
1186 build_border, build_style_properties, parse_background_attr, parse_border_color,
1187 parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
1188 parse_opacity, parse_shadow_attr, parse_transform,
1189 };
1190
1191 let mut background = None;
1192 let mut color = None;
1193 let mut border_width = None;
1194 let mut border_color = None;
1195 let mut border_radius = None;
1196 let mut border_style = None;
1197 let mut shadow = None;
1198 let mut opacity = None;
1199 let mut transform = None;
1200 let mut has_any = false;
1201
1202 if let Some(AttributeValue::Static(value)) = attributes.get("background") {
1204 background = Some(parse_background_attr(value)?);
1205 has_any = true;
1206 }
1207
1208 if let Some(AttributeValue::Static(value)) = attributes.get("color") {
1210 color = Some(parse_color_attr(value)?);
1211 has_any = true;
1212 }
1213
1214 if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
1216 border_width = Some(parse_border_width(value)?);
1217 has_any = true;
1218 }
1219
1220 if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
1221 border_color = Some(parse_border_color(value)?);
1222 has_any = true;
1223 }
1224
1225 if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
1226 border_radius = Some(parse_border_radius(value)?);
1227 has_any = true;
1228 }
1229
1230 if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
1231 border_style = Some(parse_border_style(value)?);
1232 has_any = true;
1233 }
1234
1235 if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
1237 shadow = Some(parse_shadow_attr(value)?);
1238 has_any = true;
1239 }
1240
1241 if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
1243 opacity = Some(parse_opacity(value)?);
1244 has_any = true;
1245 }
1246
1247 if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
1249 transform = Some(parse_transform(value)?);
1250 has_any = true;
1251 }
1252
1253 if has_any {
1254 let border = build_border(border_width, border_color, border_radius, border_style)?;
1255 let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
1256 Ok(Some(style))
1257 } else {
1258 Ok(None)
1259 }
1260}
1261
1262pub fn validate_no_circular_dependencies(
1275 _file_path: &std::path::Path,
1276 _visited: &mut std::collections::HashSet<std::path::PathBuf>,
1277) -> Result<(), ParseError> {
1278 Ok(())
1279}
1280
1281#[cfg(test)]
1282mod circular_dependency_tests {
1283 use super::*;
1284 use std::collections::HashSet;
1285 use std::path::PathBuf;
1286
1287 #[test]
1288 fn test_no_circular_dependencies_without_includes() {
1289 let file_path = PathBuf::from("test.dampen");
1291 let mut visited = HashSet::new();
1292
1293 let result = validate_no_circular_dependencies(&file_path, &mut visited);
1294 assert!(
1295 result.is_ok(),
1296 "Single file should have no circular dependencies"
1297 );
1298 }
1299
1300 }
1306
1307#[cfg(test)]
1308mod inline_state_styles_tests {
1309 use super::*;
1310 use crate::ir::theme::WidgetState;
1311
1312 #[test]
1313 fn test_parse_single_state_attribute() {
1314 let xml = r##"
1317 <dampen version="1.0" xmlns:hover="urn:dampen:state:hover">
1318 <button label="Click" hover:background="#ff0000" />
1319 </dampen>
1320 "##;
1321
1322 let result = parse(xml);
1323 assert!(result.is_ok(), "Should parse valid XML with hover state");
1324
1325 let doc = result.unwrap();
1326 let button = &doc.root;
1327
1328 assert!(
1330 button
1331 .inline_state_variants
1332 .contains_key(&WidgetState::Hover),
1333 "Should have hover state variant"
1334 );
1335
1336 let hover_style = button
1337 .inline_state_variants
1338 .get(&WidgetState::Hover)
1339 .unwrap();
1340
1341 assert!(
1343 hover_style.background.is_some(),
1344 "Hover state should have background"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_parse_multiple_state_attributes() {
1350 let xml = r##"
1353 <dampen version="1.0"
1354 xmlns:hover="urn:dampen:state:hover"
1355 xmlns:active="urn:dampen:state:active"
1356 xmlns:disabled="urn:dampen:state:disabled">
1357 <button
1358 label="Click"
1359 hover:background="#ff0000"
1360 active:background="#00ff00"
1361 disabled:opacity="0.5"
1362 />
1363 </dampen>
1364 "##;
1365
1366 let result = parse(xml);
1367 assert!(
1368 result.is_ok(),
1369 "Should parse valid XML with multiple states"
1370 );
1371
1372 let doc = result.unwrap();
1373 let button = &doc.root;
1374
1375 assert!(
1377 button
1378 .inline_state_variants
1379 .contains_key(&WidgetState::Hover),
1380 "Should have hover state"
1381 );
1382 assert!(
1383 button
1384 .inline_state_variants
1385 .contains_key(&WidgetState::Active),
1386 "Should have active state"
1387 );
1388 assert!(
1389 button
1390 .inline_state_variants
1391 .contains_key(&WidgetState::Disabled),
1392 "Should have disabled state"
1393 );
1394
1395 let hover_style = button
1397 .inline_state_variants
1398 .get(&WidgetState::Hover)
1399 .unwrap();
1400 assert!(
1401 hover_style.background.is_some(),
1402 "Hover state should have background"
1403 );
1404
1405 let active_style = button
1407 .inline_state_variants
1408 .get(&WidgetState::Active)
1409 .unwrap();
1410 assert!(
1411 active_style.background.is_some(),
1412 "Active state should have background"
1413 );
1414
1415 let disabled_style = button
1417 .inline_state_variants
1418 .get(&WidgetState::Disabled)
1419 .unwrap();
1420 assert!(
1421 disabled_style.opacity.is_some(),
1422 "Disabled state should have opacity"
1423 );
1424 }
1425
1426 #[test]
1427 fn test_parse_invalid_state_prefix() {
1428 let xml = r##"
1430 <dampen version="1.0" xmlns:unknown="urn:dampen:state:unknown">
1431 <button label="Click" unknown:background="#ff0000" />
1432 </dampen>
1433 "##;
1434
1435 let result = parse(xml);
1436 assert!(
1437 result.is_ok(),
1438 "Should parse with warning for invalid state"
1439 );
1440
1441 let doc = result.unwrap();
1442 let button = &doc.root;
1443
1444 assert!(
1446 button.inline_state_variants.is_empty(),
1447 "Should have no state variants for invalid prefix"
1448 );
1449
1450 assert!(
1452 button.attributes.contains_key("unknown:background"),
1453 "Invalid state prefix should be treated as regular attribute"
1454 );
1455 }
1456}