rosace_widgets/template/
parse.rs1use rosace_view_syntax::{parse_str, scan_file, ViewElement, ViewLiteral};
14
15use super::{StaticValue, Template, TemplateKey, TemplateNode};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum ParseError {
20 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
33pub 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
43pub 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 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 assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Hole(0)));
129 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); assert_eq!(col.root.children[0].props[0], ("content".into(), PropValue::Hole(0)));
165 }
166}