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(); for tok in tokens {
10 match tok {
11 Token::Text(s) => out.push_str(s),
12
13 Token::EscapedExpr(expr) => {
14 out.push_str("{{ ");
15 out.push_str(expr);
16 out.push_str(" }}");
17 }
18
19 Token::RawExpr(expr) => {
20 out.push_str("{{ ");
21 out.push_str(expr);
22 out.push_str("|safe }}");
23 }
24
25 Token::Directive { name, args } => {
26 let args_inner = args.as_deref().unwrap_or("").trim();
27 let args_stripped_quotes =
28 args_inner.trim_matches(|c| c == '"' || c == '\'');
29
30 match name.as_str() {
31 "if" => {
33 out.push_str("{% if ");
34 out.push_str(args_inner);
35 out.push_str(" %}");
36 }
37 "elseif" => {
38 out.push_str("{% elif ");
39 out.push_str(args_inner);
40 out.push_str(" %}");
41 }
42 "else" => out.push_str("{% else %}"),
43 "endif" => out.push_str("{% endif %}"),
44
45 "unless" => {
46 out.push_str("{% if !(");
47 out.push_str(args_inner);
48 out.push_str(") %}");
49 }
50 "endunless" => out.push_str("{% endif %}"),
51
52 "foreach" => {
53 let (lhs, rhs) = split_foreach(args_inner);
55 out.push_str("{% for ");
56 out.push_str(&rhs);
57 out.push_str(" in ");
58 out.push_str(&lhs);
59 out.push_str(" %}");
60 }
61 "endforeach" => out.push_str("{% endfor %}"),
62
63 "for" => {
64 out.push_str("{% for ");
65 out.push_str(args_inner);
66 out.push_str(" %}");
67 }
68 "endfor" => out.push_str("{% endfor %}"),
69
70 "while" => {
71 out.push_str("{# @while not supported, use @foreach: ");
73 out.push_str(args_inner);
74 out.push_str(" #}");
75 }
76 "endwhile" => {}
77
78 "extends" => {
80 out.push_str("{% extends \"");
81 out.push_str(&path_for(args_stripped_quotes));
82 out.push_str("\" %}");
83 }
84 "section" => {
85 out.push_str("{% block ");
86 out.push_str(args_stripped_quotes);
87 out.push_str(" %}");
88 }
89 "endsection" => out.push_str("{% endblock %}"),
90 "yield" => {
91 out.push_str("{% block ");
92 out.push_str(args_stripped_quotes);
93 out.push_str(" %}{% endblock %}");
94 }
95 "parent" => out.push_str("{{ super() }}"),
96 "include" => {
97 out.push_str("{% include \"");
98 out.push_str(&path_for(args_stripped_quotes));
99 out.push_str("\" %}");
100 }
101 "includeIf" | "includeif" => {
102 out.push_str("{% include \"");
104 out.push_str(&path_for(args_stripped_quotes));
105 out.push_str("\" %}");
106 }
107
108 "stack" => {
110 out.push_str("<!--FORGE-STACK:");
112 out.push_str(args_stripped_quotes);
113 out.push_str("-->");
114 }
115 "push" => {
116 stack_pushes.push(args_stripped_quotes.to_string());
117 out.push_str("<!--FORGE-PUSH-START:");
121 out.push_str(args_stripped_quotes);
122 out.push_str("-->");
123 }
124 "endpush" => {
125 let name = stack_pushes.pop().unwrap_or_default();
126 out.push_str("<!--FORGE-PUSH-END:");
127 out.push_str(&name);
128 out.push_str("-->");
129 }
130 "prepend" => {
131 stack_pushes.push(args_stripped_quotes.to_string());
132 out.push_str("<!--FORGE-PREPEND-START:");
133 out.push_str(args_stripped_quotes);
134 out.push_str("-->");
135 }
136 "endprepend" => {
137 let name = stack_pushes.pop().unwrap_or_default();
138 out.push_str("<!--FORGE-PREPEND-END:");
139 out.push_str(&name);
140 out.push_str("-->");
141 }
142
143 "vite" => {
145 out.push_str("{{ ::forge::vite::render(&[");
147 out.push_str(args_inner);
148 out.push_str("])|safe }}");
149 }
150
151 "auth" => {
153 out.push_str("{% if auth_user.is_some() %}");
154 }
155 "endauth" => out.push_str("{% endif %}"),
156 "guest" => out.push_str("{% if auth_user.is_none() %}"),
157 "endguest" => out.push_str("{% endif %}"),
158 "can" => {
159 out.push_str("{% if can(");
160 out.push_str(args_inner);
161 out.push_str(") %}");
162 }
163 "endcan" => out.push_str("{% endif %}"),
164
165 "verbatim" | "endverbatim" => {}
167
168 "csrf" => {
170 out.push_str(
171 "<input type=\"hidden\" name=\"_token\" value=\"{{ csrf_token }}\">",
172 );
173 }
174
175 "method" => {
177 out.push_str("<input type=\"hidden\" name=\"_method\" value=\"");
178 out.push_str(args_stripped_quotes);
179 out.push_str("\">");
180 }
181
182 _ => {
184 out.push_str("{# unknown directive @");
185 out.push_str(name);
186 if !args_inner.is_empty() {
187 out.push('(');
188 out.push_str(args_inner);
189 out.push(')');
190 }
191 out.push_str(" #}");
192 }
193 }
194 }
195
196 Token::ComponentOpen {
197 name,
198 attrs,
199 self_closing,
200 } => {
201 let component_macro = component_macro_name(name);
203 out.push_str("{% call ");
204 out.push_str(&component_macro);
205 out.push('(');
206 let mut first = true;
207 for (k, v) in attrs {
208 if !first {
209 out.push_str(", ");
210 }
211 first = false;
212 out.push_str(k);
213 out.push_str("=\"");
214 out.push_str(v);
215 out.push('"');
216 }
217 out.push_str(") %}");
218 if *self_closing {
219 out.push_str("{% endcall %}");
220 }
221 }
222
223 Token::ComponentClose { .. } => {
224 out.push_str("{% endcall %}");
225 }
226 }
227 }
228
229 out
230}
231
232fn path_for(spec: &str) -> String {
233 let s = spec.replace('.', "/");
235 if s.ends_with(".html") {
236 s
237 } else {
238 format!("{s}.html")
239 }
240}
241
242fn component_macro_name(name: &str) -> String {
243 name.replace('-', "_")
244}
245
246fn split_foreach(args: &str) -> (String, String) {
247 let args = args.trim();
252 if let Some((lhs, rhs)) = args.split_once(" as ") {
253 let lhs = lhs.trim().trim_start_matches('$').to_string();
254 let rhs = rhs.trim().trim_start_matches('$').to_string();
255 if let Some((_k, v)) = rhs.split_once("=>") {
256 return (lhs, v.trim().trim_start_matches('$').to_string());
257 }
258 (lhs, rhs)
259 } else {
260 (args.to_string(), "item".to_string())
261 }
262}