rubble_templates_evaluators/
parser.rs1use log::{debug, trace, log_enabled, Level};
4use rubble_templates_core::ast::SyntaxNode;
5use rubble_templates_core::units::Position;
6
7pub 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}