Skip to main content

rosace_widgets/template/
parse.rs

1//! The runtime `view!` parser (D103 / D102 Tier 1 — rollout step 4).
2//!
3//! Turns edited `view!` **source text** into a [`Template`] at runtime, without
4//! the compiler — the piece that lets a dev watcher pick up an edit and diff it
5//! against the running shape. It parses through the SAME grammar crate
6//! (`rosace-view-syntax`) the compile-time macro uses, then converts the AST to
7//! a `Template` with the SAME rules the macro's descriptor codegen uses
8//! (literals → [`StaticValue`], non-literals → positional [`PropValue::Hole`],
9//! props before children). Sharing the grammar is what guarantees the runtime
10//! template matches what the binary was compiled with (see the equivalence
11//! test in `rosace/tests/view_template.rs`).
12
13use rosace_view_syntax::{parse_str, scan_file, ViewElement, ViewLiteral};
14
15use super::{StaticValue, Template, TemplateKey, TemplateNode};
16
17/// Why parsing a `view!` body failed at runtime.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum ParseError {
20    /// The body did not parse as `view!` syntax (the message is syn's).
21    Syntax(String),
22}
23
24impl std::fmt::Display for ParseError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            ParseError::Syntax(m) => write!(f, "view! parse error: {m}"),
28        }
29    }
30}
31impl std::error::Error for ParseError {}
32
33/// Parse a `view!` body (the text inside `view! { … }`) into a [`Template`]
34/// keyed by `key`. The hole indices it assigns match the compile-time macro's,
35/// so the running binary's compiled hole array lines up slot-for-slot.
36pub fn parse_template(body_src: &str, key: TemplateKey) -> Result<Template, ParseError> {
37    let ast = parse_str(body_src).map_err(|e| ParseError::Syntax(e.to_string()))?;
38    let mut hole = 0usize;
39    let root = to_node(&ast, &mut hole);
40    Ok(Template::new(key, root))
41}
42
43/// Parse EVERY `view!` in a source file into a keyed [`Template`] — what a dev
44/// watcher calls on a changed `.rs`. Each template's key is `(file, line, col)`
45/// of its `view!` site, so the runtime can match it to the running template.
46///
47/// Matching note: `col` here is the `view` token's 0-based column (proc-macro2),
48/// while the macro's `TemplateKey` column comes from `column!()`; match
49/// primarily on `(file, line)` and treat column as a tiebreaker.
50pub fn parse_file_templates(src: &str, file: &str) -> Result<Vec<Template>, ParseError> {
51    let sites = scan_file(src).map_err(|e| ParseError::Syntax(e.to_string()))?;
52    let mut out = Vec::with_capacity(sites.len());
53    for site in sites {
54        let key = TemplateKey::new(file, site.line as u32, site.column as u32);
55        let mut hole = 0usize;
56        let root = to_node(&site.element, &mut hole);
57        out.push(Template::new(key, root));
58    }
59    Ok(out)
60}
61
62fn to_node(el: &ViewElement, hole: &mut usize) -> TemplateNode {
63    let mut node = TemplateNode::new(el.name_str());
64    // Positional constructor args first (they take the earliest hole slots),
65    // matching the compile-time macro's traversal order.
66    for arg in &el.args {
67        match &arg.literal {
68            Some(lit) => node = node.with_arg_static(to_static(lit)),
69            None => {
70                let idx = *hole;
71                *hole += 1;
72                node = node.with_arg_hole(idx);
73            }
74        }
75    }
76    for prop in &el.props {
77        match &prop.literal {
78            Some(lit) => node = node.with_static(prop.name_str(), to_static(lit)),
79            None => {
80                let idx = *hole;
81                *hole += 1;
82                node = node.with_hole(prop.name_str(), idx);
83            }
84        }
85    }
86    for child in &el.children {
87        node = node.with_child(to_node(child, hole));
88    }
89    node
90}
91
92fn to_static(lit: &ViewLiteral) -> StaticValue {
93    match lit {
94        ViewLiteral::Bool(b) => StaticValue::Bool(*b),
95        ViewLiteral::Int(i) => StaticValue::Int(*i),
96        ViewLiteral::Float(f) => StaticValue::Float(*f),
97        ViewLiteral::Str(s) => StaticValue::Str(s.clone()),
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::template::PropValue;
105
106    fn key() -> TemplateKey {
107        TemplateKey::new("src/edited.rs", 3, 5)
108    }
109
110    #[test]
111    fn parses_a_static_tree() {
112        let t = parse_template("Column { spacing: 8.0 Text { content: \"Hi\" } }", key()).unwrap();
113        assert_eq!(t.root.widget, "Column");
114        assert_eq!(t.hole_count, 0);
115        assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Static(StaticValue::Float(8.0))));
116        assert_eq!(t.root.children[0].widget, "Text");
117        assert_eq!(
118            t.root.children[0].props[0],
119            ("content".into(), PropValue::Static(StaticValue::Str("Hi".into())))
120        );
121    }
122
123    #[test]
124    fn assigns_positional_holes_props_before_children() {
125        let t = parse_template("Column { spacing: gap Text { content: name } }", key()).unwrap();
126        assert_eq!(t.hole_count, 2);
127        // Column.spacing is hole 0 (a prop, visited before children)...
128        assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Hole(0)));
129        // ...Text.content is hole 1.
130        assert_eq!(t.root.children[0].props[0], ("content".into(), PropValue::Hole(1)));
131    }
132
133    #[test]
134    fn mixed_static_and_hole() {
135        let t = parse_template("Column { spacing: 12.0 Text { content: title } }", key()).unwrap();
136        assert_eq!(t.hole_count, 1);
137        assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Static(StaticValue::Float(12.0))));
138        assert_eq!(t.root.children[0].props[0], ("content".into(), PropValue::Hole(0)));
139    }
140
141    #[test]
142    fn syntax_error_is_reported_not_panicked() {
143        let err = parse_template("Column { : : : }", key()).unwrap_err();
144        assert!(matches!(err, ParseError::Syntax(_)));
145    }
146
147    #[test]
148    fn parses_all_view_sites_in_a_file_with_keys() {
149        let src = "\
150fn a() { let x = view! { Row { spacing: 2.0 } }; }
151fn b() { let y = view! { Column { Text { content: name } } }; }
152";
153        let templates = parse_file_templates(src, "src/app.rs").unwrap();
154        assert_eq!(templates.len(), 2);
155
156        let row = templates.iter().find(|t| t.root.widget == "Row").unwrap();
157        assert_eq!(row.key.file, "src/app.rs");
158        assert_eq!(row.key.line, 1);
159        assert_eq!(row.hole_count, 0);
160
161        let col = templates.iter().find(|t| t.root.widget == "Column").unwrap();
162        assert_eq!(col.key.line, 2);
163        assert_eq!(col.hole_count, 1); // `name` is a hole
164        assert_eq!(col.root.children[0].props[0], ("content".into(), PropValue::Hole(0)));
165    }
166}