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 fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
48 let doc = Document::parse(xml).map_err(|e| ParseError {
50 kind: ParseErrorKind::XmlSyntax,
51 message: e.to_string(),
52 span: Span::new(0, 0, 1, 1),
53 suggestion: None,
54 })?;
55
56 let root = doc.root().first_child().ok_or_else(|| ParseError {
58 kind: ParseErrorKind::XmlSyntax,
59 message: "No root element found".to_string(),
60 span: Span::new(0, 0, 1, 1),
61 suggestion: None,
62 })?;
63
64 let root_tag = root.tag_name().name();
66
67 if root_tag == "dampen" {
68 parse_dampen_document(root, xml)
70 } else {
71 let root_widget = parse_node(root, xml)?;
73
74 Ok(DampenDocument {
75 version: SchemaVersion { major: 1, minor: 0 },
76 root: root_widget,
77 themes: HashMap::new(),
78 style_classes: HashMap::new(),
79 global_theme: None,
80 })
81 }
82}
83
84fn validate_widget_attributes(
86 kind: &WidgetKind,
87 attributes: &std::collections::HashMap<String, AttributeValue>,
88 span: Span,
89) -> Result<(), ParseError> {
90 match kind {
91 WidgetKind::ComboBox | WidgetKind::PickList => {
92 if let Some(AttributeValue::Static(options_value)) = attributes.get("options") {
94 if options_value.trim().is_empty() {
95 return Err(ParseError {
96 kind: ParseErrorKind::MissingAttribute,
97 message: format!(
98 "{:?} widget requires 'options' attribute to be non-empty",
99 kind
100 ),
101 span,
102 suggestion: Some(
103 "Add a comma-separated list: options=\"Option1,Option2\"".to_string(),
104 ),
105 });
106 }
107 } else {
108 return Err(ParseError {
109 kind: ParseErrorKind::MissingAttribute,
110 message: format!("{:?} widget requires 'options' attribute", kind),
111 span,
112 suggestion: Some(
113 "Add options attribute: options=\"Option1,Option2\"".to_string(),
114 ),
115 });
116 }
117 }
118 WidgetKind::Canvas => {
119 if !attributes.contains_key("width") {
121 return Err(ParseError {
122 kind: ParseErrorKind::MissingAttribute,
123 message: format!("{:?} widget requires 'width' attribute", kind),
124 span,
125 suggestion: Some("Add width attribute: width=\"400\"".to_string()),
126 });
127 }
128 if !attributes.contains_key("height") {
130 return Err(ParseError {
131 kind: ParseErrorKind::MissingAttribute,
132 message: format!("{:?} widget requires 'height' attribute", kind),
133 span,
134 suggestion: Some("Add height attribute: height=\"200\"".to_string()),
135 });
136 }
137 if !attributes.contains_key("program") {
139 return Err(ParseError {
140 kind: ParseErrorKind::MissingAttribute,
141 message: format!("{:?} widget requires 'program' attribute", kind),
142 span,
143 suggestion: Some("Add program attribute: program=\"{{chart}}\"".to_string()),
144 });
145 }
146
147 if let Some(AttributeValue::Static(width_str)) = attributes.get("width") {
149 if let Ok(width) = width_str.parse::<u32>() {
150 if !(50..=4000).contains(&width) {
151 return Err(ParseError {
152 kind: ParseErrorKind::InvalidValue,
153 message: format!(
154 "Canvas width must be between 50 and 4000 pixels, found {}",
155 width
156 ),
157 span,
158 suggestion: Some("Use width value between 50 and 4000".to_string()),
159 });
160 }
161 }
162 }
163
164 if let Some(AttributeValue::Static(height_str)) = attributes.get("height") {
166 if let Ok(height) = height_str.parse::<u32>() {
167 if !(50..=4000).contains(&height) {
168 return Err(ParseError {
169 kind: ParseErrorKind::InvalidValue,
170 message: format!(
171 "Canvas height must be between 50 and 4000 pixels, found {}",
172 height
173 ),
174 span,
175 suggestion: Some("Use height value between 50 and 4000".to_string()),
176 });
177 }
178 }
179 }
180 }
181 WidgetKind::Grid => {
182 if !attributes.contains_key("columns") {
184 return Err(ParseError {
185 kind: ParseErrorKind::MissingAttribute,
186 message: format!("{:?} widget requires 'columns' attribute", kind),
187 span,
188 suggestion: Some("Add columns attribute: columns=\"5\"".to_string()),
189 });
190 }
191 if let Some(AttributeValue::Static(cols)) = attributes.get("columns") {
193 if let Ok(cols_num) = cols.parse::<u32>() {
194 if !(1..=20).contains(&cols_num) {
195 return Err(ParseError {
196 kind: ParseErrorKind::InvalidValue,
197 message: format!(
198 "Grid columns must be between 1 and 20, found {}",
199 cols_num
200 ),
201 span,
202 suggestion: Some("Use columns value between 1 and 20".to_string()),
203 });
204 }
205 }
206 }
207 }
208 WidgetKind::Tooltip => {
209 if !attributes.contains_key("message") {
211 return Err(ParseError {
212 kind: ParseErrorKind::MissingAttribute,
213 message: format!("{:?} widget requires 'message' attribute", kind),
214 span,
215 suggestion: Some("Add message attribute: message=\"Help text\"".to_string()),
216 });
217 }
218 }
219 WidgetKind::For => {
220 if !attributes.contains_key("each") {
222 return Err(ParseError {
223 kind: ParseErrorKind::MissingAttribute,
224 message: "For loop requires 'each' attribute to name the loop variable"
225 .to_string(),
226 span,
227 suggestion: Some("Add each attribute: each=\"item\"".to_string()),
228 });
229 }
230 if !attributes.contains_key("in") {
232 return Err(ParseError {
233 kind: ParseErrorKind::MissingAttribute,
234 message: "For loop requires 'in' attribute with collection binding".to_string(),
235 span,
236 suggestion: Some("Add in attribute: in=\"{items}\"".to_string()),
237 });
238 }
239 }
240 _ => {}
241 }
242 Ok(())
243}
244
245fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
247 if children.is_empty() {
248 return Err(ParseError {
249 kind: ParseErrorKind::InvalidValue,
250 message: "Tooltip widget must have exactly one child widget".to_string(),
251 span,
252 suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
253 });
254 }
255 if children.len() > 1 {
256 return Err(ParseError {
257 kind: ParseErrorKind::InvalidValue,
258 message: format!(
259 "Tooltip widget must have exactly one child, found {}",
260 children.len()
261 ),
262 span,
263 suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
264 });
265 }
266 Ok(())
267}
268
269fn validate_canvas_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
271 if !children.is_empty() {
272 return Err(ParseError {
273 kind: ParseErrorKind::InvalidValue,
274 message: format!(
275 "Canvas widget cannot have children, found {}",
276 children.len()
277 ),
278 span,
279 suggestion: Some("Canvas is a leaf widget - remove child elements".to_string()),
280 });
281 }
282 Ok(())
283}
284
285fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
287 if node.node_type() != NodeType::Element {
289 return Err(ParseError {
290 kind: ParseErrorKind::XmlSyntax,
291 message: "Expected element node".to_string(),
292 span: Span::new(0, 0, 1, 1),
293 suggestion: None,
294 });
295 }
296
297 let tag_name = node.tag_name().name();
299 let kind = match tag_name {
300 "column" => WidgetKind::Column,
301 "row" => WidgetKind::Row,
302 "container" => WidgetKind::Container,
303 "scrollable" => WidgetKind::Scrollable,
304 "stack" => WidgetKind::Stack,
305 "text" => WidgetKind::Text,
306 "image" => WidgetKind::Image,
307 "svg" => WidgetKind::Svg,
308 "button" => WidgetKind::Button,
309 "text_input" => WidgetKind::TextInput,
310 "checkbox" => WidgetKind::Checkbox,
311 "slider" => WidgetKind::Slider,
312 "pick_list" => WidgetKind::PickList,
313 "toggler" => WidgetKind::Toggler,
314 "space" => WidgetKind::Space,
315 "rule" => WidgetKind::Rule,
316 "radio" => WidgetKind::Radio,
317 "combobox" => WidgetKind::ComboBox,
318 "progress_bar" => WidgetKind::ProgressBar,
319 "tooltip" => WidgetKind::Tooltip,
320 "grid" => WidgetKind::Grid,
321 "canvas" => WidgetKind::Canvas,
322 "float" => WidgetKind::Float,
323 "for" => WidgetKind::For,
324 _ => {
325 return Err(ParseError {
326 kind: ParseErrorKind::UnknownWidget,
327 message: format!("Unknown widget: <{}>", tag_name),
328 span: get_span(node, source),
329 suggestion: Some("Did you mean one of the standard widgets?".to_string()),
330 });
331 }
332 };
333
334 let mut attributes = std::collections::HashMap::new();
336 let mut breakpoint_attributes = std::collections::HashMap::new();
337 let mut events = Vec::new();
338 let mut id = None;
339
340 for attr in node.attributes() {
341 let name = attr.name();
342 let value = attr.value();
343
344 if name == "id" {
346 id = Some(value.to_string());
347 continue;
348 }
349
350 if name.starts_with("on_") {
352 let event_kind = match name {
353 "on_click" => Some(EventKind::Click),
354 "on_press" => Some(EventKind::Press),
355 "on_release" => Some(EventKind::Release),
356 "on_change" => Some(EventKind::Change),
357 "on_input" => Some(EventKind::Input),
358 "on_submit" => Some(EventKind::Submit),
359 "on_select" => Some(EventKind::Select),
360 "on_toggle" => Some(EventKind::Toggle),
361 "on_scroll" => Some(EventKind::Scroll),
362 _ => None,
363 };
364
365 if let Some(event) = event_kind {
366 let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
369 let handler = value[..colon_pos].to_string();
370 let param_str = &value[colon_pos + 1..];
371
372 let param_clean = param_str.trim_matches('{').trim_matches('}');
374
375 match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
377 Ok(expr) => (handler, Some(expr)),
378 Err(_) => {
379 (value.to_string(), None)
381 }
382 }
383 } else {
384 (value.to_string(), None)
385 };
386
387 events.push(EventBinding {
388 event,
389 handler: handler_name,
390 param,
391 span: get_span(node, source),
392 });
393 continue;
394 }
395 }
396
397 if let Some((prefix, attr_name)) = name.split_once('-') {
400 if let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix) {
401 let attr_value = parse_attribute_value(value, get_span(node, source))?;
403 breakpoint_attributes
404 .entry(breakpoint)
405 .or_insert_with(HashMap::new)
406 .insert(attr_name.to_string(), attr_value);
407 continue;
408 }
409 }
410
411 let attr_value = parse_attribute_value(value, get_span(node, source))?;
413 attributes.insert(name.to_string(), attr_value);
414 }
415
416 let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
418 class_attr
419 .split_whitespace()
420 .map(|s| s.to_string())
421 .collect()
422 } else {
423 Vec::new()
424 };
425
426 let mut children = Vec::new();
428 for child in node.children() {
429 if child.node_type() == NodeType::Element {
430 children.push(parse_node(child, source)?);
431 }
432 }
433
434 if kind == WidgetKind::Tooltip {
436 validate_tooltip_children(&children, get_span(node, source))?;
437 }
438
439 if kind == WidgetKind::Canvas {
441 validate_canvas_children(&children, get_span(node, source))?;
442 }
443
444 let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
446 kind: ParseErrorKind::InvalidValue,
447 message: e,
448 span: get_span(node, source),
449 suggestion: None,
450 })?;
451 let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
452 kind: ParseErrorKind::InvalidValue,
453 message: e,
454 span: get_span(node, source),
455 suggestion: None,
456 })?;
457
458 validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
460
461 Ok(WidgetNode {
462 kind,
463 id,
464 attributes,
465 events,
466 children,
467 span: get_span(node, source),
468 style,
469 layout,
470 theme_ref: None,
471 classes,
472 breakpoint_attributes,
473 })
474}
475
476fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
478 let mut themes = HashMap::new();
479 let mut style_classes = HashMap::new();
480 let mut root_widget = None;
481 let mut global_theme = None;
482
483 for child in root.children() {
485 if child.node_type() != NodeType::Element {
486 continue;
487 }
488
489 let tag_name = child.tag_name().name();
490
491 match tag_name {
492 "themes" => {
493 for theme_node in child.children() {
495 if theme_node.node_type() == NodeType::Element
496 && theme_node.tag_name().name() == "theme"
497 {
498 let theme =
499 crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
500 let name = theme_node
501 .attribute("name")
502 .map(|s| s.to_string())
503 .unwrap_or_else(|| "default".to_string());
504 themes.insert(name, theme);
505 }
506 }
507 }
508 "style_classes" | "classes" | "styles" => {
509 for class_node in child.children() {
511 if class_node.node_type() == NodeType::Element {
512 let tag = class_node.tag_name().name();
513 if tag == "class" || tag == "style" {
514 let class = crate::parser::theme_parser::parse_style_class_from_node(
515 class_node, source,
516 )?;
517 style_classes.insert(class.name.clone(), class);
518 }
519 }
520 }
521 }
522 "global_theme" => {
523 if let Some(theme_name) = child.attribute("name") {
525 global_theme = Some(theme_name.to_string());
526 }
527 }
528 _ => {
529 if root_widget.is_some() {
531 return Err(ParseError {
532 kind: ParseErrorKind::XmlSyntax,
533 message: "Multiple root widgets found in <dampen>".to_string(),
534 span: get_span(child, source),
535 suggestion: Some("Only one root widget is allowed".to_string()),
536 });
537 }
538 root_widget = Some(parse_node(child, source)?);
539 }
540 }
541 }
542
543 let root_widget = root_widget.ok_or_else(|| ParseError {
545 kind: ParseErrorKind::XmlSyntax,
546 message: "No root widget found in <dampen>".to_string(),
547 span: get_span(root, source),
548 suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
549 })?;
550
551 Ok(DampenDocument {
552 version: SchemaVersion { major: 1, minor: 0 },
553 root: root_widget,
554 themes,
555 style_classes,
556 global_theme,
557 })
558}
559
560pub fn parse_comma_separated(value: &str) -> Vec<String> {
562 value
563 .split(',')
564 .map(|s| s.trim().to_string())
565 .filter(|s| !s.is_empty())
566 .collect()
567}
568
569pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
571where
572 T: std::str::FromStr + std::fmt::Display,
573{
574 let normalized = value.trim().to_lowercase();
575 for variant in valid_variants.iter() {
576 if variant.to_lowercase() == normalized {
577 return T::from_str(variant).map_err(|_| {
578 format!(
579 "Failed to parse '{}' as {}",
580 variant,
581 std::any::type_name::<T>()
582 )
583 });
584 }
585 }
586 Err(format!(
587 "Invalid value '{}'. Valid options: {}",
588 value,
589 valid_variants.join(", ")
590 ))
591}
592
593fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
595 if value.contains('{') && value.contains('}') {
597 let mut parts = Vec::new();
599 let mut remaining = value;
600
601 while let Some(start_pos) = remaining.find('{') {
602 if start_pos > 0 {
604 parts.push(InterpolatedPart::Literal(
605 remaining[..start_pos].to_string(),
606 ));
607 }
608
609 if let Some(end_pos) = remaining[start_pos..].find('}') {
611 let expr_start = start_pos + 1;
612 let expr_end = start_pos + end_pos;
613 let expr_str = &remaining[expr_start..expr_end];
614
615 let binding_expr = tokenize_binding_expr(
617 expr_str,
618 span.start + expr_start,
619 span.line,
620 span.column + expr_start as u32,
621 )
622 .map_err(|e| ParseError {
623 kind: ParseErrorKind::InvalidExpression,
624 message: format!("Invalid expression: {}", e),
625 span: Span::new(
626 span.start + expr_start,
627 span.start + expr_end,
628 span.line,
629 span.column + expr_start as u32,
630 ),
631 suggestion: None,
632 })?;
633
634 parts.push(InterpolatedPart::Binding(binding_expr));
635
636 remaining = &remaining[expr_end + 1..];
638 } else {
639 parts.push(InterpolatedPart::Literal(remaining.to_string()));
641 break;
642 }
643 }
644
645 if !remaining.is_empty() {
647 parts.push(InterpolatedPart::Literal(remaining.to_string()));
648 }
649
650 if parts.len() == 1 {
653 match &parts[0] {
654 InterpolatedPart::Binding(expr) => {
655 return Ok(AttributeValue::Binding(expr.clone()));
656 }
657 InterpolatedPart::Literal(lit) => {
658 return Ok(AttributeValue::Static(lit.clone()));
659 }
660 }
661 } else {
662 return Ok(AttributeValue::Interpolated(parts));
663 }
664 }
665
666 Ok(AttributeValue::Static(value.to_string()))
668}
669
670fn get_span(node: Node, source: &str) -> Span {
672 let range = node.range();
673
674 let (line, col) = calculate_line_col(source, range.start);
676
677 Span {
678 start: range.start,
679 end: range.end,
680 line,
681 column: col,
682 }
683}
684
685fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
687 let mut line = 1;
688 let mut col = 1;
689
690 for (i, c) in source.chars().enumerate() {
691 if i >= offset {
692 break;
693 }
694 if c == '\n' {
695 line += 1;
696 col = 1;
697 } else {
698 col += 1;
699 }
700 }
701
702 (line, col)
703}
704
705fn parse_layout_attributes(
707 kind: &WidgetKind,
708 attributes: &HashMap<String, AttributeValue>,
709) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
710 use crate::ir::layout::LayoutConstraints;
711 use crate::parser::style_parser::{
712 parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
713 parse_length_attr, parse_padding_attr, parse_spacing,
714 };
715
716 let mut layout = LayoutConstraints::default();
717 let mut has_any = false;
718
719 if let Some(AttributeValue::Static(value)) = attributes.get("width") {
721 layout.width = Some(parse_length_attr(value)?);
722 has_any = true;
723 }
724
725 if let Some(AttributeValue::Static(value)) = attributes.get("height") {
727 layout.height = Some(parse_length_attr(value)?);
728 has_any = true;
729 }
730
731 if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
733 layout.min_width = Some(parse_constraint(value)?);
734 has_any = true;
735 }
736
737 if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
738 layout.max_width = Some(parse_constraint(value)?);
739 has_any = true;
740 }
741
742 if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
743 layout.min_height = Some(parse_constraint(value)?);
744 has_any = true;
745 }
746
747 if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
748 layout.max_height = Some(parse_constraint(value)?);
749 has_any = true;
750 }
751
752 if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
754 layout.padding = Some(parse_padding_attr(value)?);
755 has_any = true;
756 }
757
758 if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
760 layout.spacing = Some(parse_spacing(value)?);
761 has_any = true;
762 }
763
764 if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
766 layout.align_items = Some(parse_alignment(value)?);
767 has_any = true;
768 }
769
770 if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
771 layout.justify_content = Some(parse_justification(value)?);
772 has_any = true;
773 }
774
775 if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
776 layout.align_self = Some(parse_alignment(value)?);
777 has_any = true;
778 }
779
780 if let Some(AttributeValue::Static(value)) = attributes.get("align") {
782 let alignment = parse_alignment(value)?;
783 layout.align_items = Some(alignment);
784 layout.justify_content = Some(match alignment {
785 crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
786 crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
787 crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
788 crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
789 });
790 has_any = true;
791 }
792
793 if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
795 layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
796 has_any = true;
797 }
798
799 if !matches!(kind, WidgetKind::Tooltip) {
801 if let Some(AttributeValue::Static(value)) = attributes.get("position") {
802 layout.position = Some(crate::ir::layout::Position::parse(value)?);
803 has_any = true;
804 }
805 }
806
807 if let Some(AttributeValue::Static(value)) = attributes.get("top") {
809 layout.top = Some(parse_float_attr(value, "top")?);
810 has_any = true;
811 }
812
813 if let Some(AttributeValue::Static(value)) = attributes.get("right") {
814 layout.right = Some(parse_float_attr(value, "right")?);
815 has_any = true;
816 }
817
818 if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
819 layout.bottom = Some(parse_float_attr(value, "bottom")?);
820 has_any = true;
821 }
822
823 if let Some(AttributeValue::Static(value)) = attributes.get("left") {
824 layout.left = Some(parse_float_attr(value, "left")?);
825 has_any = true;
826 }
827
828 if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
830 layout.z_index = Some(parse_int_attr(value, "z_index")?);
831 has_any = true;
832 }
833
834 if has_any {
836 layout
837 .validate()
838 .map_err(|e| format!("Layout validation failed: {}", e))?;
839 Ok(Some(layout))
840 } else {
841 Ok(None)
842 }
843}
844
845fn parse_style_attributes(
847 attributes: &HashMap<String, AttributeValue>,
848) -> Result<Option<crate::ir::style::StyleProperties>, String> {
849 use crate::parser::style_parser::{
850 build_border, build_style_properties, parse_background_attr, parse_border_color,
851 parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
852 parse_opacity, parse_shadow_attr, parse_transform,
853 };
854
855 let mut background = None;
856 let mut color = None;
857 let mut border_width = None;
858 let mut border_color = None;
859 let mut border_radius = None;
860 let mut border_style = None;
861 let mut shadow = None;
862 let mut opacity = None;
863 let mut transform = None;
864 let mut has_any = false;
865
866 if let Some(AttributeValue::Static(value)) = attributes.get("background") {
868 background = Some(parse_background_attr(value)?);
869 has_any = true;
870 }
871
872 if let Some(AttributeValue::Static(value)) = attributes.get("color") {
874 color = Some(parse_color_attr(value)?);
875 has_any = true;
876 }
877
878 if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
880 border_width = Some(parse_border_width(value)?);
881 has_any = true;
882 }
883
884 if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
885 border_color = Some(parse_border_color(value)?);
886 has_any = true;
887 }
888
889 if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
890 border_radius = Some(parse_border_radius(value)?);
891 has_any = true;
892 }
893
894 if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
895 border_style = Some(parse_border_style(value)?);
896 has_any = true;
897 }
898
899 if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
901 shadow = Some(parse_shadow_attr(value)?);
902 has_any = true;
903 }
904
905 if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
907 opacity = Some(parse_opacity(value)?);
908 has_any = true;
909 }
910
911 if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
913 transform = Some(parse_transform(value)?);
914 has_any = true;
915 }
916
917 if has_any {
918 let border = build_border(border_width, border_color, border_radius, border_style)?;
919 let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
920 Ok(Some(style))
921 } else {
922 Ok(None)
923 }
924}
925
926#[allow(dead_code)]
957pub fn validate_no_circular_dependencies(
958 _file_path: &std::path::Path,
959 _visited: &mut std::collections::HashSet<std::path::PathBuf>,
960) -> Result<(), ParseError> {
961 Ok(())
963}
964
965#[cfg(test)]
966mod circular_dependency_tests {
967 use super::*;
968 use std::collections::HashSet;
969 use std::path::PathBuf;
970
971 #[test]
972 fn test_no_circular_dependencies_without_includes() {
973 let file_path = PathBuf::from("test.dampen");
975 let mut visited = HashSet::new();
976
977 let result = validate_no_circular_dependencies(&file_path, &mut visited);
978 assert!(
979 result.is_ok(),
980 "Single file should have no circular dependencies"
981 );
982 }
983
984 }