rubble_templates_evaluators/
parser.rs

1//! Default parser logic
2
3use log::{debug, trace, log_enabled, Level};
4use rubble_templates_core::ast::SyntaxNode;
5use rubble_templates_core::units::Position;
6
7/// Used for parsing AST for further evaluation.
8///
9/// This function tries to parse AST assuming it is Lisp-like syntax, which is practically
10/// `function arg0 arg1 arg2`, where `function` is the function name, and `arg0...` are the arguments.
11///
12/// It also allows to use parenthesis to evaluate a nested function.
13///
14/// Reserved characters (cannot be used in names):
15/// * ` ` - space
16/// * `(` - left parenthesis
17/// * `)` - right parenthesis
18///
19/// Examples:
20/// * `(function 1 2 3)` - interpreted as `function` call with parameters `1`, `2` and `3`
21/// * `plus 1 2 (times 3 4)` - interpreted as `1 + 2 + (3 * 4)`, given `plus` is an addition function and `times` is a multiplication function
22///
23pub fn parse_ast(source: &str, code_start: &str, code_end: &str) -> SyntaxNode {
24    if log_enabled!(Level::Debug) {
25        debug!("Starting to parse AST of: {}", source);
26    }
27    let source = source.strip_prefix(code_start).unwrap_or(source);
28    let source = source.strip_suffix(code_end).unwrap_or(source);
29
30    let result = next_node_of(source, 0, 0).0;
31    if log_enabled!(Level::Debug) {
32        debug!("Parsing complete, result: {:?}", result);
33    }
34    result
35}
36
37struct SyntaxScanResult(SyntaxNode, usize);
38
39fn next_node_of(source: &str, offset: usize, level: usize) -> SyntaxScanResult {
40    if log_enabled!(Level::Trace) {
41        trace!("{:->width$}>{}", "", source, width = level);
42    }
43    let mut syntax_node = SyntaxNode::AnonymousNode {
44        children: vec![],
45        starts_at: Position::RelativeToCodeStart(offset),
46    };
47    let mut identifier = "".to_string();
48    let mut string_started = false;
49    let mut identifier_start: usize = offset;
50    let mut skip_end: usize = 0;
51    let mut source_length: usize = 0;
52
53    for (index, char) in source.chars().enumerate() {
54        let position = offset + index;
55        source_length += 1;
56        if position <= skip_end {
57            continue;
58        }
59        let current_offset = index + 1;
60
61        let (id, started) = extract_string(identifier, char, string_started);
62        identifier = id;
63        string_started = started;
64        if string_started {
65            continue;
66        }
67
68        if char == '(' {
69            let (new_node, skip_pos) = start_node(syntax_node, &identifier, &source[current_offset..], identifier_start + 1, position, level);
70            syntax_node = new_node;
71            skip_end = skip_pos;
72        } else {
73            if char == ' ' || char == ')' {
74                syntax_node = add_identifier_or_child(
75                    syntax_node,
76                    &identifier,
77                    identifier_start + 1,
78                    level,
79                );
80                identifier.clear();
81                identifier_start = position + 1;
82            } else {
83                identifier.push(char);
84            }
85
86            if char == ')' {
87                if log_enabled!(Level::Trace) {
88                    trace!("{:->width$}Parsing of the following fragment is complete (finished at {}): {}, result: {:?}", "", position, source, syntax_node, width = level);
89                }
90                return SyntaxScanResult(syntax_node, position);
91            }
92        }
93    }
94
95    let end_pos = offset + source_length;
96    if log_enabled!(Level::Trace) {
97        trace!("{:->width$}Parsing of the following fragment is complete (finished at {}): {}, result: {:?}", "", end_pos, source, syntax_node, width = level);
98    }
99    SyntaxScanResult(syntax_node, end_pos)
100}
101
102fn extract_string(identifier: String, char: char, string_started: bool) -> (String, bool) {
103    let mut identifier = identifier;
104    let mut string_started = string_started;
105
106    if string_started && char != '"' {
107        identifier.push(char);
108    } else if string_started {
109        string_started = false;
110    } else if char == '"' {
111        identifier.push(char);
112        string_started = true;
113    }
114
115    (identifier, string_started)
116}
117
118fn start_node(syntax_node: SyntaxNode, identifier: &str, source_remainder: &str, identifier_start: usize, position: usize, level: usize) -> (SyntaxNode, usize) {
119    let mut syntax_node = syntax_node;
120    syntax_node = add_identifier_or_child(
121        syntax_node,
122        identifier,
123        identifier_start,
124        level,
125    );
126
127    let SyntaxScanResult(child, skip_pos) = next_node_of(source_remainder, position, level + 1);
128    syntax_node = syntax_node.add_child(child);
129
130    (syntax_node, skip_pos)
131}
132
133fn add_identifier_or_child(syntax_node: SyntaxNode, new_identifier: &str, identifier_starts_at: usize, level: usize) -> SyntaxNode {
134    if new_identifier.is_empty() {
135        return syntax_node;
136    }
137
138    trace!("{:->width$}+\"{}\" at {}", "", new_identifier, identifier_starts_at, width = level);
139
140    if syntax_node.is_anonymous() {
141        syntax_node.with_identifier(new_identifier, Position::RelativeToCodeStart(identifier_starts_at))
142
143    } else {
144        if log_enabled!(Level::Trace) {
145            trace!("{:->width$}-\"{}\" at {} (child of {})", "", new_identifier, identifier_starts_at, syntax_node, width = level);
146        }
147        syntax_node.add_child(SyntaxNode::NamedNode {
148                identifier: new_identifier.to_string(),
149                children: vec![],
150                starts_at: Position::RelativeToCodeStart(identifier_starts_at),
151            })
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use rubble_templates_core::ast::SyntaxNode::{AnonymousNode, NamedNode};
158    use log::LevelFilter;
159    use rubble_templates_core::units::Position;
160    use crate::parser::parse_ast;
161
162    fn init() {
163        let _ = env_logger::builder()
164            .filter_level(LevelFilter::Trace)
165            .is_test(true)
166            .try_init();
167    }
168
169    #[test]
170    fn should_parse_ast() {
171        init();
172
173        let input = "{{ (list 1 2 (if a b c)) }}";
174        let actual = parse_ast(input,"{{", "}}");
175
176        let expected = AnonymousNode {
177            starts_at: Position::RelativeToCodeStart(0),
178            children: vec![
179                NamedNode {
180                    identifier: "list".to_string(),
181                    starts_at: Position::RelativeToCodeStart(2),
182                    children: vec![
183                        NamedNode {
184                            identifier: "1".to_string(),
185                            children: vec![],
186                            starts_at: Position::RelativeToCodeStart(7),
187                        },
188                        NamedNode {
189                            identifier: "2".to_string(),
190                            starts_at: Position::RelativeToCodeStart(9),
191                            children: vec![],
192                        },
193                        NamedNode {
194                            identifier: "if".to_string(),
195                            starts_at: Position::RelativeToCodeStart(11),
196                            children: vec![
197                                NamedNode {
198                                    identifier: "a".to_string(),
199                                    starts_at: Position::RelativeToCodeStart(14),
200                                    children: vec![],
201                                },
202                                NamedNode {
203                                    identifier: "b".to_string(),
204                                    starts_at: Position::RelativeToCodeStart(16),
205                                    children: vec![],
206                                },
207                                NamedNode {
208                                    identifier: "c".to_string(),
209                                    starts_at: Position::RelativeToCodeStart(18),
210                                    children: vec![],
211                                },
212                            ],
213                        },
214                    ],
215                },
216            ],
217        };
218        assert_eq!(expected, actual);
219    }
220}