1use crate::writer::{Writer, comment_lines, verbatim_lines};
19use alloc::string::{String, ToString};
20use alloc::vec::Vec;
21use zerodds_lang_cst::common::{Doc, Ident, Literal};
22use zerodds_lang_cst::node::{Expr, File, Function, Stat};
23
24#[must_use]
26pub fn file(file: &File) -> String {
27 let mut w = Writer::new(" ");
28 if !file.doc.is_empty() {
29 comment_lines(&mut w, "// ", file.doc.as_lines());
30 }
31 for statement in &file.body {
32 w.blank_line();
33 render_stat(&mut w, statement);
34 }
35 w.finish()
36}
37
38#[must_use]
40pub fn stat(node: &Stat) -> String {
41 let mut w = Writer::new(" ");
42 render_stat(&mut w, node);
43 w.finish()
44}
45
46#[must_use]
48pub fn expr(node: &Expr) -> String {
49 expr_text(node)
50}
51
52#[must_use]
54pub fn ident(name: &Ident) -> String {
55 name.as_str().to_string()
56}
57
58fn render_stat(w: &mut Writer, node: &Stat) {
65 match node {
66 Stat::Declare {
67 doc,
68 kind,
69 name,
70 value,
71 } => {
72 note(w, doc);
73 let mut line = String::from(kind.as_str());
74 line.push(' ');
75 line.push_str(name.as_str());
76 if let Some(value) = value {
77 line.push_str(" = ");
78 line.push_str(&expr_text(value));
79 }
80 line.push(';');
81 w.line(&line);
82 }
83 Stat::Assign { doc, place, value } => {
84 note(w, doc);
85 let mut line = expr_text(place);
86 line.push_str(" = ");
87 line.push_str(&expr_text(value));
88 line.push(';');
89 w.line(&line);
90 }
91 Stat::Function(function) => render_function(w, function),
92 Stat::Return(value) => match value {
93 Some(value) => {
94 let mut line = String::from("return ");
95 line.push_str(&expr_text(value));
96 line.push(';');
97 w.line(&line);
98 }
99 None => w.line("return;"),
100 },
101 Stat::Expr(value) => {
102 let mut line = expr_text(value);
103 line.push(';');
104 w.line(&line);
105 }
106 Stat::If { .. } => render_if(w, node, "if ("),
107 Stat::For {
108 counter,
109 bound,
110 body,
111 } => {
112 let mut head = String::from("for (let ");
113 head.push_str(counter.as_str());
114 head.push_str(" = 0; ");
115 head.push_str(counter.as_str());
116 head.push_str(" < ");
117 head.push_str(&expr_text(bound));
118 head.push_str("; ");
119 head.push_str(counter.as_str());
120 head.push_str(" += 1) {");
121 w.line(&head);
122 w.indent();
123 for statement in body {
124 render_stat(w, statement);
125 }
126 w.dedent();
127 w.line("}");
128 }
129 Stat::While { condition, body } => {
130 let mut head = String::from("while (");
131 head.push_str(&expr_text(condition));
132 head.push_str(") {");
133 w.line(&head);
134 w.indent();
135 for statement in body {
136 render_stat(w, statement);
137 }
138 w.dedent();
139 w.line("}");
140 }
141 Stat::Break => w.line("break;"),
142 Stat::Throw(value) => {
143 let mut line = String::from("throw ");
144 line.push_str(&expr_text(value));
145 line.push(';');
146 w.line(&line);
147 }
148 Stat::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
149 Stat::Verbatim(text) => verbatim_lines(w, text),
150 }
151}
152
153fn render_if(w: &mut Writer, node: &Stat, keyword: &str) {
169 let mut node = node;
170 let mut keyword = keyword;
171 loop {
172 let Stat::If {
173 condition,
174 then,
175 otherwise,
176 } = node
177 else {
178 return;
179 };
180 let mut head = String::from(keyword);
181 head.push_str(&expr_text(condition));
182 head.push_str(") {");
183 w.line(&head);
184 w.indent();
185 for statement in then {
186 render_stat(w, statement);
187 }
188 w.dedent();
189 match otherwise {
190 None => {
191 w.line("}");
192 return;
193 }
194 Some(chain) if chain.len() == 1 && matches!(chain[0], Stat::If { .. }) => {
195 node = &chain[0];
196 keyword = "} else if (";
197 }
198 Some(block) => {
199 w.line("} else {");
200 w.indent();
201 for statement in block {
202 render_stat(w, statement);
203 }
204 w.dedent();
205 w.line("}");
206 return;
207 }
208 }
209 }
210}
211
212fn render_function(w: &mut Writer, node: &Function) {
219 note(w, &node.doc);
220 let mut head = String::from("function ");
221 head.push_str(node.name.as_str());
222 head.push('(');
223 let params: Vec<&str> = node.params.iter().map(Ident::as_str).collect();
224 head.push_str(¶ms.join(", "));
225 head.push_str(") {");
226 w.line(&head);
227 w.indent();
228 for statement in &node.body {
229 render_stat(w, statement);
230 }
231 w.dedent();
232 w.line("}");
233}
234
235fn expr_text(node: &Expr) -> String {
242 match node {
243 Expr::Literal(literal) => literal_text(literal),
244 Expr::Undefined => "undefined".to_string(),
245 Expr::Name(name) => name.as_str().to_string(),
246 Expr::Array(elements) => {
247 if elements.is_empty() {
248 return "[]".to_string();
249 }
250 let mut out = String::from("[");
251 let parts: Vec<String> = elements.iter().map(expr_text).collect();
252 out.push_str(&parts.join(", "));
253 out.push(']');
254 out
255 }
256 Expr::Object(fields) => {
257 if fields.is_empty() {
258 return "{}".to_string();
259 }
260 let mut out = String::from("{ ");
261 let parts: Vec<String> = fields
262 .iter()
263 .map(|(name, value)| {
264 let mut part = String::from(name.as_str());
265 part.push_str(": ");
266 part.push_str(&expr_text(value));
267 part
268 })
269 .collect();
270 out.push_str(&parts.join(", "));
271 out.push_str(" }");
272 out
273 }
274 Expr::New { callee, args } => {
275 let mut out = String::from("new ");
276 out.push_str(callee.as_str());
277 out.push('(');
278 let parts: Vec<String> = args.iter().map(expr_text).collect();
279 out.push_str(&parts.join(", "));
280 out.push(')');
281 out
282 }
283 Expr::Call { callee, args } => {
284 let mut out = expr_text(callee);
285 out.push('(');
286 let parts: Vec<String> = args.iter().map(expr_text).collect();
287 out.push_str(&parts.join(", "));
288 out.push(')');
289 out
290 }
291 Expr::Member { object, name } => {
292 let mut out = expr_text(object);
293 out.push('.');
294 out.push_str(name.as_str());
295 out
296 }
297 Expr::Index { object, index } => {
298 let mut out = expr_text(object);
299 out.push('[');
300 out.push_str(&expr_text(index));
301 out.push(']');
302 out
303 }
304 Expr::Paren(inner) => {
305 let mut out = String::from("(");
306 out.push_str(&expr_text(inner));
307 out.push(')');
308 out
309 }
310 Expr::Binary { op, lhs, rhs } => {
311 let mut out = expr_text(lhs);
312 out.push(' ');
313 out.push_str(op);
314 out.push(' ');
315 out.push_str(&expr_text(rhs));
316 out
317 }
318 Expr::Ternary {
319 condition,
320 then,
321 otherwise,
322 } => {
323 let mut out = expr_text(condition);
324 out.push_str(" ? ");
325 out.push_str(&expr_text(then));
326 out.push_str(" : ");
327 out.push_str(&expr_text(otherwise));
328 out
329 }
330 Expr::Unary { op, value } => {
331 let mut out = String::from(*op);
332 out.push_str(&expr_text(value));
333 out
334 }
335 }
336}
337
338fn literal_text(literal: &Literal) -> String {
339 match literal {
340 Literal::Int(text) | Literal::Float(text) => text.clone(),
341 Literal::Bool(value) => {
342 if *value {
343 "true".to_string()
344 } else {
345 "false".to_string()
346 }
347 }
348 Literal::Char(ch) => {
349 let mut out = String::from("'");
350 out.push_str(&char_text(*ch));
351 out.push('\'');
352 out
353 }
354 Literal::Str(text) => {
355 let mut out = String::from("\"");
356 out.push_str(&string_text(text));
357 out.push('"');
358 out
359 }
360 Literal::Bytes(bytes) => {
361 let mut out = String::from("Uint8Array.from([");
362 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02x}")).collect();
363 out.push_str(&parts.join(", "));
364 out.push_str("])");
365 out
366 }
367 Literal::Null => "null".to_string(),
368 }
369}
370
371#[must_use]
380pub fn string_text(text: &str) -> String {
381 let mut out = String::new();
382 for ch in text.chars() {
383 push_escaped(&mut out, ch, '"');
384 }
385 out
386}
387
388#[must_use]
390pub fn char_text(ch: char) -> String {
391 let mut out = String::new();
392 push_escaped(&mut out, ch, '\'');
393 out
394}
395
396fn push_escaped(out: &mut String, ch: char, delimiter: char) {
397 match ch {
398 '\\' => out.push_str("\\\\"),
399 '\0' => out.push_str("\\0"),
400 '\n' => out.push_str("\\n"),
401 '\r' => out.push_str("\\r"),
402 '\t' => out.push_str("\\t"),
403 c if c == delimiter => {
404 out.push('\\');
405 out.push(c);
406 }
407 c if (c as u32) < 0x20 || c as u32 == 0x7F => {
408 out.push_str(&alloc::format!("\\x{:02x}", c as u32));
409 }
410 other => out.push(other),
411 }
412}
413
414fn note(w: &mut Writer, doc: &Doc) {
415 if doc.is_empty() {
416 return;
417 }
418 comment_lines(w, "// ", doc.as_lines());
419}
420
421#[cfg(test)]
422mod tests {
423 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
424 use super::*;
425 use zerodds_lang_cst::node::{VarKind, escape_decl};
426
427 #[test]
428 fn a_const_declaration_ends_in_a_semicolon() {
429 let text = stat(&Stat::Declare {
430 doc: Doc::none(),
431 kind: VarKind::Const,
432 name: Ident::new("READY"),
433 value: Some(Expr::int(4)),
434 });
435 assert_eq!(text, "const READY = 4;\n");
436 }
437
438 #[test]
439 fn an_object_literal_writes_every_key_bare() {
440 let text = expr(&Expr::Object(alloc::vec![
441 (Ident::new("x"), Expr::int(1)),
442 (Ident::new("class"), Expr::int(2)),
443 ]));
444 assert_eq!(text, "{ x: 1, class: 2 }");
445 }
446
447 #[test]
448 fn a_for_loop_counts_from_zero_with_no_off_by_one_correction() {
449 let text = stat(&Stat::For {
450 counter: Ident::new("i"),
451 bound: Expr::name("n"),
452 body: alloc::vec![Stat::Break],
453 });
454 assert_eq!(text, "for (let i = 0; i < n; i += 1) {\n break;\n}\n");
455 }
456
457 #[test]
458 fn a_function_declaration_has_no_type_annotation_anywhere() {
459 let text = stat(&Stat::Function(Function {
460 doc: Doc::none(),
461 name: Ident::new("zd_enc_p_long"),
462 params: alloc::vec![Ident::new("w"), Ident::new("v")],
463 body: alloc::vec![Stat::Return(None)],
464 }));
465 assert_eq!(text, "function zd_enc_p_long(w, v) {\n return;\n}\n");
466 assert!(!text.contains(':'), "a type annotation reached the output");
467 }
468
469 #[test]
470 fn a_nul_is_written_as_the_named_escape_because_javascript_has_one() {
471 assert_eq!(string_text("a\0b"), "a\\0b");
472 assert_eq!(string_text("a\"b\\c"), "a\\\"b\\\\c");
473 assert_eq!(string_text("hä"), "hä");
474 }
475
476 #[test]
477 fn a_reserved_word_still_renders_bare_as_a_property_key() {
478 assert_eq!(escape_decl("class"), "class_");
482 let text = expr(&Expr::member(Expr::name("v"), "class"));
483 assert_eq!(text, "v.class");
484 }
485
486 #[test]
487 fn a_delete_expression_keeps_its_trailing_space() {
488 let text = expr(&Expr::Unary {
489 op: "delete ",
490 value: alloc::boxed::Box::new(Expr::member(Expr::name("v"), "m")),
491 });
492 assert_eq!(text, "delete v.m");
493 }
494
495 #[test]
496 fn an_if_else_if_chain_stays_flat() {
497 let text = stat(&Stat::If {
498 condition: Expr::name("a"),
499 then: alloc::vec![Stat::Break],
500 otherwise: Some(alloc::vec![Stat::If {
501 condition: Expr::name("b"),
502 then: alloc::vec![Stat::Break],
503 otherwise: None,
504 }]),
505 });
506 assert_eq!(text, "if (a) {\n break;\n} else if (b) {\n break;\n}\n");
507 }
508}