webui_parser/
handlebars_parser.rs1use crate::Result;
5use webui_protocol::WebUIFragment;
6
7pub struct HandlebarsParser;
9
10impl HandlebarsParser {
11 pub fn new() -> Self {
13 Self
14 }
15
16 pub fn parse(&self, text: &str) -> Result<Vec<WebUIFragment>> {
23 let bytes = text.as_bytes();
24 let len = bytes.len();
25 let mut fragments = Vec::new();
26 let mut raw_buf = String::new();
27 let mut pos = 0;
28
29 while pos < len {
30 let remaining = &text[pos..];
31 let Some(offset) = remaining.find("{{") else {
32 raw_buf.push_str(&text[pos..]);
33 break;
34 };
35
36 let start = pos + offset;
37 if start > pos {
38 raw_buf.push_str(&text[pos..start]);
39 }
40
41 let mut brace_count = 0;
43 let mut i = start;
44 while i < len && bytes[i] == b'{' {
45 brace_count += 1;
46 i += 1;
47 }
48
49 if brace_count >= 3 {
50 if let Some(end_offset) = text[i..].find("}}}") {
52 let var_end = i + end_offset;
53 let var_name = text[i..var_end].trim();
54 if !var_name.is_empty() {
55 for _ in 0..(brace_count - 3) {
57 raw_buf.push('{');
58 }
59 if !raw_buf.is_empty() {
60 fragments.push(WebUIFragment::raw(std::mem::take(&mut raw_buf)));
61 }
62 fragments.push(WebUIFragment::signal(var_name.to_string(), true));
63 pos = var_end + 3;
64 continue;
65 }
66 }
67
68 if brace_count >= 5 {
70 if let Some(end_offset) = text[i..].find("}}") {
71 let var_end = i + end_offset;
72 let var_name = text[i..var_end].trim();
73 if !var_name.is_empty() {
74 for _ in 0..(brace_count - 2) {
75 raw_buf.push('{');
76 }
77 if !raw_buf.is_empty() {
78 fragments.push(WebUIFragment::raw(std::mem::take(&mut raw_buf)));
79 }
80 fragments.push(WebUIFragment::signal(var_name.to_string(), false));
81 pos = var_end + 2;
82 continue;
83 }
84 }
85 }
86
87 if let Some(end_offset) = text[i..].find("}}") {
89 let raw_end = i + end_offset + 2;
90 raw_buf.push_str(&text[start..raw_end]);
91 pos = raw_end;
92 } else {
93 raw_buf.push_str(&text[start..]);
94 pos = len;
95 }
96 continue;
97 }
98
99 if let Some(end_offset) = text[i..].find("}}") {
101 let var_end = i + end_offset;
102 let var_name = text[i..var_end].trim();
103 if !var_name.is_empty() {
104 if !raw_buf.is_empty() {
105 fragments.push(WebUIFragment::raw(std::mem::take(&mut raw_buf)));
106 }
107 fragments.push(WebUIFragment::signal(var_name.to_string(), false));
108 pos = var_end + 2;
109 continue;
110 }
111 }
112
113 raw_buf.push_str(&text[start..]);
115 pos = len;
116 }
117
118 if !raw_buf.is_empty() {
119 fragments.push(WebUIFragment::raw(raw_buf));
120 }
121
122 Ok(fragments)
123 }
124}
125
126impl Default for HandlebarsParser {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use webui_protocol::web_ui_fragment::Fragment;
136
137 #[test]
138 fn test_parse_plain_text() {
139 let parser = HandlebarsParser::new();
140 let result = parser
141 .parse("Hello, World!")
142 .expect("Failed to parse plain text");
143
144 assert_eq!(result.len(), 1);
145 match result[0].fragment.as_ref() {
146 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "Hello, World!"),
147 _ => panic!("Expected Raw fragment"),
148 }
149 }
150
151 #[test]
152 fn test_parse_double_brace() {
153 let parser = HandlebarsParser::new();
154 let result = parser
155 .parse("Hello, {{name}}!")
156 .expect("Failed to parse double brace syntax");
157
158 assert_eq!(result.len(), 3);
159
160 match result[0].fragment.as_ref() {
161 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "Hello, "),
162 _ => panic!("Expected Raw fragment"),
163 }
164
165 match result[1].fragment.as_ref() {
166 Some(Fragment::Signal(signal)) => {
167 assert_eq!(signal.value, "name");
168 assert!(!signal.raw);
169 }
170 _ => panic!("Expected Signal fragment"),
171 }
172
173 match result[2].fragment.as_ref() {
174 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "!"),
175 _ => panic!("Expected Raw fragment"),
176 }
177 }
178
179 #[test]
180 fn test_parse_triple_brace() {
181 let parser = HandlebarsParser::new();
182 let result = parser
183 .parse("Content: {{{html_content}}}")
184 .expect("Failed to parse triple brace syntax");
185
186 assert_eq!(result.len(), 2);
187
188 match result[0].fragment.as_ref() {
189 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "Content: "),
190 _ => panic!("Expected Raw fragment"),
191 }
192
193 match result[1].fragment.as_ref() {
194 Some(Fragment::Signal(signal)) => {
195 assert_eq!(signal.value, "html_content");
196 assert!(signal.raw);
197 }
198 _ => panic!("Expected Signal fragment"),
199 }
200 }
201
202 #[test]
203 fn test_mixed_braces() {
204 let parser = HandlebarsParser::new();
205 let result = parser
206 .parse("Hello, {{name}}! {{{html_content}}}")
207 .expect("Failed to parse mixed brace syntax");
208
209 assert_eq!(result.len(), 4);
210
211 match result[0].fragment.as_ref() {
212 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "Hello, "),
213 _ => panic!("Expected Raw fragment"),
214 }
215
216 match result[1].fragment.as_ref() {
217 Some(Fragment::Signal(signal)) => {
218 assert_eq!(signal.value, "name");
219 assert!(!signal.raw);
220 }
221 _ => panic!("Expected Signal fragment"),
222 }
223
224 match result[2].fragment.as_ref() {
225 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "! "),
226 _ => panic!("Expected Raw fragment"),
227 }
228
229 match result[3].fragment.as_ref() {
230 Some(Fragment::Signal(signal)) => {
231 assert_eq!(signal.value, "html_content");
232 assert!(signal.raw);
233 }
234 _ => panic!("Expected Signal fragment"),
235 }
236 }
237
238 #[test]
239 fn test_invalid_triple_open() {
240 let parser = HandlebarsParser::new();
241 let result = parser.parse("{{{invalid}}").expect("parse failed");
242 assert_eq!(result.len(), 1);
243 match result[0].fragment.as_ref() {
244 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "{{{invalid}}"),
245 _ => panic!("Expected Raw fragment"),
246 }
247 }
248
249 #[test]
250 fn test_four_open_braces() {
251 let parser = HandlebarsParser::new();
252 let result = parser.parse("{{{{invalid}}").expect("parse failed");
253 assert_eq!(result.len(), 1);
254 match result[0].fragment.as_ref() {
255 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "{{{{invalid}}"),
256 _ => panic!("Expected Raw fragment"),
257 }
258 }
259
260 #[test]
261 fn test_five_braces_with_valid_double() {
262 let parser = HandlebarsParser::new();
263 let result = parser.parse("{{{{{invalid}}").expect("parse failed");
264 assert_eq!(result.len(), 2);
265 match result[0].fragment.as_ref() {
266 Some(Fragment::Raw(raw)) => assert_eq!(raw.value, "{{{"),
267 _ => panic!("Expected Raw fragment for prefix"),
268 }
269 match result[1].fragment.as_ref() {
270 Some(Fragment::Signal(s)) => {
271 assert_eq!(s.value, "invalid");
272 assert!(!s.raw);
273 }
274 _ => panic!("Expected Signal fragment"),
275 }
276 }
277}