1pub mod error;
2pub mod gradient;
3pub mod lexer;
4pub mod style_parser;
5pub mod theme_parser;
6
7use crate::expr::tokenize_binding_expr;
8use crate::expr::{BindingExpr, Expr, LiteralExpr};
9use crate::ir::{
10 AttributeValue, DampenDocument, EventBinding, EventKind, InterpolatedPart, SchemaVersion, Span,
11 WidgetKind, WidgetNode,
12};
13use crate::parser::error::{ParseError, ParseErrorKind};
14use roxmltree::{Document, Node, NodeType};
15use std::collections::HashMap;
16
17pub const MAX_SUPPORTED_VERSION: SchemaVersion = SchemaVersion { major: 1, minor: 0 };
22
23pub fn parse_version_string(version_str: &str, span: Span) -> Result<SchemaVersion, ParseError> {
42 let trimmed = version_str.trim();
43
44 if trimmed.is_empty() {
46 return Err(ParseError {
47 kind: ParseErrorKind::InvalidValue,
48 message: "Version attribute cannot be empty".to_string(),
49 span,
50 suggestion: Some("Use format: version=\"1.0\"".to_string()),
51 });
52 }
53
54 let parts: Vec<&str> = trimmed.split('.').collect();
56 if parts.len() != 2 {
57 return Err(ParseError {
58 kind: ParseErrorKind::InvalidValue,
59 message: format!(
60 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
61 trimmed
62 ),
63 span,
64 suggestion: Some("Use format: version=\"1.0\"".to_string()),
65 });
66 }
67
68 let major = parts[0].parse::<u16>().map_err(|_| ParseError {
70 kind: ParseErrorKind::InvalidValue,
71 message: format!(
72 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
73 trimmed
74 ),
75 span,
76 suggestion: Some("Use format: version=\"1.0\"".to_string()),
77 })?;
78
79 let minor = parts[1].parse::<u16>().map_err(|_| ParseError {
81 kind: ParseErrorKind::InvalidValue,
82 message: format!(
83 "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
84 trimmed
85 ),
86 span,
87 suggestion: Some("Use format: version=\"1.0\"".to_string()),
88 })?;
89
90 Ok(SchemaVersion { major, minor })
91}
92
93pub fn validate_version_supported(version: &SchemaVersion, span: Span) -> Result<(), ParseError> {
105 if (version.major, version.minor) > (MAX_SUPPORTED_VERSION.major, MAX_SUPPORTED_VERSION.minor) {
106 return Err(ParseError {
107 kind: ParseErrorKind::UnsupportedVersion,
108 message: format!(
109 "Schema version {}.{} is not supported. Maximum supported version: {}.{}",
110 version.major,
111 version.minor,
112 MAX_SUPPORTED_VERSION.major,
113 MAX_SUPPORTED_VERSION.minor
114 ),
115 span,
116 suggestion: Some(format!(
117 "Upgrade dampen-core to support v{}.{}, or use version=\"{}.{}\"",
118 version.major,
119 version.minor,
120 MAX_SUPPORTED_VERSION.major,
121 MAX_SUPPORTED_VERSION.minor
122 )),
123 });
124 }
125 Ok(())
126}
127
128#[derive(Debug, Clone, PartialEq)]
132pub struct ValidationWarning {
133 pub widget_kind: WidgetKind,
135 pub declared_version: SchemaVersion,
137 pub required_version: SchemaVersion,
139 pub span: Span,
141}
142
143impl ValidationWarning {
144 pub fn format_message(&self) -> String {
146 format!(
147 "Widget '{}' requires schema v{}.{} but document declares v{}.{}",
148 widget_kind_name(&self.widget_kind),
149 self.required_version.major,
150 self.required_version.minor,
151 self.declared_version.major,
152 self.declared_version.minor
153 )
154 }
155
156 pub fn suggestion(&self) -> String {
158 format!(
159 "Update to <dampen version=\"{}.{}\"> or remove this widget",
160 self.required_version.major, self.required_version.minor
161 )
162 }
163}
164
165fn widget_kind_name(kind: &WidgetKind) -> String {
167 match kind {
168 WidgetKind::Column => "column".to_string(),
169 WidgetKind::Row => "row".to_string(),
170 WidgetKind::Container => "container".to_string(),
171 WidgetKind::Scrollable => "scrollable".to_string(),
172 WidgetKind::Stack => "stack".to_string(),
173 WidgetKind::Text => "text".to_string(),
174 WidgetKind::Image => "image".to_string(),
175 WidgetKind::Svg => "svg".to_string(),
176 WidgetKind::Button => "button".to_string(),
177 WidgetKind::TextInput => "text_input".to_string(),
178 WidgetKind::Checkbox => "checkbox".to_string(),
179 WidgetKind::Slider => "slider".to_string(),
180 WidgetKind::PickList => "pick_list".to_string(),
181 WidgetKind::Toggler => "toggler".to_string(),
182 WidgetKind::Space => "space".to_string(),
183 WidgetKind::Rule => "rule".to_string(),
184 WidgetKind::Radio => "radio".to_string(),
185 WidgetKind::ComboBox => "combobox".to_string(),
186 WidgetKind::ProgressBar => "progress_bar".to_string(),
187 WidgetKind::Tooltip => "tooltip".to_string(),
188 WidgetKind::Grid => "grid".to_string(),
189 WidgetKind::Canvas => "canvas".to_string(),
190 WidgetKind::Float => "float".to_string(),
191 WidgetKind::For => "for".to_string(),
192 WidgetKind::Custom(name) => name.clone(),
193 }
194}
195
196pub fn validate_widget_versions(document: &DampenDocument) -> Vec<ValidationWarning> {
221 let mut warnings = Vec::new();
222 validate_widget_tree(&document.root, &document.version, &mut warnings);
223 warnings
224}
225
226fn validate_widget_tree(
228 node: &WidgetNode,
229 doc_version: &SchemaVersion,
230 warnings: &mut Vec<ValidationWarning>,
231) {
232 let min_version = node.kind.minimum_version();
233
234 if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
236 warnings.push(ValidationWarning {
237 widget_kind: node.kind.clone(),
238 declared_version: *doc_version,
239 required_version: min_version,
240 span: node.span,
241 });
242 }
243
244 for child in &node.children {
246 validate_widget_tree(child, doc_version, warnings);
247 }
248}
249
250pub fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
282 let doc = Document::parse(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 Ok(DampenDocument {
310 version: SchemaVersion::default(),
311 root: root_widget,
312 themes: HashMap::new(),
313 style_classes: HashMap::new(),
314 global_theme: None,
315 })
316 }
317}
318
319fn validate_widget_attributes(
321 kind: &WidgetKind,
322 attributes: &std::collections::HashMap<String, AttributeValue>,
323 span: Span,
324) -> Result<(), ParseError> {
325 match kind {
326 WidgetKind::ComboBox | WidgetKind::PickList => {
327 if let Some(AttributeValue::Static(options_value)) = attributes.get("options") {
329 if options_value.trim().is_empty() {
330 return Err(ParseError {
331 kind: ParseErrorKind::MissingAttribute,
332 message: format!(
333 "{:?} widget requires 'options' attribute to be non-empty",
334 kind
335 ),
336 span,
337 suggestion: Some(
338 "Add a comma-separated list: options=\"Option1,Option2\"".to_string(),
339 ),
340 });
341 }
342 } else {
343 return Err(ParseError {
344 kind: ParseErrorKind::MissingAttribute,
345 message: format!("{:?} widget requires 'options' attribute", kind),
346 span,
347 suggestion: Some(
348 "Add options attribute: options=\"Option1,Option2\"".to_string(),
349 ),
350 });
351 }
352 }
353 WidgetKind::Canvas => {
354 if !attributes.contains_key("width") {
356 return Err(ParseError {
357 kind: ParseErrorKind::MissingAttribute,
358 message: format!("{:?} widget requires 'width' attribute", kind),
359 span,
360 suggestion: Some("Add width attribute: width=\"400\"".to_string()),
361 });
362 }
363 if !attributes.contains_key("height") {
365 return Err(ParseError {
366 kind: ParseErrorKind::MissingAttribute,
367 message: format!("{:?} widget requires 'height' attribute", kind),
368 span,
369 suggestion: Some("Add height attribute: height=\"200\"".to_string()),
370 });
371 }
372 if !attributes.contains_key("program") {
374 return Err(ParseError {
375 kind: ParseErrorKind::MissingAttribute,
376 message: format!("{:?} widget requires 'program' attribute", kind),
377 span,
378 suggestion: Some("Add program attribute: program=\"{{chart}}\"".to_string()),
379 });
380 }
381
382 if let Some(AttributeValue::Static(width_str)) = attributes.get("width") {
384 if let Ok(width) = width_str.parse::<u32>() {
385 if !(50..=4000).contains(&width) {
386 return Err(ParseError {
387 kind: ParseErrorKind::InvalidValue,
388 message: format!(
389 "Canvas width must be between 50 and 4000 pixels, found {}",
390 width
391 ),
392 span,
393 suggestion: Some("Use width value between 50 and 4000".to_string()),
394 });
395 }
396 }
397 }
398
399 if let Some(AttributeValue::Static(height_str)) = attributes.get("height") {
401 if let Ok(height) = height_str.parse::<u32>() {
402 if !(50..=4000).contains(&height) {
403 return Err(ParseError {
404 kind: ParseErrorKind::InvalidValue,
405 message: format!(
406 "Canvas height must be between 50 and 4000 pixels, found {}",
407 height
408 ),
409 span,
410 suggestion: Some("Use height value between 50 and 4000".to_string()),
411 });
412 }
413 }
414 }
415 }
416 WidgetKind::Grid => {
417 if !attributes.contains_key("columns") {
419 return Err(ParseError {
420 kind: ParseErrorKind::MissingAttribute,
421 message: format!("{:?} widget requires 'columns' attribute", kind),
422 span,
423 suggestion: Some("Add columns attribute: columns=\"5\"".to_string()),
424 });
425 }
426 if let Some(AttributeValue::Static(cols)) = attributes.get("columns") {
428 if let Ok(cols_num) = cols.parse::<u32>() {
429 if !(1..=20).contains(&cols_num) {
430 return Err(ParseError {
431 kind: ParseErrorKind::InvalidValue,
432 message: format!(
433 "Grid columns must be between 1 and 20, found {}",
434 cols_num
435 ),
436 span,
437 suggestion: Some("Use columns value between 1 and 20".to_string()),
438 });
439 }
440 }
441 }
442 }
443 WidgetKind::Tooltip => {
444 if !attributes.contains_key("message") {
446 return Err(ParseError {
447 kind: ParseErrorKind::MissingAttribute,
448 message: format!("{:?} widget requires 'message' attribute", kind),
449 span,
450 suggestion: Some("Add message attribute: message=\"Help text\"".to_string()),
451 });
452 }
453 }
454 WidgetKind::For => {
455 if !attributes.contains_key("each") {
457 return Err(ParseError {
458 kind: ParseErrorKind::MissingAttribute,
459 message: "For loop requires 'each' attribute to name the loop variable"
460 .to_string(),
461 span,
462 suggestion: Some("Add each attribute: each=\"item\"".to_string()),
463 });
464 }
465 if !attributes.contains_key("in") {
467 return Err(ParseError {
468 kind: ParseErrorKind::MissingAttribute,
469 message: "For loop requires 'in' attribute with collection binding".to_string(),
470 span,
471 suggestion: Some("Add in attribute: in=\"{items}\"".to_string()),
472 });
473 }
474 }
475 _ => {}
476 }
477 Ok(())
478}
479
480fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
482 if children.is_empty() {
483 return Err(ParseError {
484 kind: ParseErrorKind::InvalidValue,
485 message: "Tooltip widget must have exactly one child widget".to_string(),
486 span,
487 suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
488 });
489 }
490 if children.len() > 1 {
491 return Err(ParseError {
492 kind: ParseErrorKind::InvalidValue,
493 message: format!(
494 "Tooltip widget must have exactly one child, found {}",
495 children.len()
496 ),
497 span,
498 suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
499 });
500 }
501 Ok(())
502}
503
504fn validate_canvas_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
506 if !children.is_empty() {
507 return Err(ParseError {
508 kind: ParseErrorKind::InvalidValue,
509 message: format!(
510 "Canvas widget cannot have children, found {}",
511 children.len()
512 ),
513 span,
514 suggestion: Some("Canvas is a leaf widget - remove child elements".to_string()),
515 });
516 }
517 Ok(())
518}
519
520fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
522 if node.node_type() != NodeType::Element {
524 return Err(ParseError {
525 kind: ParseErrorKind::XmlSyntax,
526 message: "Expected element node".to_string(),
527 span: Span::new(0, 0, 1, 1),
528 suggestion: None,
529 });
530 }
531
532 let tag_name = node.tag_name().name();
534 let kind = match tag_name {
535 "column" => WidgetKind::Column,
536 "row" => WidgetKind::Row,
537 "container" => WidgetKind::Container,
538 "scrollable" => WidgetKind::Scrollable,
539 "stack" => WidgetKind::Stack,
540 "text" => WidgetKind::Text,
541 "image" => WidgetKind::Image,
542 "svg" => WidgetKind::Svg,
543 "button" => WidgetKind::Button,
544 "text_input" => WidgetKind::TextInput,
545 "checkbox" => WidgetKind::Checkbox,
546 "slider" => WidgetKind::Slider,
547 "pick_list" => WidgetKind::PickList,
548 "toggler" => WidgetKind::Toggler,
549 "space" => WidgetKind::Space,
550 "rule" => WidgetKind::Rule,
551 "radio" => WidgetKind::Radio,
552 "combobox" => WidgetKind::ComboBox,
553 "progress_bar" => WidgetKind::ProgressBar,
554 "tooltip" => WidgetKind::Tooltip,
555 "grid" => WidgetKind::Grid,
556 "canvas" => WidgetKind::Canvas,
557 "float" => WidgetKind::Float,
558 "for" => WidgetKind::For,
559 _ => {
560 return Err(ParseError {
561 kind: ParseErrorKind::UnknownWidget,
562 message: format!("Unknown widget: <{}>", tag_name),
563 span: get_span(node, source),
564 suggestion: Some("Did you mean one of the standard widgets?".to_string()),
565 });
566 }
567 };
568
569 let mut attributes = std::collections::HashMap::new();
571 let mut breakpoint_attributes = std::collections::HashMap::new();
572 let mut events = Vec::new();
573 let mut id = None;
574
575 for attr in node.attributes() {
576 let name = attr.name();
577 let value = attr.value();
578
579 if name == "id" {
581 id = Some(value.to_string());
582 continue;
583 }
584
585 if name.starts_with("on_") {
587 let event_kind = match name {
588 "on_click" => Some(EventKind::Click),
589 "on_press" => Some(EventKind::Press),
590 "on_release" => Some(EventKind::Release),
591 "on_change" => Some(EventKind::Change),
592 "on_input" => Some(EventKind::Input),
593 "on_submit" => Some(EventKind::Submit),
594 "on_select" => Some(EventKind::Select),
595 "on_toggle" => Some(EventKind::Toggle),
596 "on_scroll" => Some(EventKind::Scroll),
597 _ => None,
598 };
599
600 if let Some(event) = event_kind {
601 let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
604 let handler = value[..colon_pos].to_string();
605 let param_str = &value[colon_pos + 1..];
606
607 if param_str.starts_with('\'')
609 && param_str.ends_with('\'')
610 && param_str.len() >= 2
611 {
612 let quoted_value = ¶m_str[1..param_str.len() - 1];
613 let expr = BindingExpr {
615 expr: Expr::Literal(LiteralExpr::String(quoted_value.to_string())),
616 span: Span::new(
617 colon_pos + 1,
618 colon_pos + 1 + param_str.len(),
619 1,
620 colon_pos as u32 + 1,
621 ),
622 };
623 (handler, Some(expr))
624 } else {
625 let param_clean = param_str.trim_matches('{').trim_matches('}');
627
628 match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
630 Ok(expr) => (handler, Some(expr)),
631 Err(_) => {
632 (value.to_string(), None)
634 }
635 }
636 }
637 } else {
638 (value.to_string(), None)
639 };
640
641 events.push(EventBinding {
642 event,
643 handler: handler_name,
644 param,
645 span: get_span(node, source),
646 });
647 continue;
648 }
649 }
650
651 if let Some((prefix, attr_name)) = name.split_once('-') {
654 if let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix) {
655 let attr_value = parse_attribute_value(value, get_span(node, source))?;
657 breakpoint_attributes
658 .entry(breakpoint)
659 .or_insert_with(HashMap::new)
660 .insert(attr_name.to_string(), attr_value);
661 continue;
662 }
663 }
664
665 let attr_value = parse_attribute_value(value, get_span(node, source))?;
667 attributes.insert(name.to_string(), attr_value);
668 }
669
670 let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
672 class_attr
673 .split_whitespace()
674 .map(|s| s.to_string())
675 .collect()
676 } else {
677 Vec::new()
678 };
679
680 let theme_ref = attributes.get("theme").cloned();
682
683 let mut children = Vec::new();
685 for child in node.children() {
686 if child.node_type() == NodeType::Element {
687 children.push(parse_node(child, source)?);
688 }
689 }
690
691 if kind == WidgetKind::Tooltip {
693 validate_tooltip_children(&children, get_span(node, source))?;
694 }
695
696 if kind == WidgetKind::Canvas {
698 validate_canvas_children(&children, get_span(node, source))?;
699 }
700
701 let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
703 kind: ParseErrorKind::InvalidValue,
704 message: e,
705 span: get_span(node, source),
706 suggestion: None,
707 })?;
708 let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
709 kind: ParseErrorKind::InvalidValue,
710 message: e,
711 span: get_span(node, source),
712 suggestion: None,
713 })?;
714
715 validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
717
718 Ok(WidgetNode {
719 kind,
720 id,
721 attributes,
722 events,
723 children,
724 span: get_span(node, source),
725 style,
726 layout,
727 theme_ref,
728 classes,
729 breakpoint_attributes,
730 })
731}
732
733fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
735 let mut themes = HashMap::new();
736 let mut style_classes = HashMap::new();
737 let mut root_widget = None;
738 let mut global_theme = None;
739
740 let span = get_span(root, source);
742 let version = if let Some(version_attr) = root.attribute("version") {
743 let parsed = parse_version_string(version_attr, span)?;
744 validate_version_supported(&parsed, span)?;
745 parsed
746 } else {
747 SchemaVersion::default()
749 };
750
751 for child in root.children() {
753 if child.node_type() != NodeType::Element {
754 continue;
755 }
756
757 let tag_name = child.tag_name().name();
758
759 match tag_name {
760 "themes" => {
761 for theme_node in child.children() {
763 if theme_node.node_type() == NodeType::Element
764 && theme_node.tag_name().name() == "theme"
765 {
766 let theme =
767 crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
768 let name = theme_node
769 .attribute("name")
770 .map(|s| s.to_string())
771 .unwrap_or_else(|| "default".to_string());
772 themes.insert(name, theme);
773 }
774 }
775 }
776 "style_classes" | "classes" | "styles" => {
777 for class_node in child.children() {
779 if class_node.node_type() == NodeType::Element {
780 let tag = class_node.tag_name().name();
781 if tag == "class" || tag == "style" {
782 let class = crate::parser::theme_parser::parse_style_class_from_node(
783 class_node, source,
784 )?;
785 style_classes.insert(class.name.clone(), class);
786 }
787 }
788 }
789 }
790 "global_theme" => {
791 if let Some(theme_name) = child.attribute("name") {
793 global_theme = Some(theme_name.to_string());
794 }
795 }
796 _ => {
797 if root_widget.is_some() {
799 return Err(ParseError {
800 kind: ParseErrorKind::XmlSyntax,
801 message: "Multiple root widgets found in <dampen>".to_string(),
802 span: get_span(child, source),
803 suggestion: Some("Only one root widget is allowed".to_string()),
804 });
805 }
806 root_widget = Some(parse_node(child, source)?);
807 }
808 }
809 }
810
811 let root_widget = root_widget.ok_or_else(|| ParseError {
813 kind: ParseErrorKind::XmlSyntax,
814 message: "No root widget found in <dampen>".to_string(),
815 span: get_span(root, source),
816 suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
817 })?;
818
819 Ok(DampenDocument {
820 version,
821 root: root_widget,
822 themes,
823 style_classes,
824 global_theme,
825 })
826}
827
828pub fn parse_comma_separated(value: &str) -> Vec<String> {
830 value
831 .split(',')
832 .map(|s| s.trim().to_string())
833 .filter(|s| !s.is_empty())
834 .collect()
835}
836
837pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
839where
840 T: std::str::FromStr + std::fmt::Display,
841{
842 let normalized = value.trim().to_lowercase();
843 for variant in valid_variants.iter() {
844 if variant.to_lowercase() == normalized {
845 return T::from_str(variant).map_err(|_| {
846 format!(
847 "Failed to parse '{}' as {}",
848 variant,
849 std::any::type_name::<T>()
850 )
851 });
852 }
853 }
854 Err(format!(
855 "Invalid value '{}'. Valid options: {}",
856 value,
857 valid_variants.join(", ")
858 ))
859}
860
861fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
863 if value.contains('{') && value.contains('}') {
865 let mut parts = Vec::new();
867 let mut remaining = value;
868
869 while let Some(start_pos) = remaining.find('{') {
870 if start_pos > 0 {
872 parts.push(InterpolatedPart::Literal(
873 remaining[..start_pos].to_string(),
874 ));
875 }
876
877 if let Some(end_pos) = remaining[start_pos..].find('}') {
879 let expr_start = start_pos + 1;
880 let expr_end = start_pos + end_pos;
881 let expr_str = &remaining[expr_start..expr_end];
882
883 let binding_expr = tokenize_binding_expr(
885 expr_str,
886 span.start + expr_start,
887 span.line,
888 span.column + expr_start as u32,
889 )
890 .map_err(|e| ParseError {
891 kind: ParseErrorKind::InvalidExpression,
892 message: format!("Invalid expression: {}", e),
893 span: Span::new(
894 span.start + expr_start,
895 span.start + expr_end,
896 span.line,
897 span.column + expr_start as u32,
898 ),
899 suggestion: None,
900 })?;
901
902 parts.push(InterpolatedPart::Binding(binding_expr));
903
904 remaining = &remaining[expr_end + 1..];
906 } else {
907 parts.push(InterpolatedPart::Literal(remaining.to_string()));
909 break;
910 }
911 }
912
913 if !remaining.is_empty() {
915 parts.push(InterpolatedPart::Literal(remaining.to_string()));
916 }
917
918 if parts.len() == 1 {
921 match &parts[0] {
922 InterpolatedPart::Binding(expr) => {
923 return Ok(AttributeValue::Binding(expr.clone()));
924 }
925 InterpolatedPart::Literal(lit) => {
926 return Ok(AttributeValue::Static(lit.clone()));
927 }
928 }
929 } else {
930 return Ok(AttributeValue::Interpolated(parts));
931 }
932 }
933
934 Ok(AttributeValue::Static(value.to_string()))
936}
937
938fn get_span(node: Node, source: &str) -> Span {
940 let range = node.range();
941
942 let (line, col) = calculate_line_col(source, range.start);
944
945 Span {
946 start: range.start,
947 end: range.end,
948 line,
949 column: col,
950 }
951}
952
953fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
955 let mut line = 1;
956 let mut col = 1;
957
958 for (i, c) in source.chars().enumerate() {
959 if i >= offset {
960 break;
961 }
962 if c == '\n' {
963 line += 1;
964 col = 1;
965 } else {
966 col += 1;
967 }
968 }
969
970 (line, col)
971}
972
973fn parse_layout_attributes(
975 kind: &WidgetKind,
976 attributes: &HashMap<String, AttributeValue>,
977) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
978 use crate::ir::layout::LayoutConstraints;
979 use crate::parser::style_parser::{
980 parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
981 parse_length_attr, parse_padding_attr, parse_spacing,
982 };
983
984 let mut layout = LayoutConstraints::default();
985 let mut has_any = false;
986
987 if let Some(AttributeValue::Static(value)) = attributes.get("width") {
989 layout.width = Some(parse_length_attr(value)?);
990 has_any = true;
991 }
992
993 if let Some(AttributeValue::Static(value)) = attributes.get("height") {
995 layout.height = Some(parse_length_attr(value)?);
996 has_any = true;
997 }
998
999 if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
1001 layout.min_width = Some(parse_constraint(value)?);
1002 has_any = true;
1003 }
1004
1005 if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
1006 layout.max_width = Some(parse_constraint(value)?);
1007 has_any = true;
1008 }
1009
1010 if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
1011 layout.min_height = Some(parse_constraint(value)?);
1012 has_any = true;
1013 }
1014
1015 if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
1016 layout.max_height = Some(parse_constraint(value)?);
1017 has_any = true;
1018 }
1019
1020 if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
1022 layout.padding = Some(parse_padding_attr(value)?);
1023 has_any = true;
1024 }
1025
1026 if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
1028 layout.spacing = Some(parse_spacing(value)?);
1029 has_any = true;
1030 }
1031
1032 if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
1034 layout.align_items = Some(parse_alignment(value)?);
1035 has_any = true;
1036 }
1037
1038 if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
1039 layout.justify_content = Some(parse_justification(value)?);
1040 has_any = true;
1041 }
1042
1043 if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
1044 layout.align_self = Some(parse_alignment(value)?);
1045 has_any = true;
1046 }
1047
1048 if let Some(AttributeValue::Static(value)) = attributes.get("align_x") {
1050 layout.align_x = Some(parse_alignment(value)?);
1051 has_any = true;
1052 }
1053
1054 if let Some(AttributeValue::Static(value)) = attributes.get("align_y") {
1055 layout.align_y = Some(parse_alignment(value)?);
1056 has_any = true;
1057 }
1058
1059 if let Some(AttributeValue::Static(value)) = attributes.get("align") {
1061 let alignment = parse_alignment(value)?;
1062 layout.align_items = Some(alignment);
1063 layout.justify_content = Some(match alignment {
1064 crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
1065 crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
1066 crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
1067 crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
1068 });
1069 has_any = true;
1070 }
1071
1072 if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
1074 layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
1075 has_any = true;
1076 }
1077
1078 if !matches!(kind, WidgetKind::Tooltip) {
1080 if let Some(AttributeValue::Static(value)) = attributes.get("position") {
1081 layout.position = Some(crate::ir::layout::Position::parse(value)?);
1082 has_any = true;
1083 }
1084 }
1085
1086 if let Some(AttributeValue::Static(value)) = attributes.get("top") {
1088 layout.top = Some(parse_float_attr(value, "top")?);
1089 has_any = true;
1090 }
1091
1092 if let Some(AttributeValue::Static(value)) = attributes.get("right") {
1093 layout.right = Some(parse_float_attr(value, "right")?);
1094 has_any = true;
1095 }
1096
1097 if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
1098 layout.bottom = Some(parse_float_attr(value, "bottom")?);
1099 has_any = true;
1100 }
1101
1102 if let Some(AttributeValue::Static(value)) = attributes.get("left") {
1103 layout.left = Some(parse_float_attr(value, "left")?);
1104 has_any = true;
1105 }
1106
1107 if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
1109 layout.z_index = Some(parse_int_attr(value, "z_index")?);
1110 has_any = true;
1111 }
1112
1113 if has_any {
1115 layout
1116 .validate()
1117 .map_err(|e| format!("Layout validation failed: {}", e))?;
1118 Ok(Some(layout))
1119 } else {
1120 Ok(None)
1121 }
1122}
1123
1124fn parse_style_attributes(
1126 attributes: &HashMap<String, AttributeValue>,
1127) -> Result<Option<crate::ir::style::StyleProperties>, String> {
1128 use crate::parser::style_parser::{
1129 build_border, build_style_properties, parse_background_attr, parse_border_color,
1130 parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
1131 parse_opacity, parse_shadow_attr, parse_transform,
1132 };
1133
1134 let mut background = None;
1135 let mut color = None;
1136 let mut border_width = None;
1137 let mut border_color = None;
1138 let mut border_radius = None;
1139 let mut border_style = None;
1140 let mut shadow = None;
1141 let mut opacity = None;
1142 let mut transform = None;
1143 let mut has_any = false;
1144
1145 if let Some(AttributeValue::Static(value)) = attributes.get("background") {
1147 background = Some(parse_background_attr(value)?);
1148 has_any = true;
1149 }
1150
1151 if let Some(AttributeValue::Static(value)) = attributes.get("color") {
1153 color = Some(parse_color_attr(value)?);
1154 has_any = true;
1155 }
1156
1157 if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
1159 border_width = Some(parse_border_width(value)?);
1160 has_any = true;
1161 }
1162
1163 if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
1164 border_color = Some(parse_border_color(value)?);
1165 has_any = true;
1166 }
1167
1168 if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
1169 border_radius = Some(parse_border_radius(value)?);
1170 has_any = true;
1171 }
1172
1173 if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
1174 border_style = Some(parse_border_style(value)?);
1175 has_any = true;
1176 }
1177
1178 if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
1180 shadow = Some(parse_shadow_attr(value)?);
1181 has_any = true;
1182 }
1183
1184 if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
1186 opacity = Some(parse_opacity(value)?);
1187 has_any = true;
1188 }
1189
1190 if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
1192 transform = Some(parse_transform(value)?);
1193 has_any = true;
1194 }
1195
1196 if has_any {
1197 let border = build_border(border_width, border_color, border_radius, border_style)?;
1198 let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
1199 Ok(Some(style))
1200 } else {
1201 Ok(None)
1202 }
1203}
1204
1205#[allow(dead_code)]
1236pub fn validate_no_circular_dependencies(
1237 _file_path: &std::path::Path,
1238 _visited: &mut std::collections::HashSet<std::path::PathBuf>,
1239) -> Result<(), ParseError> {
1240 Ok(())
1242}
1243
1244#[cfg(test)]
1245mod circular_dependency_tests {
1246 use super::*;
1247 use std::collections::HashSet;
1248 use std::path::PathBuf;
1249
1250 #[test]
1251 fn test_no_circular_dependencies_without_includes() {
1252 let file_path = PathBuf::from("test.dampen");
1254 let mut visited = HashSet::new();
1255
1256 let result = validate_no_circular_dependencies(&file_path, &mut visited);
1257 assert!(
1258 result.is_ok(),
1259 "Single file should have no circular dependencies"
1260 );
1261 }
1262
1263 }