1use crate::parser::Token;
4
5pub fn lower(tokens: &[Token]) -> String {
6 let mut out = String::new();
7 let mut stack_pushes: Vec<String> = Vec::new();
8 let mut forelse_stack: Vec<String> = Vec::new(); let mut switch_started = false; for tok in tokens {
12 match tok {
13 Token::Text(s) => out.push_str(s),
14
15 Token::EscapedExpr(expr) => {
16 out.push_str("{{ ");
17 out.push_str(expr);
18 out.push_str(" }}");
19 }
20
21 Token::RawExpr(expr) => {
22 out.push_str("{{ ");
23 out.push_str(expr);
24 out.push_str("|safe }}");
25 }
26
27 Token::Directive { name, args } => {
28 let args_inner = args.as_deref().unwrap_or("").trim();
29 let args_stripped_quotes =
30 args_inner.trim_matches(|c| c == '"' || c == '\'');
31
32 match name.as_str() {
33 "if" => emit_if(&mut out, args_inner),
35 "elseif" => emit_elif(&mut out, args_inner),
36 "else" => out.push_str("{% else %}"),
37 "endif" => out.push_str("{% endif %}"),
38
39 "unless" => {
40 out.push_str("{% if !(");
41 out.push_str(args_inner);
42 out.push_str(") %}");
43 }
44 "endunless" => out.push_str("{% endif %}"),
45
46 "isset" => {
47 out.push_str("{% if (");
49 out.push_str(args_inner);
50 out.push_str(").is_some() %}");
51 }
52 "endisset" => out.push_str("{% endif %}"),
53
54 "empty" if !forelse_stack.is_empty() => {
55 out.push_str("{% endfor %}");
57 let _ = forelse_stack.pop();
58 out.push_str("{% if true %}");
59 forelse_stack.push("__empty_branch__".to_string());
61 }
62 "empty" => {
63 out.push_str("{% if (");
65 out.push_str(args_inner);
66 out.push_str(").is_empty() %}");
67 }
68 "endempty" => out.push_str("{% endif %}"),
69
70 "foreach" => {
72 let (lhs, rhs) = split_foreach(args_inner);
73 out.push_str("{% for ");
74 out.push_str(&rhs);
75 out.push_str(" in ");
76 out.push_str(&lhs);
77 out.push_str(" %}");
78 }
79 "endforeach" => out.push_str("{% endfor %}"),
80
81 "forelse" => {
82 let (lhs, rhs) = split_foreach(args_inner);
83 forelse_stack.push(lhs.clone());
84 out.push_str("{% for ");
85 out.push_str(&rhs);
86 out.push_str(" in ");
87 out.push_str(&lhs);
88 out.push_str(" %}");
89 }
90 "endforelse" => {
91 if let Some(top) = forelse_stack.pop() {
93 if top == "__empty_branch__" {
94 out.push_str("{% endif %}");
95 } else {
96 out.push_str("{% endfor %}");
97 }
98 }
99 }
100
101 "for" => {
102 out.push_str("{% for ");
103 out.push_str(args_inner);
104 out.push_str(" %}");
105 }
106 "endfor" => out.push_str("{% endfor %}"),
107
108 "continue" => {
109 if args_inner.is_empty() {
110 out.push_str("{% continue %}");
111 } else {
112 out.push_str("{% if ");
113 out.push_str(args_inner);
114 out.push_str(" %}{% continue %}{% endif %}");
115 }
116 }
117 "break" => {
118 if args_inner.is_empty() {
119 out.push_str("{% break %}");
120 } else {
121 out.push_str("{% if ");
122 out.push_str(args_inner);
123 out.push_str(" %}{% break %}{% endif %}");
124 }
125 }
126
127 "while" => {
128 out.push_str("{# @while not supported, use @foreach: ");
129 out.push_str(args_inner);
130 out.push_str(" #}");
131 }
132 "endwhile" => {}
133
134 "switch" => {
136 out.push_str("{# @switch(");
137 out.push_str(args_inner);
138 out.push_str(") #}");
139 switch_started = false;
140 out.push_str("<!--SWITCH:");
143 out.push_str(args_inner);
144 out.push_str("-->");
145 }
146 "case" => {
147 if !switch_started {
151 out.push_str("{% if __switch__ == ");
152 switch_started = true;
153 } else {
154 out.push_str("{% elif __switch__ == ");
155 }
156 out.push_str(args_inner);
157 out.push_str(" %}");
158 }
159 "default" => {
160 if switch_started {
161 out.push_str("{% else %}");
162 }
163 }
164 "endswitch" => {
165 if switch_started {
166 out.push_str("{% endif %}");
167 switch_started = false;
168 }
169 }
170
171 "extends" => {
173 out.push_str("{% extends \"");
174 out.push_str(&path_for(args_stripped_quotes));
175 out.push_str("\" %}");
176 }
177 "section" => {
178 if let Some((name, value)) = parse_inline_section(args_inner) {
180 out.push_str("{% block ");
181 out.push_str(&name);
182 out.push_str(" %}");
183 out.push_str(&value);
184 out.push_str("{% endblock %}");
185 } else {
186 out.push_str("{% block ");
187 out.push_str(args_stripped_quotes);
188 out.push_str(" %}");
189 }
190 }
191 "endsection" | "show" | "stop" | "overwrite" => out.push_str("{% endblock %}"),
192 "yield" => {
193 let (n, d) = parse_yield_args(args_inner);
196 out.push_str("{% block ");
197 out.push_str(&n);
198 out.push_str(" %}");
199 out.push_str(&d);
200 out.push_str("{% endblock %}");
201 }
202 "parent" => out.push_str("{{ super() }}"),
203 "include" => {
204 out.push_str("{% include \"");
205 out.push_str(&path_for(args_stripped_quotes));
206 out.push_str("\" %}");
207 }
208 "includeIf" | "includeif" | "includeWhen" | "includewhen" => {
209 out.push_str("{% include \"");
210 out.push_str(&path_for(args_stripped_quotes));
211 out.push_str("\" %}");
212 }
213 "hasSection" | "hassection" => {
214 out.push_str("{% if true %}{# @hasSection placeholder for '");
215 out.push_str(args_stripped_quotes);
216 out.push_str("' #}");
217 }
218 "endhasSection" | "endhassection" | "sectionMissing" | "sectionmissing" => {
219 out.push_str("{% endif %}");
220 }
221
222 "stack" => {
224 out.push_str("<!--FORGE-STACK:");
225 out.push_str(args_stripped_quotes);
226 out.push_str("-->");
227 }
228 "push" => {
229 stack_pushes.push(args_stripped_quotes.to_string());
230 out.push_str("<!--FORGE-PUSH-START:");
231 out.push_str(args_stripped_quotes);
232 out.push_str("-->");
233 }
234 "endpush" => {
235 let name = stack_pushes.pop().unwrap_or_default();
236 out.push_str("<!--FORGE-PUSH-END:");
237 out.push_str(&name);
238 out.push_str("-->");
239 }
240 "pushOnce" | "pushonce" => {
241 stack_pushes.push(args_stripped_quotes.to_string());
242 out.push_str("<!--FORGE-PUSHONCE-START:");
243 out.push_str(args_stripped_quotes);
244 out.push_str("-->");
245 }
246 "endPushOnce" | "endpushonce" => {
247 let name = stack_pushes.pop().unwrap_or_default();
248 out.push_str("<!--FORGE-PUSHONCE-END:");
249 out.push_str(&name);
250 out.push_str("-->");
251 }
252 "prepend" => {
253 stack_pushes.push(args_stripped_quotes.to_string());
254 out.push_str("<!--FORGE-PREPEND-START:");
255 out.push_str(args_stripped_quotes);
256 out.push_str("-->");
257 }
258 "endprepend" => {
259 let name = stack_pushes.pop().unwrap_or_default();
260 out.push_str("<!--FORGE-PREPEND-END:");
261 out.push_str(&name);
262 out.push_str("-->");
263 }
264 "once" => out.push_str("<!--FORGE-ONCE-START-->"),
265 "endonce" => out.push_str("<!--FORGE-ONCE-END-->"),
266
267 "vite" => {
269 out.push_str("{{ ::forge::vite::render(&[");
270 out.push_str(args_inner);
271 out.push_str("])|safe }}");
272 }
273
274 "auth" => out.push_str("{% if auth_user.is_some() %}"),
276 "endauth" => out.push_str("{% endif %}"),
277 "guest" => out.push_str("{% if auth_user.is_none() %}"),
278 "endguest" => out.push_str("{% endif %}"),
279 "can" => {
280 out.push_str("{% if can(");
281 out.push_str(args_inner);
282 out.push_str(") %}");
283 }
284 "endcan" => out.push_str("{% endif %}"),
285 "cannot" => {
286 out.push_str("{% if !can(");
287 out.push_str(args_inner);
288 out.push_str(") %}");
289 }
290 "endcannot" => out.push_str("{% endif %}"),
291 "role" => {
292 out.push_str("{% if has_role(auth_user, ");
293 out.push_str(args_inner);
294 out.push_str(") %}");
295 }
296 "endrole" => out.push_str("{% endif %}"),
297
298 "checked" => emit_attr_helper(&mut out, "checked", args_inner),
300 "selected" => emit_attr_helper(&mut out, "selected", args_inner),
301 "disabled" => emit_attr_helper(&mut out, "disabled", args_inner),
302 "readonly" => emit_attr_helper(&mut out, "readonly", args_inner),
303 "required" => emit_attr_helper(&mut out, "required", args_inner),
304
305 "error" => {
307 out.push_str("{% if let Some(message) = errors.get(\"");
309 out.push_str(args_stripped_quotes);
310 out.push_str("\").and_then(|v| v.first()) %}");
311 }
312 "enderror" => out.push_str("{% endif %}"),
313 "old" => {
314 let (field, default) = parse_old_args(args_inner);
316 out.push_str("{{ ::forge::escape::html(&old_input.get(\"");
317 out.push_str(&field);
318 out.push_str("\").cloned().unwrap_or_else(|| ");
319 out.push_str(&default);
320 out.push_str(".to_string())) }}");
321 }
322
323 "class" => {
325 out.push_str("class=\"{{ ::forge::escape::html(&::forge::class_list(&[");
327 out.push_str(args_inner);
328 out.push_str("])) }}\"");
329 }
330 "style" => {
331 out.push_str("style=\"{{ ::forge::escape::html(&::forge::style_list(&[");
332 out.push_str(args_inner);
333 out.push_str("])) }}\"");
334 }
335
336 "dump" | "dd" => {
338 out.push_str("<pre>{{ format!(\"{:#?}\", ");
339 out.push_str(args_inner);
340 out.push_str(")|escape }}</pre>");
341 }
342 "json" => {
343 out.push_str("{{ ::serde_json::to_string(&");
345 out.push_str(args_inner);
346 out.push_str(").unwrap_or_default()|safe }}");
347 }
348
349 "verbatim" | "endverbatim" => {}
351
352 "csrf" => {
354 out.push_str(
355 "<input type=\"hidden\" name=\"_token\" value=\"{{ csrf_token }}\">",
356 );
357 }
358 "method" => {
359 out.push_str("<input type=\"hidden\" name=\"_method\" value=\"");
360 out.push_str(args_stripped_quotes);
361 out.push_str("\">");
362 }
363
364 "lang" | "trans" => {
366 out.push_str("{{ ::forge::escape::html(&::forge::lang(");
367 out.push_str(args_inner);
368 out.push_str(")) }}");
369 }
370 "choice" => {
371 out.push_str("{{ ::forge::escape::html(&::forge::lang_choice(");
372 out.push_str(args_inner);
373 out.push_str(")) }}");
374 }
375
376 "props" => {
378 out.push_str("{# props: ");
381 out.push_str(args_inner);
382 out.push_str(" #}");
383 }
384
385 _ => {
387 out.push_str("{# unknown directive @");
388 out.push_str(name);
389 if !args_inner.is_empty() {
390 out.push('(');
391 out.push_str(args_inner);
392 out.push(')');
393 }
394 out.push_str(" #}");
395 }
396 }
397 }
398
399 Token::ComponentOpen {
400 name,
401 attrs,
402 self_closing,
403 } => {
404 let component_macro = component_macro_name(name);
406 out.push_str("{% call ");
407 out.push_str(&component_macro);
408 out.push('(');
409 let mut first = true;
410 for (k, v) in attrs {
411 if !first {
412 out.push_str(", ");
413 }
414 first = false;
415 out.push_str(k);
416 out.push_str("=\"");
417 out.push_str(v);
418 out.push('"');
419 }
420 out.push_str(") %}");
421 if *self_closing {
422 out.push_str("{% endcall %}");
423 }
424 }
425
426 Token::ComponentClose { .. } => {
427 out.push_str("{% endcall %}");
428 }
429 }
430 }
431
432 out
433}
434
435fn emit_if(out: &mut String, expr: &str) {
438 out.push_str("{% if ");
439 out.push_str(expr);
440 out.push_str(" %}");
441}
442
443fn emit_elif(out: &mut String, expr: &str) {
444 out.push_str("{% elif ");
445 out.push_str(expr);
446 out.push_str(" %}");
447}
448
449fn emit_attr_helper(out: &mut String, attr: &str, condition: &str) {
450 out.push_str("{% if (");
451 out.push_str(condition);
452 out.push_str(") %}");
453 out.push_str(attr);
454 out.push_str("{% endif %}");
455}
456
457fn parse_inline_section(args: &str) -> Option<(String, String)> {
458 let args = args.trim();
460 if let Some(comma) = find_top_level_comma(args) {
461 let name = args[..comma]
462 .trim()
463 .trim_matches(|c| c == '"' || c == '\'')
464 .to_string();
465 let value = args[comma + 1..].trim().to_string();
466 if !name.is_empty() {
467 return Some((name, value));
468 }
469 }
470 None
471}
472
473fn parse_yield_args(args: &str) -> (String, String) {
474 if let Some(comma) = find_top_level_comma(args) {
475 let n = args[..comma]
476 .trim()
477 .trim_matches(|c| c == '"' || c == '\'')
478 .to_string();
479 let d_raw = args[comma + 1..].trim();
480 let d = if d_raw.starts_with('"') || d_raw.starts_with('\'') {
481 d_raw.trim_matches(|c| c == '"' || c == '\'').to_string()
483 } else {
484 format!("{{{{ {d_raw} }}}}")
486 };
487 return (n, d);
488 }
489 (
490 args.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
491 String::new(),
492 )
493}
494
495fn parse_old_args(args: &str) -> (String, String) {
496 if let Some(comma) = find_top_level_comma(args) {
497 let f = args[..comma]
498 .trim()
499 .trim_matches(|c| c == '"' || c == '\'')
500 .to_string();
501 let d = args[comma + 1..].trim().to_string();
502 return (f, d);
503 }
504 (
505 args.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
506 "\"\"".to_string(),
507 )
508}
509
510fn find_top_level_comma(s: &str) -> Option<usize> {
511 let mut depth_paren = 0;
512 let mut depth_bracket = 0;
513 let mut in_string = None::<char>;
514 for (i, ch) in s.char_indices() {
515 if let Some(q) = in_string {
516 if ch == q {
517 in_string = None;
518 }
519 continue;
520 }
521 match ch {
522 '"' | '\'' => in_string = Some(ch),
523 '(' => depth_paren += 1,
524 ')' => depth_paren -= 1,
525 '[' => depth_bracket += 1,
526 ']' => depth_bracket -= 1,
527 ',' if depth_paren == 0 && depth_bracket == 0 => return Some(i),
528 _ => {}
529 }
530 }
531 None
532}
533
534fn path_for(spec: &str) -> String {
535 let s = spec.replace('.', "/");
536 if s.ends_with(".html") {
537 s
538 } else {
539 format!("{s}.html")
540 }
541}
542
543fn component_macro_name(name: &str) -> String {
544 name.replace('-', "_")
545}
546
547fn split_foreach(args: &str) -> (String, String) {
548 let args = args.trim();
549 if let Some((lhs, rhs)) = args.split_once(" as ") {
550 let lhs = lhs.trim().trim_start_matches('$').to_string();
551 let rhs = rhs.trim().trim_start_matches('$').to_string();
552 if let Some((_k, v)) = rhs.split_once("=>") {
553 return (lhs, v.trim().trim_start_matches('$').to_string());
554 }
555 (lhs, rhs)
556 } else {
557 (args.to_string(), "item".to_string())
558 }
559}