1pub mod attribute_standard;
2pub mod canvas;
3pub mod color_validator;
4pub mod error;
5pub mod gradient;
6pub mod lexer;
7pub mod style_parser;
8pub mod theme_parser;
9
10use crate::expr::tokenize_binding_expr;
11use crate::expr::{BindingExpr, Expr, LiteralExpr};
12use crate::ir::style::StyleProperties;
13use crate::ir::theme::WidgetState;
14use crate::ir::{
15 AttributeValue, Breakpoint, DampenDocument, EventBinding, EventKind, InterpolatedPart,
16 SchemaVersion, Span, WidgetKind, WidgetNode,
17};
18use crate::parser::error::{ParseError, ParseErrorKind};
19use chrono::{NaiveDate, NaiveTime};
20use roxmltree::{Document, Node, NodeType};
21use std::collections::HashMap;
22
23pub const MAX_SUPPORTED_VERSION: SchemaVersion = SchemaVersion { major: 1, minor: 1 };
28
29pub fn parse_version_string(version_str: &str, span: Span) -> Result<SchemaVersion, ParseError> {
48 let trimmed = version_str.trim();
49
50 if trimmed.is_empty() {
52 return Err(ParseError {
53 kind: ParseErrorKind::InvalidValue,
54 message: "Version attribute cannot be empty".to_string(),
55 span,
56 suggestion: Some("Use format: version=\"1.0\"".to_string()),
57 });
58 }
59
60 let parts: Vec<&str> = trimmed.split('.').collect();
62 if parts.len() != 2 {
63 return Err(ParseError {
64 kind: ParseErrorKind::InvalidValue,
65 message: format!(
66 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
67 trimmed
68 ),
69 span,
70 suggestion: Some("Use format: version=\"1.0\"".to_string()),
71 });
72 }
73
74 let major = parts[0].parse::<u16>().map_err(|_| ParseError {
76 kind: ParseErrorKind::InvalidValue,
77 message: format!(
78 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
79 trimmed
80 ),
81 span,
82 suggestion: Some("Use format: version=\"1.0\"".to_string()),
83 })?;
84
85 let minor = parts[1].parse::<u16>().map_err(|_| ParseError {
87 kind: ParseErrorKind::InvalidValue,
88 message: format!(
89 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
90 trimmed
91 ),
92 span,
93 suggestion: Some("Use format: version=\"1.0\"".to_string()),
94 })?;
95
96 Ok(SchemaVersion { major, minor })
97}
98
99pub fn validate_version_supported(version: &SchemaVersion, span: Span) -> Result<(), ParseError> {
111 if (version.major, version.minor) > (MAX_SUPPORTED_VERSION.major, MAX_SUPPORTED_VERSION.minor) {
112 return Err(ParseError {
113 kind: ParseErrorKind::UnsupportedVersion,
114 message: format!(
115 "Schema version {}.{} is not supported. Maximum supported version: {}.{}",
116 version.major,
117 version.minor,
118 MAX_SUPPORTED_VERSION.major,
119 MAX_SUPPORTED_VERSION.minor
120 ),
121 span,
122 suggestion: Some(format!(
123 "Upgrade dampen-core to support v{}.{}, or use version=\"{}.{}\"",
124 version.major,
125 version.minor,
126 MAX_SUPPORTED_VERSION.major,
127 MAX_SUPPORTED_VERSION.minor
128 )),
129 });
130 }
131 Ok(())
132}
133
134#[derive(Debug, Clone, PartialEq)]
138pub struct ValidationWarning {
139 pub widget_kind: WidgetKind,
141 pub declared_version: SchemaVersion,
143 pub required_version: SchemaVersion,
145 pub span: Span,
147}
148
149impl ValidationWarning {
150 pub fn format_message(&self) -> String {
152 format!(
153 "Widget '{}' requires schema v{}.{} but document declares v{}.{}",
154 self.widget_kind,
155 self.required_version.major,
156 self.required_version.minor,
157 self.declared_version.major,
158 self.declared_version.minor
159 )
160 }
161
162 pub fn suggestion(&self) -> String {
164 format!(
165 "Update to <dampen version=\"{}.{}\"> or remove this widget",
166 self.required_version.major, self.required_version.minor
167 )
168 }
169}
170
171pub fn validate_widget_versions(document: &DampenDocument) -> Vec<ValidationWarning> {
197 let mut warnings = Vec::new();
198 validate_widget_tree(&document.root, &document.version, &mut warnings);
199 warnings
200}
201
202fn validate_widget_tree(
204 node: &WidgetNode,
205 doc_version: &SchemaVersion,
206 warnings: &mut Vec<ValidationWarning>,
207) {
208 let min_version = node.kind.minimum_version();
209
210 if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
212 warnings.push(ValidationWarning {
213 widget_kind: node.kind.clone(),
214 declared_version: *doc_version,
215 required_version: min_version,
216 span: node.span,
217 });
218 }
219
220 for child in &node.children {
222 validate_widget_tree(child, doc_version, warnings);
223 }
224}
225
226fn preprocess_xml(xml: &str) -> String {
231 let mut result = xml.to_string();
232 let states = ["hover", "active", "focus", "disabled"];
233 let prefixes = [' ', '\n', '\t', '\r'];
234
235 for state in states {
236 for prefix in prefixes {
237 let target = format!("{}{}:", prefix, state);
240 let sub = format!("{}{}_state_", prefix, state);
241 result = result.replace(&target, &sub);
242 }
243 }
244 result
245}
246
247pub fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
279 let processed_xml = preprocess_xml(xml);
281
282 let doc = Document::parse(&processed_xml).map_err(|e| ParseError {
284 kind: ParseErrorKind::XmlSyntax,
285 message: e.to_string(),
286 span: Span::new(0, 0, 1, 1),
287 suggestion: None,
288 })?;
289
290 let root = doc.root().first_child().ok_or_else(|| ParseError {
292 kind: ParseErrorKind::XmlSyntax,
293 message: "No root element found".to_string(),
294 span: Span::new(0, 0, 1, 1),
295 suggestion: None,
296 })?;
297
298 let root_tag = root.tag_name().name();
300
301 if root_tag == "dampen" {
302 parse_dampen_document(root, xml)
304 } else {
305 let root_widget = parse_node(root, xml)?;
308
309 validate_nesting_constraints(&root_widget, None)?;
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 follow_system: true,
319 })
320 }
321}
322
323fn validate_widget_attributes(
325 kind: &WidgetKind,
326 attributes: &std::collections::HashMap<String, AttributeValue>,
327 span: Span,
328) -> Result<(), ParseError> {
329 match kind {
330 WidgetKind::ComboBox | WidgetKind::PickList => {
331 require_non_empty_attribute(
332 kind,
333 "options",
334 attributes,
335 span,
336 "Add a comma-separated list: options=\"Option1,Option2\"",
337 )?;
338 }
339 WidgetKind::DatePicker => {
340 validate_date_format(kind, attributes, span)?;
341 validate_date_range(kind, attributes, span)?;
342 }
343 WidgetKind::TimePicker => {
344 validate_time_format(kind, attributes, span)?;
345 }
346 WidgetKind::Canvas => {
347 validate_numeric_range(kind, "width", attributes, span, 50..=4000)?;
350 validate_numeric_range(kind, "height", attributes, span, 50..=4000)?;
351
352 if attributes.contains_key("program") {
354 }
360 }
361 WidgetKind::Grid => {
362 require_attribute(
363 kind,
364 "columns",
365 attributes,
366 span,
367 "Add columns attribute: columns=\"5\"",
368 )?;
369 validate_numeric_range(kind, "columns", attributes, span, 1..=20)?;
370 }
371 WidgetKind::Tooltip => {
372 require_attribute(
373 kind,
374 "message",
375 attributes,
376 span,
377 "Add message attribute: message=\"Help text\"",
378 )?;
379 }
380 WidgetKind::For => {
381 require_attribute(
382 kind,
383 "each",
384 attributes,
385 span,
386 "Add each attribute: each=\"item\"",
387 )?;
388 require_attribute(
389 kind,
390 "in",
391 attributes,
392 span,
393 "Add in attribute: in=\"{items}\"",
394 )?;
395 }
396 WidgetKind::CanvasRect
397 | WidgetKind::CanvasCircle
398 | WidgetKind::CanvasLine
399 | WidgetKind::CanvasText
400 | WidgetKind::CanvasGroup => {
401 canvas::validate_shape_attributes(kind, attributes, span)?;
402 }
403 _ => {}
404 }
405 Ok(())
406}
407
408fn validate_date_format(
410 kind: &WidgetKind,
411 attributes: &HashMap<String, AttributeValue>,
412 span: Span,
413) -> Result<(), ParseError> {
414 if let Some(AttributeValue::Static(value)) = attributes.get("value") {
415 let format = if let Some(AttributeValue::Static(f)) = attributes.get("format") {
416 f.as_str()
417 } else {
418 "%Y-%m-%d"
419 };
420
421 if NaiveDate::parse_from_str(value, format).is_err() {
422 return Err(ParseError {
423 kind: ParseErrorKind::InvalidDateFormat,
424 message: format!(
425 "Invalid date format for {:?}: '{}' does not match format '{}'",
426 kind, value, format
427 ),
428 span,
429 suggestion: Some(
430 "Use ISO 8601 format (YYYY-MM-DD) or specify correct format attribute (e.g., format=\"%d/%m/%Y\")".to_string()
431 ),
432 });
433 }
434 }
435 Ok(())
436}
437
438fn validate_time_format(
440 kind: &WidgetKind,
441 attributes: &HashMap<String, AttributeValue>,
442 span: Span,
443) -> Result<(), ParseError> {
444 if let Some(AttributeValue::Static(value)) = attributes.get("value") {
445 let format = if let Some(AttributeValue::Static(f)) = attributes.get("format") {
446 f.as_str()
447 } else {
448 "%H:%M:%S"
449 };
450
451 if NaiveTime::parse_from_str(value, format).is_err() {
452 return Err(ParseError {
453 kind: ParseErrorKind::InvalidTimeFormat,
454 message: format!(
455 "Invalid time format for {:?}: '{}' does not match format '{}'",
456 kind, value, format
457 ),
458 span,
459 suggestion: Some(
460 "Use 24-hour format (HH:MM:SS) or specify correct format attribute (e.g., format=\"%I:%M %p\")".to_string()
461 ),
462 });
463 }
464 }
465 Ok(())
466}
467
468fn validate_date_range(
470 kind: &WidgetKind,
471 attributes: &HashMap<String, AttributeValue>,
472 span: Span,
473) -> Result<(), ParseError> {
474 let min_date = if let Some(AttributeValue::Static(m)) = attributes.get("min_date") {
475 NaiveDate::parse_from_str(m, "%Y-%m-%d").ok()
476 } else {
477 None
478 };
479
480 let max_date = if let Some(AttributeValue::Static(m)) = attributes.get("max_date") {
481 NaiveDate::parse_from_str(m, "%Y-%m-%d").ok()
482 } else {
483 None
484 };
485
486 if let (Some(min), Some(max)) = (min_date, max_date)
487 && min > max
488 {
489 return Err(ParseError {
490 kind: ParseErrorKind::InvalidDateRange,
491 message: format!(
492 "Invalid date range for {:?}: min_date ({}) is after max_date ({})",
493 kind, min, max
494 ),
495 span,
496 suggestion: Some("Ensure min_date is before or equal to max_date".to_string()),
497 });
498 }
499 Ok(())
500}
501
502fn require_attribute(
504 kind: &WidgetKind,
505 attr_name: &str,
506 attributes: &HashMap<String, AttributeValue>,
507 span: Span,
508 suggestion: &str,
509) -> Result<(), ParseError> {
510 if !attributes.contains_key(attr_name) {
511 return Err(ParseError {
512 kind: ParseErrorKind::MissingAttribute,
513 message: format!("{:?} widget requires '{}' attribute", kind, attr_name),
514 span,
515 suggestion: Some(suggestion.to_string()),
516 });
517 }
518 Ok(())
519}
520
521fn require_non_empty_attribute(
523 kind: &WidgetKind,
524 attr_name: &str,
525 attributes: &HashMap<String, AttributeValue>,
526 span: Span,
527 suggestion: &str,
528) -> Result<(), ParseError> {
529 match attributes.get(attr_name) {
530 Some(AttributeValue::Static(value)) if !value.trim().is_empty() => Ok(()),
531 _ => Err(ParseError {
532 kind: ParseErrorKind::MissingAttribute,
533 message: format!(
534 "{:?} widget requires '{}' attribute to be non-empty",
535 kind, attr_name
536 ),
537 span,
538 suggestion: Some(suggestion.to_string()),
539 }),
540 }
541}
542
543fn validate_numeric_range<T: PartialOrd + std::fmt::Display + std::str::FromStr>(
545 kind: &WidgetKind,
546 attr_name: &str,
547 attributes: &HashMap<String, AttributeValue>,
548 span: Span,
549 range: std::ops::RangeInclusive<T>,
550) -> Result<(), ParseError> {
551 if let Some(AttributeValue::Static(value_str)) = attributes.get(attr_name)
552 && let Ok(value) = value_str.parse::<T>()
553 && !range.contains(&value)
554 {
555 return Err(ParseError {
556 kind: ParseErrorKind::InvalidValue,
557 message: format!(
558 "{} for {:?} {} must be between {} and {}, found {}",
559 attr_name,
560 kind,
561 attr_name,
562 range.start(),
563 range.end(),
564 value
565 ),
566 span,
567 suggestion: Some(format!(
568 "Use {} value between {} and {}",
569 attr_name,
570 range.start(),
571 range.end()
572 )),
573 });
574 }
575 Ok(())
576}
577
578fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
580 if children.is_empty() {
581 return Err(ParseError {
582 kind: ParseErrorKind::InvalidValue,
583 message: "Tooltip widget must have exactly one child widget".to_string(),
584 span,
585 suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
586 });
587 }
588 if children.len() > 1 {
589 return Err(ParseError {
590 kind: ParseErrorKind::InvalidValue,
591 message: format!(
592 "Tooltip widget must have exactly one child, found {}",
593 children.len()
594 ),
595 span,
596 suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
597 });
598 }
599 Ok(())
600}
601
602fn validate_canvas_children(
604 attributes: &HashMap<String, AttributeValue>,
605 children: &[WidgetNode],
606 span: Span,
607) -> Result<(), ParseError> {
608 if attributes.contains_key("program") && !children.is_empty() {
610 return Err(ParseError {
616 kind: ParseErrorKind::InvalidValue,
617 message: "Canvas cannot have both a 'program' attribute and child shapes".to_string(),
618 span,
619 suggestion: Some("Remove the 'program' attribute to use declarative shapes, or remove children to use a custom program".to_string()),
620 });
621 }
622
623 canvas::validate_canvas_children(children, span)
624}
625
626fn validate_datetime_picker_children(
628 kind: &WidgetKind,
629 children: &[WidgetNode],
630 span: Span,
631) -> Result<(), ParseError> {
632 if children.is_empty() {
633 return Err(ParseError {
634 kind: ParseErrorKind::InvalidValue,
635 message: format!(
636 "{:?} widget must have exactly one child widget (the underlay)",
637 kind
638 ),
639 span,
640 suggestion: Some(format!(
641 "Wrap a single widget (e.g., <button>) in <{}>",
642 kind
643 )),
644 });
645 }
646 if children.len() > 1 {
647 return Err(ParseError {
648 kind: ParseErrorKind::InvalidValue,
649 message: format!(
650 "{:?} widget must have exactly one child, found {}",
651 kind,
652 children.len()
653 ),
654 span,
655 suggestion: Some(format!("Wrap only one widget in <{}>", kind)),
656 });
657 }
658 Ok(())
659}
660
661fn validate_context_menu_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
663 if children.len() != 2 {
664 return Err(ParseError {
665 kind: ParseErrorKind::InvalidValue,
666 message: "ContextMenu requires exactly 2 children: underlay and menu".to_string(),
667 span,
668 suggestion: Some(
669 "Add an underlay widget (1st child) and a <menu> element (2nd child)".to_string(),
670 ),
671 });
672 }
673 if children[1].kind != WidgetKind::Menu {
674 return Err(ParseError {
675 kind: ParseErrorKind::InvalidValue,
676 message: "Second child of ContextMenu must be <menu>".to_string(),
677 span: children[1].span,
678 suggestion: None,
679 });
680 }
681 Ok(())
682}
683
684fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
686 if node.node_type() != NodeType::Element {
688 return Err(ParseError {
689 kind: ParseErrorKind::XmlSyntax,
690 message: "Expected element node".to_string(),
691 span: Span::new(0, 0, 1, 1),
692 suggestion: None,
693 });
694 }
695
696 let tag_name = node.tag_name().name();
698 let kind = match tag_name {
699 "column" => WidgetKind::Column,
700 "row" => WidgetKind::Row,
701 "container" => WidgetKind::Container,
702 "scrollable" => WidgetKind::Scrollable,
703 "stack" => WidgetKind::Stack,
704 "text" => WidgetKind::Text,
705 "image" => WidgetKind::Image,
706 "svg" => WidgetKind::Svg,
707 "button" => WidgetKind::Button,
708 "text_input" => WidgetKind::TextInput,
709 "checkbox" => WidgetKind::Checkbox,
710 "slider" => WidgetKind::Slider,
711 "pick_list" => WidgetKind::PickList,
712 "toggler" => WidgetKind::Toggler,
713 "space" => WidgetKind::Space,
714 "rule" => WidgetKind::Rule,
715 "radio" => WidgetKind::Radio,
716 "combobox" => WidgetKind::ComboBox,
717 "progress_bar" => WidgetKind::ProgressBar,
718 "tooltip" => WidgetKind::Tooltip,
719 "grid" => WidgetKind::Grid,
720 "canvas" => WidgetKind::Canvas,
721 "rect" => WidgetKind::CanvasRect,
722 "circle" => WidgetKind::CanvasCircle,
723 "line" => WidgetKind::CanvasLine,
724 "canvas_text" => WidgetKind::CanvasText,
725 "group" => WidgetKind::CanvasGroup,
726 "date_picker" => WidgetKind::DatePicker,
727 "time_picker" => WidgetKind::TimePicker,
728 "color_picker" => WidgetKind::ColorPicker,
729 "menu" => WidgetKind::Menu,
730 "menu_item" => WidgetKind::MenuItem,
731 "menu_separator" => WidgetKind::MenuSeparator,
732 "context_menu" => WidgetKind::ContextMenu,
733 "float" => WidgetKind::Float,
734 "data_table" => WidgetKind::DataTable,
735 "data_column" => WidgetKind::DataColumn,
736 "tree_view" => WidgetKind::TreeView,
737 "tree_node" => WidgetKind::TreeNode,
738 "template" => WidgetKind::Custom("template".to_string()),
739 "for" => WidgetKind::For,
740 "if" => WidgetKind::If,
741 unknown => {
742 return Err(ParseError {
743 kind: ParseErrorKind::UnknownWidget,
744 message: format!("Unknown widget: {}", unknown),
745 span: get_span(node, source),
746 suggestion: Some(format!(
747 "Valid widgets are: {}",
748 WidgetKind::all_standard().join(", ")
749 )),
750 });
751 }
752 };
753
754 let mut attributes = std::collections::HashMap::new();
756 let mut breakpoint_attributes: HashMap<Breakpoint, HashMap<String, AttributeValue>> =
757 HashMap::new();
758 let mut inline_state_variants: HashMap<WidgetState, HashMap<String, AttributeValue>> =
759 HashMap::new();
760 let mut events = Vec::new();
761 let mut id = None;
762
763 for attr in node.attributes() {
765 if kind == WidgetKind::ColorPicker && attr.name() == "value" {
766 color_validator::validate_color_format(attr.value(), get_span(node, source))?;
767 }
768 }
769
770 for attr in node.attributes() {
771 let name = if let Some(ns) = attr.namespace() {
773 if ns.starts_with("urn:dampen:state") {
775 let prefix = node
777 .namespaces()
778 .find(|n| n.uri() == ns)
779 .and_then(|n| n.name())
780 .unwrap_or("");
781 format!("{}:{}", prefix, attr.name())
782 } else {
783 attr.name().to_string()
784 }
785 } else {
786 attr.name().to_string()
787 };
788 let value = attr.value();
789
790 if name == "id" {
792 id = Some(value.to_string());
793 continue;
794 }
795
796 if name.starts_with("on_") {
798 let event_kind = match name.as_str() {
799 "on_click" => Some(EventKind::CanvasClick), "on_press" => Some(EventKind::Press),
801 "on_release" => Some(EventKind::CanvasRelease),
802 "on_drag" => Some(EventKind::CanvasDrag),
803 "on_move" => Some(EventKind::CanvasMove),
804 "on_change" => Some(EventKind::Change),
805 "on_input" => Some(EventKind::Input),
806 "on_submit" => Some(EventKind::Submit),
807 "on_select" => Some(EventKind::Select),
808 "on_toggle" => Some(EventKind::Toggle),
809 "on_scroll" => Some(EventKind::Scroll),
810 "on_cancel" => Some(EventKind::Cancel),
811 "on_open" => Some(EventKind::Open),
812 "on_close" => Some(EventKind::Close),
813 "on_row_click" => Some(EventKind::RowClick),
814 _ => None,
815 };
816
817 let event_kind = if kind != WidgetKind::Canvas {
819 match name.as_str() {
820 "on_click" => Some(EventKind::Click),
821 "on_release" => Some(EventKind::Release),
822 _ => event_kind,
823 }
824 } else {
825 event_kind
826 };
827
828 if let Some(event) = event_kind {
829 let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
832 let handler = value[..colon_pos].to_string();
833 let param_str = &value[colon_pos + 1..];
834
835 if param_str.starts_with('\'')
837 && param_str.ends_with('\'')
838 && param_str.len() >= 2
839 {
840 let quoted_value = ¶m_str[1..param_str.len() - 1];
841 let expr = BindingExpr {
843 expr: Expr::Literal(LiteralExpr::String(quoted_value.to_string())),
844 span: Span::new(
845 colon_pos + 1,
846 colon_pos + 1 + param_str.len(),
847 1,
848 colon_pos as u32 + 1,
849 ),
850 };
851 (handler, Some(expr))
852 } else {
853 let param_clean = param_str.trim_matches('{').trim_matches('}');
855
856 match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
858 Ok(expr) => (handler, Some(expr)),
859 Err(_) => {
860 (value.to_string(), None)
862 }
863 }
864 }
865 } else {
866 (value.to_string(), None)
867 };
868
869 events.push(EventBinding {
870 event,
871 handler: handler_name,
872 param,
873 span: get_span(node, source),
874 });
875 continue;
876 }
877 }
878
879 if let Some((prefix, attr_name)) = name.split_once('-')
882 && let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix)
883 {
884 let attr_value = parse_attribute_value(value, get_span(node, source))?;
885 breakpoint_attributes
886 .entry(breakpoint)
887 .or_default()
888 .insert(attr_name.to_string(), attr_value);
889 continue;
890 }
891
892 if let Some((state_prefix, attr_name)) = name.split_once(':')
893 && let Some(state) = WidgetState::from_prefix(state_prefix)
894 {
895 let attr_value = parse_attribute_value(value, get_span(node, source))?;
896 inline_state_variants
897 .entry(state)
898 .or_default()
899 .insert(attr_name.to_string(), attr_value);
900 continue;
901 }
902
903 if let Some((state_prefix, attr_name)) = name.split_once("_state_")
905 && let Some(state) = WidgetState::from_prefix(state_prefix)
906 {
907 let attr_value = parse_attribute_value(value, get_span(node, source))?;
908 inline_state_variants
909 .entry(state)
910 .or_default()
911 .insert(attr_name.to_string(), attr_value);
912 continue;
913 }
914
915 let attr_value = parse_attribute_value(value, get_span(node, source))?;
920 attributes.insert(name.to_string(), attr_value);
921 }
922
923 let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
925 class_attr
926 .split_whitespace()
927 .map(|s| s.to_string())
928 .collect()
929 } else {
930 Vec::new()
931 };
932
933 let theme_ref = attributes.get("theme").cloned();
935
936 let mut children = Vec::new();
938 for child in node.children() {
939 if child.node_type() == NodeType::Element {
940 children.push(parse_node(child, source)?);
941 }
942 }
943
944 if kind == WidgetKind::Tooltip {
946 validate_tooltip_children(&children, get_span(node, source))?;
947 }
948
949 if kind == WidgetKind::Canvas {
951 validate_canvas_children(&attributes, &children, get_span(node, source))?;
952 }
953
954 if matches!(kind, WidgetKind::DatePicker | WidgetKind::TimePicker) {
956 validate_datetime_picker_children(&kind, &children, get_span(node, source))?;
957 }
958
959 if kind == WidgetKind::ContextMenu {
960 validate_context_menu_children(&children, get_span(node, source))?;
961 }
962
963 let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
965 kind: ParseErrorKind::InvalidValue,
966 message: e,
967 span: get_span(node, source),
968 suggestion: None,
969 })?;
970 let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
971 kind: ParseErrorKind::InvalidValue,
972 message: e,
973 span: get_span(node, source),
974 suggestion: None,
975 })?;
976
977 let _attr_warnings = attribute_standard::normalize_attributes(&kind, &mut attributes);
979 validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
983
984 let mut final_state_variants: HashMap<WidgetState, StyleProperties> = HashMap::new();
987 for (state, state_attrs) in inline_state_variants {
988 if let Some(state_style) = parse_style_attributes(&state_attrs).map_err(|e| ParseError {
989 kind: ParseErrorKind::InvalidValue,
990 message: format!("Invalid style in {:?} state: {}", state, e),
991 span: get_span(node, source),
992 suggestion: None,
993 })? {
994 final_state_variants.insert(state, state_style);
995 }
996 }
997
998 Ok(WidgetNode {
999 kind,
1000 id,
1001 attributes,
1002 events,
1003 children,
1004 span: get_span(node, source),
1005 style,
1006 layout,
1007 theme_ref,
1008 classes,
1009 breakpoint_attributes,
1010 inline_state_variants: final_state_variants,
1011 })
1012}
1013
1014fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
1016 let mut themes = HashMap::new();
1017 let mut style_classes = HashMap::new();
1018 let mut root_widget = None;
1019 let mut global_theme = None;
1020 let mut follow_system = true;
1021
1022 let span = get_span(root, source);
1024 let version = if let Some(version_attr) = root.attribute("version") {
1025 let parsed = parse_version_string(version_attr, span)?;
1026 validate_version_supported(&parsed, span)?;
1027 parsed
1028 } else {
1029 SchemaVersion::default()
1031 };
1032
1033 for child in root.children() {
1035 if child.node_type() != NodeType::Element {
1036 continue;
1037 }
1038
1039 let tag_name = child.tag_name().name();
1040
1041 match tag_name {
1042 "themes" => {
1043 for theme_node in child.children() {
1045 if theme_node.node_type() == NodeType::Element
1046 && theme_node.tag_name().name() == "theme"
1047 {
1048 let theme =
1049 crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
1050 let name = theme_node
1051 .attribute("name")
1052 .map(|s| s.to_string())
1053 .unwrap_or_else(|| "default".to_string());
1054 themes.insert(name, theme);
1055 }
1056 }
1057 }
1058 "style_classes" | "classes" | "styles" => {
1059 for class_node in child.children() {
1061 if class_node.node_type() == NodeType::Element {
1062 let tag = class_node.tag_name().name();
1063 if tag == "class" || tag == "style" {
1064 let class = crate::parser::theme_parser::parse_style_class_from_node(
1065 class_node, source,
1066 )?;
1067 style_classes.insert(class.name.clone(), class);
1068 }
1069 }
1070 }
1071 }
1072 "global_theme" | "default_theme" => {
1073 if let Some(theme_name) = child.attribute("name") {
1075 global_theme = Some(theme_name.to_string());
1076 }
1077 }
1078 "follow_system" => {
1079 if let Some(enabled) = child.attribute("enabled") {
1080 follow_system = enabled.parse::<bool>().unwrap_or(true);
1081 }
1082 }
1083 _ => {
1084 if root_widget.is_some() {
1086 return Err(ParseError {
1087 kind: ParseErrorKind::XmlSyntax,
1088 message: "Multiple root widgets found in <dampen>".to_string(),
1089 span: get_span(child, source),
1090 suggestion: Some("Only one root widget is allowed".to_string()),
1091 });
1092 }
1093 root_widget = Some(parse_node(child, source)?);
1094 }
1095 }
1096 }
1097
1098 let root_widget = if let Some(w) = root_widget {
1100 w
1101 } else if !themes.is_empty() || !style_classes.is_empty() {
1102 WidgetNode::default()
1104 } else {
1105 return Err(ParseError {
1106 kind: ParseErrorKind::XmlSyntax,
1107 message: "No root widget found in <dampen>".to_string(),
1108 span: get_span(root, source),
1109 suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
1110 });
1111 };
1112
1113 validate_widget_versions_strict(&root_widget, &version)?;
1116
1117 validate_nesting_constraints(&root_widget, None)?;
1119
1120 Ok(DampenDocument {
1121 version,
1122 root: root_widget,
1123 themes,
1124 style_classes,
1125 global_theme,
1126 follow_system,
1127 })
1128}
1129
1130fn validate_nesting_constraints(
1132 node: &WidgetNode,
1133 parent_kind: Option<&WidgetKind>,
1134) -> Result<(), ParseError> {
1135 if node.kind == WidgetKind::DataColumn && parent_kind != Some(&WidgetKind::DataTable) {
1137 return Err(ParseError {
1138 kind: ParseErrorKind::InvalidChild,
1139 message: "DataColumn must be a direct child of DataTable".to_string(),
1140 span: node.span,
1141 suggestion: Some("Wrap this column in a <data_table>".to_string()),
1142 });
1143 }
1144
1145 for child in &node.children {
1147 validate_nesting_constraints(child, Some(&node.kind))?;
1148 }
1149
1150 Ok(())
1151}
1152
1153fn validate_widget_versions_strict(
1155 node: &WidgetNode,
1156 doc_version: &SchemaVersion,
1157) -> Result<(), ParseError> {
1158 let min_version = node.kind.minimum_version();
1159
1160 if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
1161 return Err(ParseError {
1162 kind: ParseErrorKind::UnsupportedVersion,
1163 message: format!(
1164 "Widget '{}' requires schema v{}.{} but document declares v{}.{}",
1165 node.kind,
1166 min_version.major,
1167 min_version.minor,
1168 doc_version.major,
1169 doc_version.minor
1170 ),
1171 span: node.span,
1172 suggestion: Some(format!(
1173 "Update to <dampen version=\"{}.{}\"> or remove this widget",
1174 min_version.major, min_version.minor
1175 )),
1176 });
1177 }
1178
1179 for child in &node.children {
1180 validate_widget_versions_strict(child, doc_version)?;
1181 }
1182
1183 Ok(())
1184}
1185
1186pub fn parse_comma_separated(value: &str) -> Vec<String> {
1188 value
1189 .split(',')
1190 .map(|s| s.trim().to_string())
1191 .filter(|s| !s.is_empty())
1192 .collect()
1193}
1194
1195pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
1197where
1198 T: std::str::FromStr + std::fmt::Display,
1199{
1200 let normalized = value.trim().to_lowercase();
1201 for variant in valid_variants.iter() {
1202 if variant.to_lowercase() == normalized {
1203 return T::from_str(variant).map_err(|_| {
1204 format!(
1205 "Failed to parse '{}' as {}",
1206 variant,
1207 std::any::type_name::<T>()
1208 )
1209 });
1210 }
1211 }
1212 Err(format!(
1213 "Invalid value '{}'. Valid options: {}",
1214 value,
1215 valid_variants.join(", ")
1216 ))
1217}
1218
1219fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
1221 if value.contains('{') && value.contains('}') {
1223 let mut parts = Vec::new();
1225 let mut remaining = value;
1226
1227 while let Some(start_pos) = remaining.find('{') {
1228 if start_pos > 0 {
1230 parts.push(InterpolatedPart::Literal(
1231 remaining[..start_pos].to_string(),
1232 ));
1233 }
1234
1235 if let Some(end_pos) = remaining[start_pos..].find('}') {
1237 let expr_start = start_pos + 1;
1238 let expr_end = start_pos + end_pos;
1239 let expr_str = &remaining[expr_start..expr_end];
1240
1241 let binding_expr = tokenize_binding_expr(
1243 expr_str,
1244 span.start + expr_start,
1245 span.line,
1246 span.column + expr_start as u32,
1247 )
1248 .map_err(|e| ParseError {
1249 kind: ParseErrorKind::InvalidExpression,
1250 message: format!("Invalid expression: {}", e),
1251 span: Span::new(
1252 span.start + expr_start,
1253 span.start + expr_end,
1254 span.line,
1255 span.column + expr_start as u32,
1256 ),
1257 suggestion: None,
1258 })?;
1259
1260 parts.push(InterpolatedPart::Binding(binding_expr));
1261
1262 remaining = &remaining[expr_end + 1..];
1264 } else {
1265 parts.push(InterpolatedPart::Literal(remaining.to_string()));
1267 break;
1268 }
1269 }
1270
1271 if !remaining.is_empty() {
1273 parts.push(InterpolatedPart::Literal(remaining.to_string()));
1274 }
1275
1276 if parts.len() == 1 {
1279 match &parts[0] {
1280 InterpolatedPart::Binding(expr) => {
1281 return Ok(AttributeValue::Binding(expr.clone()));
1282 }
1283 InterpolatedPart::Literal(lit) => {
1284 return Ok(AttributeValue::Static(lit.clone()));
1285 }
1286 }
1287 } else {
1288 return Ok(AttributeValue::Interpolated(parts));
1289 }
1290 }
1291
1292 Ok(AttributeValue::Static(value.to_string()))
1294}
1295
1296fn get_span(node: Node, source: &str) -> Span {
1298 let range = node.range();
1299
1300 let (line, col) = calculate_line_col(source, range.start);
1302
1303 Span {
1304 start: range.start,
1305 end: range.end,
1306 line,
1307 column: col,
1308 }
1309}
1310
1311fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
1315 if offset == 0 {
1316 return (1, 1);
1317 }
1318
1319 let mut line = 1;
1320 let mut col = 1;
1321
1322 for (i, c) in source.char_indices().take(offset.saturating_add(1)) {
1323 if i >= offset {
1324 break;
1325 }
1326 if c == '\n' {
1327 line += 1;
1328 col = 1;
1329 } else {
1330 col += 1;
1331 }
1332 }
1333
1334 (line, col)
1335}
1336
1337fn parse_layout_attributes(
1339 kind: &WidgetKind,
1340 attributes: &HashMap<String, AttributeValue>,
1341) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
1342 use crate::ir::layout::LayoutConstraints;
1343 use crate::parser::style_parser::{
1344 parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
1345 parse_length_attr, parse_padding_attr, parse_spacing,
1346 };
1347
1348 let mut layout = LayoutConstraints::default();
1349 let mut has_any = false;
1350
1351 if let Some(AttributeValue::Static(value)) = attributes.get("width") {
1353 layout.width = Some(parse_length_attr(value)?);
1354 has_any = true;
1355 }
1356
1357 if let Some(AttributeValue::Static(value)) = attributes.get("height") {
1359 layout.height = Some(parse_length_attr(value)?);
1360 has_any = true;
1361 }
1362
1363 if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
1365 layout.min_width = Some(parse_constraint(value)?);
1366 has_any = true;
1367 }
1368
1369 if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
1370 layout.max_width = Some(parse_constraint(value)?);
1371 has_any = true;
1372 }
1373
1374 if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
1375 layout.min_height = Some(parse_constraint(value)?);
1376 has_any = true;
1377 }
1378
1379 if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
1380 layout.max_height = Some(parse_constraint(value)?);
1381 has_any = true;
1382 }
1383
1384 if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
1386 layout.padding = Some(parse_padding_attr(value)?);
1387 has_any = true;
1388 }
1389
1390 if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
1392 layout.spacing = Some(parse_spacing(value)?);
1393 has_any = true;
1394 }
1395
1396 if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
1398 layout.align_items = Some(parse_alignment(value)?);
1399 has_any = true;
1400 }
1401
1402 if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
1403 layout.justify_content = Some(parse_justification(value)?);
1404 has_any = true;
1405 }
1406
1407 if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
1408 layout.align_self = Some(parse_alignment(value)?);
1409 has_any = true;
1410 }
1411
1412 if let Some(AttributeValue::Static(value)) = attributes.get("align_x") {
1414 layout.align_x = Some(parse_alignment(value)?);
1415 has_any = true;
1416 }
1417
1418 if let Some(AttributeValue::Static(value)) = attributes.get("align_y") {
1419 layout.align_y = Some(parse_alignment(value)?);
1420 has_any = true;
1421 }
1422
1423 if let Some(AttributeValue::Static(value)) = attributes.get("align") {
1425 let alignment = parse_alignment(value)?;
1426 layout.align_items = Some(alignment);
1427 layout.justify_content = Some(match alignment {
1428 crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
1429 crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
1430 crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
1431 crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
1432 });
1433 has_any = true;
1434 }
1435
1436 if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
1438 layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
1439 has_any = true;
1440 }
1441
1442 if !matches!(kind, WidgetKind::Tooltip)
1444 && let Some(AttributeValue::Static(value)) = attributes.get("position")
1445 {
1446 layout.position = Some(crate::ir::layout::Position::parse(value)?);
1447 has_any = true;
1448 }
1449
1450 if let Some(AttributeValue::Static(value)) = attributes.get("top") {
1452 layout.top = Some(parse_float_attr(value, "top")?);
1453 has_any = true;
1454 }
1455
1456 if let Some(AttributeValue::Static(value)) = attributes.get("right") {
1457 layout.right = Some(parse_float_attr(value, "right")?);
1458 has_any = true;
1459 }
1460
1461 if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
1462 layout.bottom = Some(parse_float_attr(value, "bottom")?);
1463 has_any = true;
1464 }
1465
1466 if let Some(AttributeValue::Static(value)) = attributes.get("left") {
1467 layout.left = Some(parse_float_attr(value, "left")?);
1468 has_any = true;
1469 }
1470
1471 if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
1473 layout.z_index = Some(parse_int_attr(value, "z_index")?);
1474 has_any = true;
1475 }
1476
1477 if has_any {
1479 layout
1480 .validate()
1481 .map_err(|e| format!("Layout validation failed: {}", e))?;
1482 Ok(Some(layout))
1483 } else {
1484 Ok(None)
1485 }
1486}
1487
1488fn parse_style_attributes(
1490 attributes: &HashMap<String, AttributeValue>,
1491) -> Result<Option<crate::ir::style::StyleProperties>, String> {
1492 use crate::parser::style_parser::{
1493 build_border, build_style_properties, parse_background_attr, parse_border_color,
1494 parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
1495 parse_opacity, parse_shadow_attr, parse_transform,
1496 };
1497
1498 let mut background = None;
1499 let mut color = None;
1500 let mut border_width = None;
1501 let mut border_color = None;
1502 let mut border_radius = None;
1503 let mut border_style = None;
1504 let mut shadow = None;
1505 let mut opacity = None;
1506 let mut transform = None;
1507 let mut has_any = false;
1508
1509 if let Some(AttributeValue::Static(value)) = attributes.get("background") {
1511 background = Some(parse_background_attr(value)?);
1512 has_any = true;
1513 }
1514
1515 if let Some(AttributeValue::Static(value)) = attributes.get("color") {
1517 color = Some(parse_color_attr(value)?);
1518 has_any = true;
1519 }
1520
1521 if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
1523 border_width = Some(parse_border_width(value)?);
1524 has_any = true;
1525 }
1526
1527 if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
1528 border_color = Some(parse_border_color(value)?);
1529 has_any = true;
1530 }
1531
1532 if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
1533 border_radius = Some(parse_border_radius(value)?);
1534 has_any = true;
1535 }
1536
1537 if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
1538 border_style = Some(parse_border_style(value)?);
1539 has_any = true;
1540 }
1541
1542 if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
1544 shadow = Some(parse_shadow_attr(value)?);
1545 has_any = true;
1546 }
1547
1548 if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
1550 opacity = Some(parse_opacity(value)?);
1551 has_any = true;
1552 }
1553
1554 if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
1556 transform = Some(parse_transform(value)?);
1557 has_any = true;
1558 }
1559
1560 if has_any {
1561 let border = build_border(border_width, border_color, border_radius, border_style)?;
1562 let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
1563 Ok(Some(style))
1564 } else {
1565 Ok(None)
1566 }
1567}
1568
1569pub fn validate_no_circular_dependencies(
1582 _file_path: &std::path::Path,
1583 _visited: &mut std::collections::HashSet<std::path::PathBuf>,
1584) -> Result<(), ParseError> {
1585 Ok(())
1586}
1587
1588#[cfg(test)]
1589mod circular_dependency_tests {
1590 use super::*;
1591 use std::collections::HashSet;
1592 use std::path::PathBuf;
1593
1594 #[test]
1595 fn test_no_circular_dependencies_without_includes() {
1596 let file_path = PathBuf::from("test.dampen");
1598 let mut visited = HashSet::new();
1599
1600 let result = validate_no_circular_dependencies(&file_path, &mut visited);
1601 assert!(
1602 result.is_ok(),
1603 "Single file should have no circular dependencies"
1604 );
1605 }
1606
1607 }
1613
1614#[cfg(test)]
1615mod inline_state_styles_tests {
1616 use super::*;
1617 use crate::ir::theme::WidgetState;
1618
1619 #[test]
1620 fn test_parse_single_state_attribute() {
1621 let xml = r##"
1624 <dampen version="1.0" xmlns:hover="urn:dampen:state:hover">
1625 <button label="Click" hover:background="#ff0000" />
1626 </dampen>
1627 "##;
1628
1629 let result = parse(xml);
1630 assert!(result.is_ok(), "Should parse valid XML with hover state");
1631
1632 let doc = result.unwrap();
1633 let button = &doc.root;
1634
1635 assert!(
1637 button
1638 .inline_state_variants
1639 .contains_key(&WidgetState::Hover),
1640 "Should have hover state variant"
1641 );
1642
1643 let hover_style = button
1644 .inline_state_variants
1645 .get(&WidgetState::Hover)
1646 .unwrap();
1647
1648 assert!(
1650 hover_style.background.is_some(),
1651 "Hover state should have background"
1652 );
1653 }
1654
1655 #[test]
1656 fn test_parse_multiple_state_attributes() {
1657 let xml = r##"
1660 <dampen version="1.0"
1661 xmlns:hover="urn:dampen:state:hover"
1662 xmlns:active="urn:dampen:state:active"
1663 xmlns:disabled="urn:dampen:state:disabled">
1664 <button
1665 label="Click"
1666 hover:background="#ff0000"
1667 active:background="#00ff00"
1668 disabled:opacity="0.5"
1669 />
1670 </dampen>
1671 "##;
1672
1673 let result = parse(xml);
1674 assert!(
1675 result.is_ok(),
1676 "Should parse valid XML with multiple states"
1677 );
1678
1679 let doc = result.unwrap();
1680 let button = &doc.root;
1681
1682 assert!(
1684 button
1685 .inline_state_variants
1686 .contains_key(&WidgetState::Hover),
1687 "Should have hover state"
1688 );
1689 assert!(
1690 button
1691 .inline_state_variants
1692 .contains_key(&WidgetState::Active),
1693 "Should have active state"
1694 );
1695 assert!(
1696 button
1697 .inline_state_variants
1698 .contains_key(&WidgetState::Disabled),
1699 "Should have disabled state"
1700 );
1701
1702 let hover_style = button
1704 .inline_state_variants
1705 .get(&WidgetState::Hover)
1706 .unwrap();
1707 assert!(
1708 hover_style.background.is_some(),
1709 "Hover state should have background"
1710 );
1711
1712 let active_style = button
1714 .inline_state_variants
1715 .get(&WidgetState::Active)
1716 .unwrap();
1717 assert!(
1718 active_style.background.is_some(),
1719 "Active state should have background"
1720 );
1721
1722 let disabled_style = button
1724 .inline_state_variants
1725 .get(&WidgetState::Disabled)
1726 .unwrap();
1727 assert!(
1728 disabled_style.opacity.is_some(),
1729 "Disabled state should have opacity"
1730 );
1731 }
1732
1733 #[test]
1734 fn test_parse_invalid_state_prefix() {
1735 let xml = r##"
1737 <dampen version="1.0" xmlns:unknown="urn:dampen:state:unknown">
1738 <button label="Click" unknown:background="#ff0000" />
1739 </dampen>
1740 "##;
1741
1742 let result = parse(xml);
1743 assert!(
1744 result.is_ok(),
1745 "Should parse with warning for invalid state"
1746 );
1747
1748 let doc = result.unwrap();
1749 let button = &doc.root;
1750
1751 assert!(
1753 button.inline_state_variants.is_empty(),
1754 "Should have no state variants for invalid prefix"
1755 );
1756
1757 assert!(
1759 button.attributes.contains_key("unknown:background"),
1760 "Invalid state prefix should be treated as regular attribute"
1761 );
1762 }
1763}