use rosace_view_syntax::{parse_str, scan_file, ViewElement, ViewLiteral};
use super::{StaticValue, Template, TemplateKey, TemplateNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Syntax(String),
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::Syntax(m) => write!(f, "view! parse error: {m}"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse_template(body_src: &str, key: TemplateKey) -> Result<Template, ParseError> {
let ast = parse_str(body_src).map_err(|e| ParseError::Syntax(e.to_string()))?;
let mut hole = 0usize;
let root = to_node(&ast, &mut hole);
Ok(Template::new(key, root))
}
pub fn parse_file_templates(src: &str, file: &str) -> Result<Vec<Template>, ParseError> {
let sites = scan_file(src).map_err(|e| ParseError::Syntax(e.to_string()))?;
let mut out = Vec::with_capacity(sites.len());
for site in sites {
let key = TemplateKey::new(file, site.line as u32, site.column as u32);
let mut hole = 0usize;
let root = to_node(&site.element, &mut hole);
out.push(Template::new(key, root));
}
Ok(out)
}
fn to_node(el: &ViewElement, hole: &mut usize) -> TemplateNode {
let mut node = TemplateNode::new(el.name_str());
for arg in &el.args {
match &arg.literal {
Some(lit) => node = node.with_arg_static(to_static(lit)),
None => {
let idx = *hole;
*hole += 1;
node = node.with_arg_hole(idx);
}
}
}
for prop in &el.props {
match &prop.literal {
Some(lit) => node = node.with_static(prop.name_str(), to_static(lit)),
None => {
let idx = *hole;
*hole += 1;
node = node.with_hole(prop.name_str(), idx);
}
}
}
for child in &el.children {
node = node.with_child(to_node(child, hole));
}
node
}
fn to_static(lit: &ViewLiteral) -> StaticValue {
match lit {
ViewLiteral::Bool(b) => StaticValue::Bool(*b),
ViewLiteral::Int(i) => StaticValue::Int(*i),
ViewLiteral::Float(f) => StaticValue::Float(*f),
ViewLiteral::Str(s) => StaticValue::Str(s.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::template::PropValue;
fn key() -> TemplateKey {
TemplateKey::new("src/edited.rs", 3, 5)
}
#[test]
fn parses_a_static_tree() {
let t = parse_template("Column { spacing: 8.0 Text { content: \"Hi\" } }", key()).unwrap();
assert_eq!(t.root.widget, "Column");
assert_eq!(t.hole_count, 0);
assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Static(StaticValue::Float(8.0))));
assert_eq!(t.root.children[0].widget, "Text");
assert_eq!(
t.root.children[0].props[0],
("content".into(), PropValue::Static(StaticValue::Str("Hi".into())))
);
}
#[test]
fn assigns_positional_holes_props_before_children() {
let t = parse_template("Column { spacing: gap Text { content: name } }", key()).unwrap();
assert_eq!(t.hole_count, 2);
assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Hole(0)));
assert_eq!(t.root.children[0].props[0], ("content".into(), PropValue::Hole(1)));
}
#[test]
fn mixed_static_and_hole() {
let t = parse_template("Column { spacing: 12.0 Text { content: title } }", key()).unwrap();
assert_eq!(t.hole_count, 1);
assert_eq!(t.root.props[0], ("spacing".into(), PropValue::Static(StaticValue::Float(12.0))));
assert_eq!(t.root.children[0].props[0], ("content".into(), PropValue::Hole(0)));
}
#[test]
fn syntax_error_is_reported_not_panicked() {
let err = parse_template("Column { : : : }", key()).unwrap_err();
assert!(matches!(err, ParseError::Syntax(_)));
}
#[test]
fn parses_all_view_sites_in_a_file_with_keys() {
let src = "\
fn a() { let x = view! { Row { spacing: 2.0 } }; }
fn b() { let y = view! { Column { Text { content: name } } }; }
";
let templates = parse_file_templates(src, "src/app.rs").unwrap();
assert_eq!(templates.len(), 2);
let row = templates.iter().find(|t| t.root.widget == "Row").unwrap();
assert_eq!(row.key.file, "src/app.rs");
assert_eq!(row.key.line, 1);
assert_eq!(row.hole_count, 0);
let col = templates.iter().find(|t| t.root.widget == "Column").unwrap();
assert_eq!(col.key.line, 2);
assert_eq!(col.hole_count, 1); assert_eq!(col.root.children[0].props[0], ("content".into(), PropValue::Hole(0)));
}
}