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