Skip to main content

fncc_core/
parser.rs

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                    // extract @state directive
57                    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                // extract tag_name and attrs from open_tag
95                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                // self-closing tag: extract tag_name and attrs
110                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                    // children contains nodes; unwrap node.inner
126                    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                // nothing to extract from close_tag for now
149            }
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}