1use pest::Parser;
2
3#[derive(pest_derive::Parser)]
4#[grammar = "parser/fncc.pest"]
5pub struct FnccParser;
6
7use pest::iterators::Pair;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct Document {
11 pub frontmatter: Option<String>,
12 pub state_type: Option<String>,
13 pub root: Element,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct Element {
18 pub name: String,
19 pub attrs: Vec<(String, AttrValue)>,
20 pub children: Vec<Node>,
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum AttrValue {
25 String(String),
26 Interpolation(String),
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub enum Node {
31 Element(Element),
32 Text(String),
33 Interpolation(String),
34}
35
36pub fn parse(source: &str) -> Result<Document, String> {
37 let mut pairs = FnccParser::parse(Rule::document, source)
38 .map_err(|e| format!("parse error: {e}"))?;
39
40 let pair = pairs.next().expect("document should exist");
41
42 let mut frontmatter = None;
43 let mut state_type = None;
44 let mut root = None;
45
46 for inner in pair.into_inner() {
47 match inner.as_rule() {
48 Rule::frontmatter => {
49 let content = inner.as_str().trim();
50 let content = content
51 .strip_prefix("---")
52 .and_then(|s| s.strip_suffix("---"))
53 .map(|s| s.trim().to_string());
54
55 if let Some(ref raw) = content {
56 let mut clean_lines = Vec::new();
58 for line in raw.lines() {
59 let trimmed = line.trim();
60 if let Some(st) = trimmed.strip_prefix("@state ") {
61 state_type = Some(st.trim().to_string());
62 } else {
63 clean_lines.push(line);
64 }
65 }
66 let clean = clean_lines.join("\n");
67 if !clean.trim().is_empty() {
68 frontmatter = Some(clean);
69 }
70 }
71 }
72 Rule::element => {
73 root = Some(parse_element(inner));
74 }
75 _ => {}
76 }
77 }
78
79 Ok(Document {
80 frontmatter,
81 state_type,
82 root: root.expect("document must have a root element"),
83 })
84}
85
86fn parse_element(pair: Pair<Rule>) -> Element {
87 let mut name = String::new();
88 let mut attrs = Vec::new();
89 let mut children = Vec::new();
90
91 for inner in pair.into_inner() {
92 match inner.as_rule() {
93 Rule::open_tag => {
94 for tag_inner in inner.into_inner() {
96 match tag_inner.as_rule() {
97 Rule::tag_name => {
98 name = tag_inner.as_str().to_string();
99 }
100 Rule::attr => {
101 let (aname, avalue) = parse_attr(tag_inner);
102 attrs.push((aname, avalue));
103 }
104 _ => {}
105 }
106 }
107 }
108 Rule::self_closing_tag => {
109 for tag_inner in inner.into_inner() {
111 match tag_inner.as_rule() {
112 Rule::tag_name => {
113 name = tag_inner.as_str().to_string();
114 }
115 Rule::attr => {
116 let (aname, avalue) = parse_attr(tag_inner);
117 attrs.push((aname, avalue));
118 }
119 _ => {}
120 }
121 }
122 }
123 Rule::children => {
124 for child in inner.into_inner() {
125 let actual = child.into_inner().next()
127 .expect("node should have one child");
128 match actual.as_rule() {
129 Rule::element => {
130 children.push(Node::Element(parse_element(actual)));
131 }
132 Rule::inner_text => {
133 let text = actual.as_str().trim().to_string();
134 if !text.is_empty() {
135 children.push(Node::Text(text));
136 }
137 }
138 Rule::interpolation => {
139 let expr = actual.as_str().trim();
140 let expr = expr.strip_prefix('{').and_then(|s| s.strip_suffix('}')).unwrap_or(expr);
141 children.push(Node::Interpolation(expr.to_string()));
142 }
143 _ => {}
144 }
145 }
146 }
147 Rule::close_tag => {
148 }
150 _ => {}
151 }
152 }
153
154 Element { name, attrs, children }
155}
156
157fn parse_attr(pair: Pair<Rule>) -> (String, AttrValue) {
158 let mut attr_name = String::new();
159 let mut attr_value = AttrValue::String(String::new());
160
161 for inner in pair.into_inner() {
162 match inner.as_rule() {
163 Rule::attr_name => {
164 attr_name = inner.as_str().to_string();
165 }
166 Rule::attr_value => {
167 let val = inner.as_str();
168 let val = val.strip_prefix('"').and_then(|s| s.strip_suffix('"')).unwrap_or(val);
169 attr_value = AttrValue::String(val.to_string());
170 }
171 Rule::interpolation => {
172 let expr = inner.as_str().trim();
173 let expr = expr.strip_prefix('{').and_then(|s| s.strip_suffix('}')).unwrap_or(expr);
174 attr_value = AttrValue::Interpolation(expr.to_string());
175 }
176 _ => {}
177 }
178 }
179
180 (attr_name, attr_value)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_simple_element() {
189 let src = "<Text size=\"xl\">hello</Text>";
190 let doc = parse(src).unwrap();
191 assert_eq!(doc.root.name, "Text");
192 assert_eq!(doc.root.attrs.len(), 1);
193 assert_eq!(doc.root.attrs[0].0, "size");
194 }
195
196 #[test]
197 fn test_frontmatter() {
198 let src = "---\nuse crate::lib::State;\n---\n<App></App>";
199 let doc = parse(src).unwrap();
200 assert_eq!(doc.frontmatter, Some("use crate::lib::State;".to_string()));
201 assert_eq!(doc.root.name, "App");
202 }
203
204 #[test]
205 fn test_interpolation() {
206 let src = "<Text>{state.msg}</Text>";
207 let doc = parse(src).unwrap();
208 assert_eq!(doc.root.children.len(), 1);
209 assert_eq!(doc.root.children[0], Node::Interpolation("state.msg".to_string()));
210 }
211
212 #[test]
213 fn test_nested_elements() {
214 let src = "<Stack direction=\"vertical\">\n <Text>oi</Text>\n</Stack>";
215 let doc = parse(src).unwrap();
216 assert_eq!(doc.root.name, "Stack");
217 assert_eq!(doc.root.children.len(), 1);
218 if let Node::Element(child) = &doc.root.children[0] {
219 assert_eq!(child.name, "Text");
220 } else {
221 panic!("expected element node");
222 }
223 }
224}