dampen_core/parser/
mod.rs

1pub mod attribute_standard;
2pub mod error;
3pub mod gradient;
4pub mod lexer;
5pub mod style_parser;
6pub mod theme_parser;
7
8use crate::expr::tokenize_binding_expr;
9use crate::expr::{BindingExpr, Expr, LiteralExpr};
10use crate::ir::style::StyleProperties;
11use crate::ir::theme::WidgetState;
12use crate::ir::{
13    AttributeValue, Breakpoint, DampenDocument, EventBinding, EventKind, InterpolatedPart,
14    SchemaVersion, Span, WidgetKind, WidgetNode,
15};
16use crate::parser::error::{ParseError, ParseErrorKind};
17use roxmltree::{Document, Node, NodeType};
18use std::collections::HashMap;
19
20/// Maximum schema version supported by this framework release.
21///
22/// Files declaring a version higher than this will be rejected with an error.
23/// Update this constant when the framework adds support for new schema versions.
24pub const MAX_SUPPORTED_VERSION: SchemaVersion = SchemaVersion { major: 1, minor: 0 };
25
26/// Parse a version string in "major.minor" format into a SchemaVersion.
27///
28/// # Arguments
29///
30/// * `version_str` - Raw version string from XML attribute (e.g., "1.0")
31/// * `span` - Source location for error reporting
32///
33/// # Returns
34///
35/// `Ok(SchemaVersion)` on success, `Err(ParseError)` for invalid formats.
36///
37/// # Examples
38///
39/// ```ignore
40/// let v = parse_version_string("1.0", span)?;
41/// assert_eq!(v.major, 1);
42/// assert_eq!(v.minor, 0);
43/// ```
44pub fn parse_version_string(version_str: &str, span: Span) -> Result<SchemaVersion, ParseError> {
45    let trimmed = version_str.trim();
46
47    // Reject empty strings
48    if trimmed.is_empty() {
49        return Err(ParseError {
50            kind: ParseErrorKind::InvalidValue,
51            message: "Version attribute cannot be empty".to_string(),
52            span,
53            suggestion: Some("Use format: version=\"1.0\"".to_string()),
54        });
55    }
56
57    // Split on "." and validate exactly 2 parts
58    let parts: Vec<&str> = trimmed.split('.').collect();
59    if parts.len() != 2 {
60        return Err(ParseError {
61            kind: ParseErrorKind::InvalidValue,
62            message: format!(
63                "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
64                trimmed
65            ),
66            span,
67            suggestion: Some("Use format: version=\"1.0\"".to_string()),
68        });
69    }
70
71    // Parse major version
72    let major = parts[0].parse::<u16>().map_err(|_| ParseError {
73        kind: ParseErrorKind::InvalidValue,
74        message: format!(
75            "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
76            trimmed
77        ),
78        span,
79        suggestion: Some("Use format: version=\"1.0\"".to_string()),
80    })?;
81
82    // Parse minor version
83    let minor = parts[1].parse::<u16>().map_err(|_| ParseError {
84        kind: ParseErrorKind::InvalidValue,
85        message: format!(
86            "Invalid version format '{}'. Expected 'major.minor' (e.g., '1.0')",
87            trimmed
88        ),
89        span,
90        suggestion: Some("Use format: version=\"1.0\"".to_string()),
91    })?;
92
93    Ok(SchemaVersion { major, minor })
94}
95
96/// Validate that a parsed version is supported by this framework.
97///
98/// # Arguments
99///
100/// * `version` - Parsed version to validate
101/// * `span` - Source location for error reporting
102///
103/// # Returns
104///
105/// `Ok(())` if the version is supported, `Err(ParseError)` if the version
106/// is newer than `MAX_SUPPORTED_VERSION`.
107pub fn validate_version_supported(version: &SchemaVersion, span: Span) -> Result<(), ParseError> {
108    if (version.major, version.minor) > (MAX_SUPPORTED_VERSION.major, MAX_SUPPORTED_VERSION.minor) {
109        return Err(ParseError {
110            kind: ParseErrorKind::UnsupportedVersion,
111            message: format!(
112                "Schema version {}.{} is not supported. Maximum supported version: {}.{}",
113                version.major,
114                version.minor,
115                MAX_SUPPORTED_VERSION.major,
116                MAX_SUPPORTED_VERSION.minor
117            ),
118            span,
119            suggestion: Some(format!(
120                "Upgrade dampen-core to support v{}.{}, or use version=\"{}.{}\"",
121                version.major,
122                version.minor,
123                MAX_SUPPORTED_VERSION.major,
124                MAX_SUPPORTED_VERSION.minor
125            )),
126        });
127    }
128    Ok(())
129}
130
131/// Warning about a widget requiring a higher schema version than declared.
132///
133/// This is non-blocking validation - widgets may work but compatibility is not guaranteed.
134#[derive(Debug, Clone, PartialEq)]
135pub struct ValidationWarning {
136    /// The widget that requires a higher version
137    pub widget_kind: WidgetKind,
138    /// The schema version declared in the document
139    pub declared_version: SchemaVersion,
140    /// The minimum version required by the widget
141    pub required_version: SchemaVersion,
142    /// Source location of the widget
143    pub span: Span,
144}
145
146impl ValidationWarning {
147    /// Format the warning as a human-readable message
148    pub fn format_message(&self) -> String {
149        format!(
150            "Widget '{}' requires schema v{}.{} but document declares v{}.{}",
151            widget_kind_name(&self.widget_kind),
152            self.required_version.major,
153            self.required_version.minor,
154            self.declared_version.major,
155            self.declared_version.minor
156        )
157    }
158
159    /// Get a suggestion for resolving the warning
160    pub fn suggestion(&self) -> String {
161        format!(
162            "Update to <dampen version=\"{}.{}\"> or remove this widget",
163            self.required_version.major, self.required_version.minor
164        )
165    }
166}
167
168/// Helper function to get widget kind name as string
169fn widget_kind_name(kind: &WidgetKind) -> String {
170    match kind {
171        WidgetKind::Column => "column".to_string(),
172        WidgetKind::Row => "row".to_string(),
173        WidgetKind::Container => "container".to_string(),
174        WidgetKind::Scrollable => "scrollable".to_string(),
175        WidgetKind::Stack => "stack".to_string(),
176        WidgetKind::Text => "text".to_string(),
177        WidgetKind::Image => "image".to_string(),
178        WidgetKind::Svg => "svg".to_string(),
179        WidgetKind::Button => "button".to_string(),
180        WidgetKind::TextInput => "text_input".to_string(),
181        WidgetKind::Checkbox => "checkbox".to_string(),
182        WidgetKind::Slider => "slider".to_string(),
183        WidgetKind::PickList => "pick_list".to_string(),
184        WidgetKind::Toggler => "toggler".to_string(),
185        WidgetKind::Space => "space".to_string(),
186        WidgetKind::Rule => "rule".to_string(),
187        WidgetKind::Radio => "radio".to_string(),
188        WidgetKind::ComboBox => "combobox".to_string(),
189        WidgetKind::ProgressBar => "progress_bar".to_string(),
190        WidgetKind::Tooltip => "tooltip".to_string(),
191        WidgetKind::Grid => "grid".to_string(),
192        WidgetKind::Canvas => "canvas".to_string(),
193        WidgetKind::Float => "float".to_string(),
194        WidgetKind::For => "for".to_string(),
195        WidgetKind::If => "if".to_string(),
196        WidgetKind::Custom(name) => name.clone(),
197    }
198}
199
200/// Validate that all widgets in the document are compatible with the declared schema version.
201///
202/// Returns warnings (not errors) for widgets that require a higher version than declared.
203/// This is non-blocking validation to help developers identify potential compatibility issues.
204///
205/// # Arguments
206///
207/// * `document` - The parsed document to validate
208///
209/// # Returns
210///
211/// A vector of `ValidationWarning` for widgets requiring higher versions.
212/// Empty vector means all widgets are compatible with the declared version.
213///
214/// # Examples
215///
216/// ```rust
217/// use dampen_core::{parse, validate_widget_versions};
218///
219/// let xml = r#"<dampen version="1.0"><canvas width="400" height="200" program="{chart}" /></dampen>"#;
220/// let doc = parse(xml).unwrap();
221/// let warnings = validate_widget_versions(&doc);
222/// assert_eq!(warnings.len(), 1); // Canvas requires v1.1
223/// ```
224pub fn validate_widget_versions(document: &DampenDocument) -> Vec<ValidationWarning> {
225    let mut warnings = Vec::new();
226    validate_widget_tree(&document.root, &document.version, &mut warnings);
227    warnings
228}
229
230/// Recursively validate widget tree for version compatibility
231fn validate_widget_tree(
232    node: &WidgetNode,
233    doc_version: &SchemaVersion,
234    warnings: &mut Vec<ValidationWarning>,
235) {
236    let min_version = node.kind.minimum_version();
237
238    // Check if widget requires a higher version than declared
239    if (min_version.major, min_version.minor) > (doc_version.major, doc_version.minor) {
240        warnings.push(ValidationWarning {
241            widget_kind: node.kind.clone(),
242            declared_version: *doc_version,
243            required_version: min_version,
244            span: node.span,
245        });
246    }
247
248    // Recursively check children
249    for child in &node.children {
250        validate_widget_tree(child, doc_version, warnings);
251    }
252}
253
254/// Parse XML markup into a DampenDocument.
255///
256/// This is the main entry point for the parser. It takes XML markup and
257/// converts it into the Intermediate Representation (IR) suitable for
258/// rendering or code generation.
259///
260/// # Arguments
261///
262/// * `xml` - XML markup string
263///
264/// # Returns
265///
266/// `Ok(DampenDocument)` on success, `Err(ParseError)` on failure
267///
268/// # Examples
269///
270/// ```rust
271/// use dampen_core::parse;
272///
273/// let xml = r#"<dampen><column><text value="Hello" /></column></dampen>"#;
274/// let doc = parse(xml).unwrap();
275/// assert_eq!(doc.root.children.len(), 1);
276/// ```
277///
278/// # Errors
279///
280/// Returns `ParseError` for:
281/// - Invalid XML syntax
282/// - Unknown widget elements
283/// - Invalid attribute values
284/// - Malformed binding expressions
285pub fn parse(xml: &str) -> Result<DampenDocument, ParseError> {
286    // Parse XML using roxmltree
287    let doc = Document::parse(xml).map_err(|e| ParseError {
288        kind: ParseErrorKind::XmlSyntax,
289        message: e.to_string(),
290        span: Span::new(0, 0, 1, 1),
291        suggestion: None,
292    })?;
293
294    // Find root element (skip XML declaration)
295    let root = doc.root().first_child().ok_or_else(|| ParseError {
296        kind: ParseErrorKind::XmlSyntax,
297        message: "No root element found".to_string(),
298        span: Span::new(0, 0, 1, 1),
299        suggestion: None,
300    })?;
301
302    // Check if root is <dampen> wrapper
303    let root_tag = root.tag_name().name();
304
305    if root_tag == "dampen" {
306        // Parse <dampen> document with themes and widgets
307        parse_dampen_document(root, xml)
308    } else {
309        // Parse direct widget (backward compatibility)
310        // Default to version 1.0 for backward compatibility
311        let root_widget = parse_node(root, xml)?;
312
313        Ok(DampenDocument {
314            version: SchemaVersion::default(),
315            root: root_widget,
316            themes: HashMap::new(),
317            style_classes: HashMap::new(),
318            global_theme: None,
319            follow_system: true,
320        })
321    }
322}
323
324/// Validate widget-specific required attributes
325fn validate_widget_attributes(
326    kind: &WidgetKind,
327    attributes: &std::collections::HashMap<String, AttributeValue>,
328    span: Span,
329) -> Result<(), ParseError> {
330    match kind {
331        WidgetKind::ComboBox | WidgetKind::PickList => {
332            require_non_empty_attribute(
333                kind,
334                "options",
335                attributes,
336                span,
337                "Add a comma-separated list: options=\"Option1,Option2\"",
338            )?;
339        }
340        WidgetKind::Canvas => {
341            require_attribute(
342                kind,
343                "width",
344                attributes,
345                span,
346                "Add width attribute: width=\"400\"",
347            )?;
348            require_attribute(
349                kind,
350                "height",
351                attributes,
352                span,
353                "Add height attribute: height=\"200\"",
354            )?;
355            require_attribute(
356                kind,
357                "program",
358                attributes,
359                span,
360                "Add program attribute: program=\"{{chart}}\"",
361            )?;
362
363            validate_numeric_range(kind, "width", attributes, span, 50..=4000)?;
364            validate_numeric_range(kind, "height", attributes, span, 50..=4000)?;
365        }
366        WidgetKind::Grid => {
367            require_attribute(
368                kind,
369                "columns",
370                attributes,
371                span,
372                "Add columns attribute: columns=\"5\"",
373            )?;
374            validate_numeric_range(kind, "columns", attributes, span, 1..=20)?;
375        }
376        WidgetKind::Tooltip => {
377            require_attribute(
378                kind,
379                "message",
380                attributes,
381                span,
382                "Add message attribute: message=\"Help text\"",
383            )?;
384        }
385        WidgetKind::For => {
386            require_attribute(
387                kind,
388                "each",
389                attributes,
390                span,
391                "Add each attribute: each=\"item\"",
392            )?;
393            require_attribute(
394                kind,
395                "in",
396                attributes,
397                span,
398                "Add in attribute: in=\"{items}\"",
399            )?;
400        }
401        _ => {}
402    }
403    Ok(())
404}
405
406/// Helper to require an attribute exists
407fn require_attribute(
408    kind: &WidgetKind,
409    attr_name: &str,
410    attributes: &HashMap<String, AttributeValue>,
411    span: Span,
412    suggestion: &str,
413) -> Result<(), ParseError> {
414    if !attributes.contains_key(attr_name) {
415        return Err(ParseError {
416            kind: ParseErrorKind::MissingAttribute,
417            message: format!("{:?} widget requires '{}' attribute", kind, attr_name),
418            span,
419            suggestion: Some(suggestion.to_string()),
420        });
421    }
422    Ok(())
423}
424
425/// Helper to require a non-empty attribute
426fn require_non_empty_attribute(
427    kind: &WidgetKind,
428    attr_name: &str,
429    attributes: &HashMap<String, AttributeValue>,
430    span: Span,
431    suggestion: &str,
432) -> Result<(), ParseError> {
433    match attributes.get(attr_name) {
434        Some(AttributeValue::Static(value)) if !value.trim().is_empty() => Ok(()),
435        _ => Err(ParseError {
436            kind: ParseErrorKind::MissingAttribute,
437            message: format!(
438                "{:?} widget requires '{}' attribute to be non-empty",
439                kind, attr_name
440            ),
441            span,
442            suggestion: Some(suggestion.to_string()),
443        }),
444    }
445}
446
447/// Helper to validate numeric range
448fn validate_numeric_range<T: PartialOrd + std::fmt::Display + std::str::FromStr>(
449    kind: &WidgetKind,
450    attr_name: &str,
451    attributes: &HashMap<String, AttributeValue>,
452    span: Span,
453    range: std::ops::RangeInclusive<T>,
454) -> Result<(), ParseError> {
455    if let Some(AttributeValue::Static(value_str)) = attributes.get(attr_name)
456        && let Ok(value) = value_str.parse::<T>()
457        && !range.contains(&value)
458    {
459        return Err(ParseError {
460            kind: ParseErrorKind::InvalidValue,
461            message: format!(
462                "{} for {:?} {} must be between {} and {}, found {}",
463                attr_name,
464                kind,
465                attr_name,
466                range.start(),
467                range.end(),
468                value
469            ),
470            span,
471            suggestion: Some(format!(
472                "Use {} value between {} and {}",
473                attr_name,
474                range.start(),
475                range.end()
476            )),
477        });
478    }
479    Ok(())
480}
481
482/// Validate Tooltip widget has exactly one child
483fn validate_tooltip_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
484    if children.is_empty() {
485        return Err(ParseError {
486            kind: ParseErrorKind::InvalidValue,
487            message: "Tooltip widget must have exactly one child widget".to_string(),
488            span,
489            suggestion: Some("Wrap a single widget in <tooltip></tooltip>".to_string()),
490        });
491    }
492    if children.len() > 1 {
493        return Err(ParseError {
494            kind: ParseErrorKind::InvalidValue,
495            message: format!(
496                "Tooltip widget must have exactly one child, found {}",
497                children.len()
498            ),
499            span,
500            suggestion: Some("Wrap only one widget in <tooltip></tooltip>".to_string()),
501        });
502    }
503    Ok(())
504}
505
506/// Validate Canvas widget has no children (is a leaf widget)
507fn validate_canvas_children(children: &[WidgetNode], span: Span) -> Result<(), ParseError> {
508    if !children.is_empty() {
509        return Err(ParseError {
510            kind: ParseErrorKind::InvalidValue,
511            message: format!(
512                "Canvas widget cannot have children, found {}",
513                children.len()
514            ),
515            span,
516            suggestion: Some("Canvas is a leaf widget - remove child elements".to_string()),
517        });
518    }
519    Ok(())
520}
521
522/// Parse a single XML node into a WidgetNode
523fn parse_node(node: Node, source: &str) -> Result<WidgetNode, ParseError> {
524    // Only process element nodes
525    if node.node_type() != NodeType::Element {
526        return Err(ParseError {
527            kind: ParseErrorKind::XmlSyntax,
528            message: "Expected element node".to_string(),
529            span: Span::new(0, 0, 1, 1),
530            suggestion: None,
531        });
532    }
533
534    // Get element name and map to WidgetKind
535    let tag_name = node.tag_name().name();
536    let kind = match tag_name {
537        "column" => WidgetKind::Column,
538        "row" => WidgetKind::Row,
539        "container" => WidgetKind::Container,
540        "scrollable" => WidgetKind::Scrollable,
541        "stack" => WidgetKind::Stack,
542        "text" => WidgetKind::Text,
543        "image" => WidgetKind::Image,
544        "svg" => WidgetKind::Svg,
545        "button" => WidgetKind::Button,
546        "text_input" => WidgetKind::TextInput,
547        "checkbox" => WidgetKind::Checkbox,
548        "slider" => WidgetKind::Slider,
549        "pick_list" => WidgetKind::PickList,
550        "toggler" => WidgetKind::Toggler,
551        "space" => WidgetKind::Space,
552        "rule" => WidgetKind::Rule,
553        "radio" => WidgetKind::Radio,
554        "combobox" => WidgetKind::ComboBox,
555        "progress_bar" => WidgetKind::ProgressBar,
556        "tooltip" => WidgetKind::Tooltip,
557        "grid" => WidgetKind::Grid,
558        "canvas" => WidgetKind::Canvas,
559        "float" => WidgetKind::Float,
560        "for" => WidgetKind::For,
561        "if" => WidgetKind::If,
562        unknown => {
563            return Err(ParseError {
564                kind: ParseErrorKind::UnknownWidget,
565                message: format!("Unknown widget: {}", unknown),
566                span: get_span(node, source),
567                suggestion: Some(format!(
568                    "Valid widgets are: {}",
569                    WidgetKind::all_standard().join(", ")
570                )),
571            });
572        }
573    };
574
575    // Parse attributes - separate breakpoint-prefixed and state-prefixed from regular
576    let mut attributes = std::collections::HashMap::new();
577    let mut breakpoint_attributes: HashMap<Breakpoint, HashMap<String, AttributeValue>> =
578        HashMap::new();
579    let mut inline_state_variants: HashMap<WidgetState, HashMap<String, AttributeValue>> =
580        HashMap::new();
581    let mut events = Vec::new();
582    let mut id = None;
583
584    for attr in node.attributes() {
585        // Get full attribute name (including namespace prefix if present)
586        let name = if let Some(ns) = attr.namespace() {
587            // If attribute has a Dampen state namespace, find the prefix
588            if ns.starts_with("urn:dampen:state") {
589                // Find the namespace prefix by iterating through namespace declarations
590                let prefix = node
591                    .namespaces()
592                    .find(|n| n.uri() == ns)
593                    .and_then(|n| n.name())
594                    .unwrap_or("");
595                format!("{}:{}", prefix, attr.name())
596            } else {
597                attr.name().to_string()
598            }
599        } else {
600            attr.name().to_string()
601        };
602        let value = attr.value();
603
604        // Check for id attribute
605        if name == "id" {
606            id = Some(value.to_string());
607            continue;
608        }
609
610        // Check for event attributes (on_click, on_change, etc.)
611        if name.starts_with("on_") {
612            let event_kind = match name.as_str() {
613                "on_click" => Some(EventKind::Click),
614                "on_press" => Some(EventKind::Press),
615                "on_release" => Some(EventKind::Release),
616                "on_change" => Some(EventKind::Change),
617                "on_input" => Some(EventKind::Input),
618                "on_submit" => Some(EventKind::Submit),
619                "on_select" => Some(EventKind::Select),
620                "on_toggle" => Some(EventKind::Toggle),
621                "on_scroll" => Some(EventKind::Scroll),
622                _ => None,
623            };
624
625            if let Some(event) = event_kind {
626                // Parse handler name and optional parameter
627                // Syntax: "handler_name", "handler_name:{expression}", or "handler_name:'value'"
628                let (handler_name, param) = if let Some(colon_pos) = value.find(':') {
629                    let handler = value[..colon_pos].to_string();
630                    let param_str = &value[colon_pos + 1..];
631
632                    // Check for single-quoted string: 'value'
633                    if param_str.starts_with('\'')
634                        && param_str.ends_with('\'')
635                        && param_str.len() >= 2
636                    {
637                        let quoted_value = &param_str[1..param_str.len() - 1];
638                        // Create a static string binding expression
639                        let expr = BindingExpr {
640                            expr: Expr::Literal(LiteralExpr::String(quoted_value.to_string())),
641                            span: Span::new(
642                                colon_pos + 1,
643                                colon_pos + 1 + param_str.len(),
644                                1,
645                                colon_pos as u32 + 1,
646                            ),
647                        };
648                        (handler, Some(expr))
649                    } else {
650                        // Remove surrounding braces if present: {item.id} -> item.id
651                        let param_clean = param_str.trim_matches('{').trim_matches('}');
652
653                        // Parse parameter as binding expression
654                        match crate::expr::tokenize_binding_expr(param_clean, 0, 1, 1) {
655                            Ok(expr) => (handler, Some(expr)),
656                            Err(_) => {
657                                // If parsing fails, treat the whole string as handler name
658                                (value.to_string(), None)
659                            }
660                        }
661                    }
662                } else {
663                    (value.to_string(), None)
664                };
665
666                events.push(EventBinding {
667                    event,
668                    handler: handler_name,
669                    param,
670                    span: get_span(node, source),
671                });
672                continue;
673            }
674        }
675
676        // Check for breakpoint-prefixed attributes (e.g., "mobile-spacing", "tablet-width")
677        // Note: We use hyphen instead of colon to avoid XML namespace issues
678        if let Some((prefix, attr_name)) = name.split_once('-')
679            && let Ok(breakpoint) = crate::ir::layout::Breakpoint::parse(prefix)
680        {
681            let attr_value = parse_attribute_value(value, get_span(node, source))?;
682            breakpoint_attributes
683                .entry(breakpoint)
684                .or_default()
685                .insert(attr_name.to_string(), attr_value);
686            continue;
687        }
688
689        if let Some((state_prefix, attr_name)) = name.split_once(':')
690            && let Some(state) = WidgetState::from_prefix(state_prefix)
691        {
692            let attr_value = parse_attribute_value(value, get_span(node, source))?;
693            inline_state_variants
694                .entry(state)
695                .or_default()
696                .insert(attr_name.to_string(), attr_value);
697            continue;
698        }
699        // If state prefix is invalid, log warning and treat as regular attribute
700        // TODO: Add proper logging when verbose mode is implemented
701
702        // Parse attribute value (check for bindings)
703        let attr_value = parse_attribute_value(value, get_span(node, source))?;
704        attributes.insert(name.to_string(), attr_value);
705    }
706
707    // Extract class attribute into classes field
708    let classes = if let Some(AttributeValue::Static(class_attr)) = attributes.get("class") {
709        class_attr
710            .split_whitespace()
711            .map(|s| s.to_string())
712            .collect()
713    } else {
714        Vec::new()
715    };
716
717    // Extract theme attribute into theme_ref field (supports both static and binding)
718    let theme_ref = attributes.get("theme").cloned();
719
720    // Parse children
721    let mut children = Vec::new();
722    for child in node.children() {
723        if child.node_type() == NodeType::Element {
724            children.push(parse_node(child, source)?);
725        }
726    }
727
728    // Validate Tooltip has exactly one child
729    if kind == WidgetKind::Tooltip {
730        validate_tooltip_children(&children, get_span(node, source))?;
731    }
732
733    // Validate Canvas has no children (leaf widget)
734    if kind == WidgetKind::Canvas {
735        validate_canvas_children(&children, get_span(node, source))?;
736    }
737
738    // Parse layout and style attributes into structured fields
739    let layout = parse_layout_attributes(&kind, &attributes).map_err(|e| ParseError {
740        kind: ParseErrorKind::InvalidValue,
741        message: e,
742        span: get_span(node, source),
743        suggestion: None,
744    })?;
745    let style = parse_style_attributes(&attributes).map_err(|e| ParseError {
746        kind: ParseErrorKind::InvalidValue,
747        message: e,
748        span: get_span(node, source),
749        suggestion: None,
750    })?;
751
752    // Normalize deprecated attributes to standard names (with warnings)
753    let _attr_warnings = attribute_standard::normalize_attributes(&kind, &mut attributes);
754    // TODO: Log warnings in verbose mode
755
756    // Validate widget-specific required attributes
757    validate_widget_attributes(&kind, &attributes, get_span(node, source))?;
758
759    // Convert inline_state_variants from HashMap<WidgetState, HashMap<String, AttributeValue>>
760    // to HashMap<WidgetState, StyleProperties>
761    let mut final_state_variants: HashMap<WidgetState, StyleProperties> = HashMap::new();
762    for (state, state_attrs) in inline_state_variants {
763        if let Some(state_style) = parse_style_attributes(&state_attrs).map_err(|e| ParseError {
764            kind: ParseErrorKind::InvalidValue,
765            message: format!("Invalid style in {:?} state: {}", state, e),
766            span: get_span(node, source),
767            suggestion: None,
768        })? {
769            final_state_variants.insert(state, state_style);
770        }
771    }
772
773    Ok(WidgetNode {
774        kind,
775        id,
776        attributes,
777        events,
778        children,
779        span: get_span(node, source),
780        style,
781        layout,
782        theme_ref,
783        classes,
784        breakpoint_attributes,
785        inline_state_variants: final_state_variants,
786    })
787}
788
789/// Parse a <dampen> document with themes and widgets
790fn parse_dampen_document(root: Node, source: &str) -> Result<DampenDocument, ParseError> {
791    let mut themes = HashMap::new();
792    let mut style_classes = HashMap::new();
793    let mut root_widget = None;
794    let mut global_theme = None;
795    let mut follow_system = true;
796
797    // Parse version attribute from <dampen> root element
798    let span = get_span(root, source);
799    let version = if let Some(version_attr) = root.attribute("version") {
800        let parsed = parse_version_string(version_attr, span)?;
801        validate_version_supported(&parsed, span)?;
802        parsed
803    } else {
804        // Default to version 1.0 for backward compatibility
805        SchemaVersion::default()
806    };
807
808    // Iterate through children of <dampen>
809    for child in root.children() {
810        if child.node_type() != NodeType::Element {
811            continue;
812        }
813
814        let tag_name = child.tag_name().name();
815
816        match tag_name {
817            "themes" => {
818                // Parse themes section
819                for theme_node in child.children() {
820                    if theme_node.node_type() == NodeType::Element
821                        && theme_node.tag_name().name() == "theme"
822                    {
823                        let theme =
824                            crate::parser::theme_parser::parse_theme_from_node(theme_node, source)?;
825                        let name = theme_node
826                            .attribute("name")
827                            .map(|s| s.to_string())
828                            .unwrap_or_else(|| "default".to_string());
829                        themes.insert(name, theme);
830                    }
831                }
832            }
833            "style_classes" | "classes" | "styles" => {
834                // Parse style classes
835                for class_node in child.children() {
836                    if class_node.node_type() == NodeType::Element {
837                        let tag = class_node.tag_name().name();
838                        if tag == "class" || tag == "style" {
839                            let class = crate::parser::theme_parser::parse_style_class_from_node(
840                                class_node, source,
841                            )?;
842                            style_classes.insert(class.name.clone(), class);
843                        }
844                    }
845                }
846            }
847            "global_theme" | "default_theme" => {
848                // Set global theme reference
849                if let Some(theme_name) = child.attribute("name") {
850                    global_theme = Some(theme_name.to_string());
851                }
852            }
853            "follow_system" => {
854                if let Some(enabled) = child.attribute("enabled") {
855                    follow_system = enabled.parse::<bool>().unwrap_or(true);
856                }
857            }
858            _ => {
859                // This should be a widget - parse as root
860                if root_widget.is_some() {
861                    return Err(ParseError {
862                        kind: ParseErrorKind::XmlSyntax,
863                        message: "Multiple root widgets found in <dampen>".to_string(),
864                        span: get_span(child, source),
865                        suggestion: Some("Only one root widget is allowed".to_string()),
866                    });
867                }
868                root_widget = Some(parse_node(child, source)?);
869            }
870        }
871    }
872
873    // Ensure we have a root widget, or provide a default if themes are present
874    let root_widget = if let Some(w) = root_widget {
875        w
876    } else if !themes.is_empty() || !style_classes.is_empty() {
877        // Create an empty default container if this is a theme/style-only file
878        WidgetNode::default()
879    } else {
880        return Err(ParseError {
881            kind: ParseErrorKind::XmlSyntax,
882            message: "No root widget found in <dampen>".to_string(),
883            span: get_span(root, source),
884            suggestion: Some("Add a widget like <column> or <row> inside <dampen>".to_string()),
885        });
886    };
887
888    Ok(DampenDocument {
889        version,
890        root: root_widget,
891        themes,
892        style_classes,
893        global_theme,
894        follow_system,
895    })
896}
897
898/// Parse comma-separated list into `Vec<String>`
899pub fn parse_comma_separated(value: &str) -> Vec<String> {
900    value
901        .split(',')
902        .map(|s| s.trim().to_string())
903        .filter(|s| !s.is_empty())
904        .collect()
905}
906
907/// Parse a simple enum value (case-insensitive) and return the matched variant
908pub fn parse_enum_value<T>(value: &str, valid_variants: &[&str]) -> Result<T, String>
909where
910    T: std::str::FromStr + std::fmt::Display,
911{
912    let normalized = value.trim().to_lowercase();
913    for variant in valid_variants.iter() {
914        if variant.to_lowercase() == normalized {
915            return T::from_str(variant).map_err(|_| {
916                format!(
917                    "Failed to parse '{}' as {}",
918                    variant,
919                    std::any::type_name::<T>()
920                )
921            });
922        }
923    }
924    Err(format!(
925        "Invalid value '{}'. Valid options: {}",
926        value,
927        valid_variants.join(", ")
928    ))
929}
930
931/// Parse attribute value, detecting binding expressions
932fn parse_attribute_value(value: &str, span: Span) -> Result<AttributeValue, ParseError> {
933    // Check if value contains binding syntax {expr}
934    if value.contains('{') && value.contains('}') {
935        // Parse interpolated parts
936        let mut parts = Vec::new();
937        let mut remaining = value;
938
939        while let Some(start_pos) = remaining.find('{') {
940            // Add literal before {
941            if start_pos > 0 {
942                parts.push(InterpolatedPart::Literal(
943                    remaining[..start_pos].to_string(),
944                ));
945            }
946
947            // Find closing }
948            if let Some(end_pos) = remaining[start_pos..].find('}') {
949                let expr_start = start_pos + 1;
950                let expr_end = start_pos + end_pos;
951                let expr_str = &remaining[expr_start..expr_end];
952
953                // Parse the expression
954                let binding_expr = tokenize_binding_expr(
955                    expr_str,
956                    span.start + expr_start,
957                    span.line,
958                    span.column + expr_start as u32,
959                )
960                .map_err(|e| ParseError {
961                    kind: ParseErrorKind::InvalidExpression,
962                    message: format!("Invalid expression: {}", e),
963                    span: Span::new(
964                        span.start + expr_start,
965                        span.start + expr_end,
966                        span.line,
967                        span.column + expr_start as u32,
968                    ),
969                    suggestion: None,
970                })?;
971
972                parts.push(InterpolatedPart::Binding(binding_expr));
973
974                // Move past the }
975                remaining = &remaining[expr_end + 1..];
976            } else {
977                // No closing }, treat rest as literal
978                parts.push(InterpolatedPart::Literal(remaining.to_string()));
979                break;
980            }
981        }
982
983        // Add remaining literal
984        if !remaining.is_empty() {
985            parts.push(InterpolatedPart::Literal(remaining.to_string()));
986        }
987
988        // If only one binding with no literals, return Binding
989        // If multiple parts, return Interpolated
990        if parts.len() == 1 {
991            match &parts[0] {
992                InterpolatedPart::Binding(expr) => {
993                    return Ok(AttributeValue::Binding(expr.clone()));
994                }
995                InterpolatedPart::Literal(lit) => {
996                    return Ok(AttributeValue::Static(lit.clone()));
997                }
998            }
999        } else {
1000            return Ok(AttributeValue::Interpolated(parts));
1001        }
1002    }
1003
1004    // Static value
1005    Ok(AttributeValue::Static(value.to_string()))
1006}
1007
1008/// Extract span information from roxmltree node
1009fn get_span(node: Node, source: &str) -> Span {
1010    let range = node.range();
1011
1012    // Calculate line and column from byte offset
1013    let (line, col) = calculate_line_col(source, range.start);
1014
1015    Span {
1016        start: range.start,
1017        end: range.end,
1018        line,
1019        column: col,
1020    }
1021}
1022
1023/// Calculate line and column from byte offset
1024///
1025/// Optimized to stop early once the target offset is reached.
1026fn calculate_line_col(source: &str, offset: usize) -> (u32, u32) {
1027    if offset == 0 {
1028        return (1, 1);
1029    }
1030
1031    let mut line = 1;
1032    let mut col = 1;
1033
1034    for (i, c) in source.char_indices().take(offset.saturating_add(1)) {
1035        if i >= offset {
1036            break;
1037        }
1038        if c == '\n' {
1039            line += 1;
1040            col = 1;
1041        } else {
1042            col += 1;
1043        }
1044    }
1045
1046    (line, col)
1047}
1048
1049/// Parse layout-related attributes from the attributes map
1050fn parse_layout_attributes(
1051    kind: &WidgetKind,
1052    attributes: &HashMap<String, AttributeValue>,
1053) -> Result<Option<crate::ir::layout::LayoutConstraints>, String> {
1054    use crate::ir::layout::LayoutConstraints;
1055    use crate::parser::style_parser::{
1056        parse_alignment, parse_constraint, parse_float_attr, parse_int_attr, parse_justification,
1057        parse_length_attr, parse_padding_attr, parse_spacing,
1058    };
1059
1060    let mut layout = LayoutConstraints::default();
1061    let mut has_any = false;
1062
1063    // Parse width
1064    if let Some(AttributeValue::Static(value)) = attributes.get("width") {
1065        layout.width = Some(parse_length_attr(value)?);
1066        has_any = true;
1067    }
1068
1069    // Parse height
1070    if let Some(AttributeValue::Static(value)) = attributes.get("height") {
1071        layout.height = Some(parse_length_attr(value)?);
1072        has_any = true;
1073    }
1074
1075    // Parse min/max constraints
1076    if let Some(AttributeValue::Static(value)) = attributes.get("min_width") {
1077        layout.min_width = Some(parse_constraint(value)?);
1078        has_any = true;
1079    }
1080
1081    if let Some(AttributeValue::Static(value)) = attributes.get("max_width") {
1082        layout.max_width = Some(parse_constraint(value)?);
1083        has_any = true;
1084    }
1085
1086    if let Some(AttributeValue::Static(value)) = attributes.get("min_height") {
1087        layout.min_height = Some(parse_constraint(value)?);
1088        has_any = true;
1089    }
1090
1091    if let Some(AttributeValue::Static(value)) = attributes.get("max_height") {
1092        layout.max_height = Some(parse_constraint(value)?);
1093        has_any = true;
1094    }
1095
1096    // Parse padding
1097    if let Some(AttributeValue::Static(value)) = attributes.get("padding") {
1098        layout.padding = Some(parse_padding_attr(value)?);
1099        has_any = true;
1100    }
1101
1102    // Parse spacing
1103    if let Some(AttributeValue::Static(value)) = attributes.get("spacing") {
1104        layout.spacing = Some(parse_spacing(value)?);
1105        has_any = true;
1106    }
1107
1108    // Parse alignment
1109    if let Some(AttributeValue::Static(value)) = attributes.get("align_items") {
1110        layout.align_items = Some(parse_alignment(value)?);
1111        has_any = true;
1112    }
1113
1114    if let Some(AttributeValue::Static(value)) = attributes.get("justify_content") {
1115        layout.justify_content = Some(parse_justification(value)?);
1116        has_any = true;
1117    }
1118
1119    if let Some(AttributeValue::Static(value)) = attributes.get("align_self") {
1120        layout.align_self = Some(parse_alignment(value)?);
1121        has_any = true;
1122    }
1123
1124    // Parse direct alignment (align_x, align_y)
1125    if let Some(AttributeValue::Static(value)) = attributes.get("align_x") {
1126        layout.align_x = Some(parse_alignment(value)?);
1127        has_any = true;
1128    }
1129
1130    if let Some(AttributeValue::Static(value)) = attributes.get("align_y") {
1131        layout.align_y = Some(parse_alignment(value)?);
1132        has_any = true;
1133    }
1134
1135    // Parse align shorthand (sets both align_items and justify_content)
1136    if let Some(AttributeValue::Static(value)) = attributes.get("align") {
1137        let alignment = parse_alignment(value)?;
1138        layout.align_items = Some(alignment);
1139        layout.justify_content = Some(match alignment {
1140            crate::ir::layout::Alignment::Start => crate::ir::layout::Justification::Start,
1141            crate::ir::layout::Alignment::Center => crate::ir::layout::Justification::Center,
1142            crate::ir::layout::Alignment::End => crate::ir::layout::Justification::End,
1143            crate::ir::layout::Alignment::Stretch => crate::ir::layout::Justification::Center,
1144        });
1145        has_any = true;
1146    }
1147
1148    // Parse direction
1149    if let Some(AttributeValue::Static(value)) = attributes.get("direction") {
1150        layout.direction = Some(crate::ir::layout::Direction::parse(value)?);
1151        has_any = true;
1152    }
1153
1154    // Parse position (skip for Tooltip - it has its own position attribute)
1155    if !matches!(kind, WidgetKind::Tooltip)
1156        && let Some(AttributeValue::Static(value)) = attributes.get("position")
1157    {
1158        layout.position = Some(crate::ir::layout::Position::parse(value)?);
1159        has_any = true;
1160    }
1161
1162    // Parse position offsets
1163    if let Some(AttributeValue::Static(value)) = attributes.get("top") {
1164        layout.top = Some(parse_float_attr(value, "top")?);
1165        has_any = true;
1166    }
1167
1168    if let Some(AttributeValue::Static(value)) = attributes.get("right") {
1169        layout.right = Some(parse_float_attr(value, "right")?);
1170        has_any = true;
1171    }
1172
1173    if let Some(AttributeValue::Static(value)) = attributes.get("bottom") {
1174        layout.bottom = Some(parse_float_attr(value, "bottom")?);
1175        has_any = true;
1176    }
1177
1178    if let Some(AttributeValue::Static(value)) = attributes.get("left") {
1179        layout.left = Some(parse_float_attr(value, "left")?);
1180        has_any = true;
1181    }
1182
1183    // Parse z-index
1184    if let Some(AttributeValue::Static(value)) = attributes.get("z_index") {
1185        layout.z_index = Some(parse_int_attr(value, "z_index")?);
1186        has_any = true;
1187    }
1188
1189    // Validate the layout
1190    if has_any {
1191        layout
1192            .validate()
1193            .map_err(|e| format!("Layout validation failed: {}", e))?;
1194        Ok(Some(layout))
1195    } else {
1196        Ok(None)
1197    }
1198}
1199
1200/// Parse style-related attributes from the attributes map
1201fn parse_style_attributes(
1202    attributes: &HashMap<String, AttributeValue>,
1203) -> Result<Option<crate::ir::style::StyleProperties>, String> {
1204    use crate::parser::style_parser::{
1205        build_border, build_style_properties, parse_background_attr, parse_border_color,
1206        parse_border_radius, parse_border_style, parse_border_width, parse_color_attr,
1207        parse_opacity, parse_shadow_attr, parse_transform,
1208    };
1209
1210    let mut background = None;
1211    let mut color = None;
1212    let mut border_width = None;
1213    let mut border_color = None;
1214    let mut border_radius = None;
1215    let mut border_style = None;
1216    let mut shadow = None;
1217    let mut opacity = None;
1218    let mut transform = None;
1219    let mut has_any = false;
1220
1221    // Parse background
1222    if let Some(AttributeValue::Static(value)) = attributes.get("background") {
1223        background = Some(parse_background_attr(value)?);
1224        has_any = true;
1225    }
1226
1227    // Parse color
1228    if let Some(AttributeValue::Static(value)) = attributes.get("color") {
1229        color = Some(parse_color_attr(value)?);
1230        has_any = true;
1231    }
1232
1233    // Parse border attributes
1234    if let Some(AttributeValue::Static(value)) = attributes.get("border_width") {
1235        border_width = Some(parse_border_width(value)?);
1236        has_any = true;
1237    }
1238
1239    if let Some(AttributeValue::Static(value)) = attributes.get("border_color") {
1240        border_color = Some(parse_border_color(value)?);
1241        has_any = true;
1242    }
1243
1244    if let Some(AttributeValue::Static(value)) = attributes.get("border_radius") {
1245        border_radius = Some(parse_border_radius(value)?);
1246        has_any = true;
1247    }
1248
1249    if let Some(AttributeValue::Static(value)) = attributes.get("border_style") {
1250        border_style = Some(parse_border_style(value)?);
1251        has_any = true;
1252    }
1253
1254    // Parse shadow
1255    if let Some(AttributeValue::Static(value)) = attributes.get("shadow") {
1256        shadow = Some(parse_shadow_attr(value)?);
1257        has_any = true;
1258    }
1259
1260    // Parse opacity
1261    if let Some(AttributeValue::Static(value)) = attributes.get("opacity") {
1262        opacity = Some(parse_opacity(value)?);
1263        has_any = true;
1264    }
1265
1266    // Parse transform
1267    if let Some(AttributeValue::Static(value)) = attributes.get("transform") {
1268        transform = Some(parse_transform(value)?);
1269        has_any = true;
1270    }
1271
1272    if has_any {
1273        let border = build_border(border_width, border_color, border_radius, border_style)?;
1274        let style = build_style_properties(background, color, border, shadow, opacity, transform)?;
1275        Ok(Some(style))
1276    } else {
1277        Ok(None)
1278    }
1279}
1280
1281/// Validates that there are no circular dependencies in UI file includes.
1282///
1283/// **Feature T125 - Not Yet Implemented**
1284///
1285/// Currently, Dampen does not support file includes/imports in XML.
1286/// This function is a placeholder that will be implemented when UI file
1287/// composition is added.
1288///
1289/// # Returns
1290///
1291/// Currently returns `Ok(())` since includes are not supported.
1292/// Once implemented, returns `Err(ParseError)` for circular dependencies.
1293pub fn validate_no_circular_dependencies(
1294    _file_path: &std::path::Path,
1295    _visited: &mut std::collections::HashSet<std::path::PathBuf>,
1296) -> Result<(), ParseError> {
1297    Ok(())
1298}
1299
1300#[cfg(test)]
1301mod circular_dependency_tests {
1302    use super::*;
1303    use std::collections::HashSet;
1304    use std::path::PathBuf;
1305
1306    #[test]
1307    fn test_no_circular_dependencies_without_includes() {
1308        // T125: Validate that single files have no circular dependencies
1309        let file_path = PathBuf::from("test.dampen");
1310        let mut visited = HashSet::new();
1311
1312        let result = validate_no_circular_dependencies(&file_path, &mut visited);
1313        assert!(
1314            result.is_ok(),
1315            "Single file should have no circular dependencies"
1316        );
1317    }
1318
1319    // Future tests when includes are supported:
1320    // - test_detect_simple_circular_dependency: A -> B -> A
1321    // - test_detect_complex_circular_dependency: A -> B -> C -> D -> B
1322    // - test_allow_diamond_dependencies: A->B, A->C, B->D, C->D (this is OK, not circular)
1323    // - test_self_include_rejected: A -> A
1324}
1325
1326#[cfg(test)]
1327mod inline_state_styles_tests {
1328    use super::*;
1329    use crate::ir::theme::WidgetState;
1330
1331    #[test]
1332    fn test_parse_single_state_attribute() {
1333        // T011: Parse button with single hover:background state attribute
1334        // Note: XML requires namespace declaration for colons in attribute names
1335        let xml = r##"
1336            <dampen version="1.0" xmlns:hover="urn:dampen:state:hover">
1337                <button label="Click" hover:background="#ff0000" />
1338            </dampen>
1339        "##;
1340
1341        let result = parse(xml);
1342        assert!(result.is_ok(), "Should parse valid XML with hover state");
1343
1344        let doc = result.unwrap();
1345        let button = &doc.root;
1346
1347        // Verify inline_state_variants contains hover state
1348        assert!(
1349            button
1350                .inline_state_variants
1351                .contains_key(&WidgetState::Hover),
1352            "Should have hover state variant"
1353        );
1354
1355        let hover_style = button
1356            .inline_state_variants
1357            .get(&WidgetState::Hover)
1358            .unwrap();
1359
1360        // Verify hover background color is red
1361        assert!(
1362            hover_style.background.is_some(),
1363            "Hover state should have background"
1364        );
1365    }
1366
1367    #[test]
1368    fn test_parse_multiple_state_attributes() {
1369        // T012: Parse button with multiple state attributes
1370        // Note: Each state needs its own unique namespace URI to avoid attribute conflicts
1371        let xml = r##"
1372            <dampen version="1.0"
1373                xmlns:hover="urn:dampen:state:hover"
1374                xmlns:active="urn:dampen:state:active"
1375                xmlns:disabled="urn:dampen:state:disabled">
1376                <button
1377                    label="Click"
1378                    hover:background="#ff0000"
1379                    active:background="#00ff00"
1380                    disabled:opacity="0.5"
1381                />
1382            </dampen>
1383        "##;
1384
1385        let result = parse(xml);
1386        assert!(
1387            result.is_ok(),
1388            "Should parse valid XML with multiple states"
1389        );
1390
1391        let doc = result.unwrap();
1392        let button = &doc.root;
1393
1394        // Verify all three state variants exist
1395        assert!(
1396            button
1397                .inline_state_variants
1398                .contains_key(&WidgetState::Hover),
1399            "Should have hover state"
1400        );
1401        assert!(
1402            button
1403                .inline_state_variants
1404                .contains_key(&WidgetState::Active),
1405            "Should have active state"
1406        );
1407        assert!(
1408            button
1409                .inline_state_variants
1410                .contains_key(&WidgetState::Disabled),
1411            "Should have disabled state"
1412        );
1413
1414        // Verify hover has background
1415        let hover_style = button
1416            .inline_state_variants
1417            .get(&WidgetState::Hover)
1418            .unwrap();
1419        assert!(
1420            hover_style.background.is_some(),
1421            "Hover state should have background"
1422        );
1423
1424        // Verify active has background
1425        let active_style = button
1426            .inline_state_variants
1427            .get(&WidgetState::Active)
1428            .unwrap();
1429        assert!(
1430            active_style.background.is_some(),
1431            "Active state should have background"
1432        );
1433
1434        // Verify disabled has opacity
1435        let disabled_style = button
1436            .inline_state_variants
1437            .get(&WidgetState::Disabled)
1438            .unwrap();
1439        assert!(
1440            disabled_style.opacity.is_some(),
1441            "Disabled state should have opacity"
1442        );
1443    }
1444
1445    #[test]
1446    fn test_parse_invalid_state_prefix() {
1447        // T013: Parse button with invalid state prefix should treat as regular attribute
1448        let xml = r##"
1449            <dampen version="1.0" xmlns:unknown="urn:dampen:state:unknown">
1450                <button label="Click" unknown:background="#ff0000" />
1451            </dampen>
1452        "##;
1453
1454        let result = parse(xml);
1455        assert!(
1456            result.is_ok(),
1457            "Should parse with warning for invalid state"
1458        );
1459
1460        let doc = result.unwrap();
1461        let button = &doc.root;
1462
1463        // Verify inline_state_variants is empty (invalid prefix ignored)
1464        assert!(
1465            button.inline_state_variants.is_empty(),
1466            "Should have no state variants for invalid prefix"
1467        );
1468
1469        // Verify unknown:background is treated as regular attribute
1470        assert!(
1471            button.attributes.contains_key("unknown:background"),
1472            "Invalid state prefix should be treated as regular attribute"
1473        );
1474    }
1475}