@frontiers-labs/argon 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/annotations.d.ts +23 -0
  4. package/dist/annotations.d.ts.map +1 -0
  5. package/dist/annotations.js +23 -0
  6. package/dist/annotations.js.map +1 -0
  7. package/dist/codegen/client.d.ts +14 -0
  8. package/dist/codegen/client.d.ts.map +1 -0
  9. package/dist/codegen/client.js +964 -0
  10. package/dist/codegen/client.js.map +1 -0
  11. package/dist/codegen/dts.d.ts +3 -0
  12. package/dist/codegen/dts.d.ts.map +1 -0
  13. package/dist/codegen/dts.js +82 -0
  14. package/dist/codegen/dts.js.map +1 -0
  15. package/dist/codegen/js.d.ts +3 -0
  16. package/dist/codegen/js.d.ts.map +1 -0
  17. package/dist/codegen/js.js +278 -0
  18. package/dist/codegen/js.js.map +1 -0
  19. package/dist/codegen/rust.d.ts +6 -0
  20. package/dist/codegen/rust.d.ts.map +1 -0
  21. package/dist/codegen/rust.js +1076 -0
  22. package/dist/codegen/rust.js.map +1 -0
  23. package/dist/component.d.ts +35 -0
  24. package/dist/component.d.ts.map +1 -0
  25. package/dist/component.js +120 -0
  26. package/dist/component.js.map +1 -0
  27. package/dist/diagnostics.d.ts +20 -0
  28. package/dist/diagnostics.d.ts.map +1 -0
  29. package/dist/diagnostics.js +40 -0
  30. package/dist/diagnostics.js.map +1 -0
  31. package/dist/docs.d.ts +3 -0
  32. package/dist/docs.d.ts.map +1 -0
  33. package/dist/docs.js +42 -0
  34. package/dist/docs.js.map +1 -0
  35. package/dist/error-catalog.d.ts +11 -0
  36. package/dist/error-catalog.d.ts.map +1 -0
  37. package/dist/error-catalog.js +92 -0
  38. package/dist/error-catalog.js.map +1 -0
  39. package/dist/html-attrs.d.ts +2 -0
  40. package/dist/html-attrs.d.ts.map +1 -0
  41. package/dist/html-attrs.js +37 -0
  42. package/dist/html-attrs.js.map +1 -0
  43. package/dist/html.d.ts +41 -0
  44. package/dist/html.d.ts.map +1 -0
  45. package/dist/html.js +188 -0
  46. package/dist/html.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +139 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir.d.ts +321 -0
  52. package/dist/ir.d.ts.map +1 -0
  53. package/dist/ir.js +243 -0
  54. package/dist/ir.js.map +1 -0
  55. package/dist/jsx.d.ts +31 -0
  56. package/dist/jsx.d.ts.map +1 -0
  57. package/dist/jsx.js +242 -0
  58. package/dist/jsx.js.map +1 -0
  59. package/dist/lower.d.ts +6 -0
  60. package/dist/lower.d.ts.map +1 -0
  61. package/dist/lower.js +1341 -0
  62. package/dist/lower.js.map +1 -0
  63. package/dist/parser.d.ts +3 -0
  64. package/dist/parser.d.ts.map +1 -0
  65. package/dist/parser.js +714 -0
  66. package/dist/parser.js.map +1 -0
  67. package/dist/ssr-subset.d.ts +29 -0
  68. package/dist/ssr-subset.d.ts.map +1 -0
  69. package/dist/ssr-subset.js +119 -0
  70. package/dist/ssr-subset.js.map +1 -0
  71. package/dist/template.d.ts +32 -0
  72. package/dist/template.d.ts.map +1 -0
  73. package/dist/template.js +90 -0
  74. package/dist/template.js.map +1 -0
  75. package/package.json +67 -0
@@ -0,0 +1,1076 @@
1
+ // Rust SSR backend: ModuleIR → a dependency-free .rs file.
2
+ //
3
+ // Emission is type-directed: the IR carries inferred types, so strings are
4
+ // borrowed (&str) where possible and owned (String) where required, props
5
+ // support sequences (Vec<T>), and generated components get an idiomatic
6
+ // `new(...)` constructor taking `impl Into<String>` / `impl IntoIterator`.
7
+ import { attrPositions, isMarkup, markupValue, referencedNames, valueExprIndices, visitExpr, visitStmt, } from "../ir.js";
8
+ import { assertNoDiagnostics } from "../diagnostics.js";
9
+ import { MARKER_RE } from "../template.js";
10
+ import { serialize } from "../html.js";
11
+ import { isBooleanAttr } from "../html-attrs.js";
12
+ export function compileRust(module, opts = {}) {
13
+ assertNoDiagnostics(analyze(module), module.source);
14
+ return new RustGen(module, opts).generate();
15
+ }
16
+ // ── Naming ───────────────────────────────────────────────────────────────────
17
+ function snake(name) {
18
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
19
+ }
20
+ function kebab(name) {
21
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
22
+ }
23
+ // ── SSR analysis ─────────────────────────────────────────────────────────────
24
+ //
25
+ // Lowering already marked everything the SSR subset cannot express as opaque
26
+ // nodes carrying diagnostics; here we collect the ones that are actually
27
+ // reachable from server-rendered values.
28
+ // Props may be scalars, interface (struct) types, or arrays of either —
29
+ // every shape the compiler can both render in Rust and serialize for
30
+ // hydration with a generated (dependency-free) JSON encoder.
31
+ function supportedProp(type, structs, seen = new Set()) {
32
+ switch (type.kind) {
33
+ case "string":
34
+ case "number":
35
+ case "boolean":
36
+ return true;
37
+ case "struct": {
38
+ if (seen.has(type.name))
39
+ return true;
40
+ const struct = structs.get(type.name);
41
+ if (!struct)
42
+ return false;
43
+ seen.add(type.name);
44
+ return struct.fields.every(f => supportedProp(f.type, structs, seen));
45
+ }
46
+ case "array":
47
+ case "fixed":
48
+ return supportedProp(type.elem, structs, seen);
49
+ case "tuple":
50
+ return type.elems.every(e => supportedProp(e, structs, seen));
51
+ case "option":
52
+ return supportedProp(type.inner, structs, seen);
53
+ default:
54
+ return false;
55
+ }
56
+ }
57
+ function analyze(module) {
58
+ const diagnostics = [];
59
+ if (module.components.length === 0) {
60
+ return [{
61
+ code: "ARGON001",
62
+ target: "all",
63
+ message: "No Argon component found.",
64
+ span: { start: 0, end: 0 },
65
+ hint: "Export a capitalized multi-word function returning Component with a TSX template.",
66
+ }];
67
+ }
68
+ const collectOpaque = (expr) => {
69
+ visitExpr(expr, e => {
70
+ if (e.kind === "opaque")
71
+ diagnostics.push(e.diagnostic);
72
+ });
73
+ };
74
+ for (const component of module.components) {
75
+ for (const prop of component.props) {
76
+ if (!supportedProp(prop.type, module.structs)) {
77
+ diagnostics.push({
78
+ code: "ARGON102",
79
+ target: "ssr",
80
+ message: `Rust SSR props support scalars, interface types, and arrays of them; '${prop.name}' is unsupported.`,
81
+ span: component.span,
82
+ hint: "Annotate the prop with string/number/boolean, an interface declared in this file, or an array of those.",
83
+ });
84
+ }
85
+ }
86
+ for (const stmt of component.setup) {
87
+ if (stmt.kind === "opaque")
88
+ diagnostics.push(stmt.diagnostic);
89
+ else if (stmt.kind === "let")
90
+ collectOpaque(stmt.init);
91
+ }
92
+ for (const index of valueExprIndices(component.template)) {
93
+ collectOpaque(component.template.exprs[index]);
94
+ }
95
+ }
96
+ for (const decl of reachableDecls(module, module.components)) {
97
+ if (decl.kind === "opaque")
98
+ diagnostics.push(decl.diagnostic);
99
+ }
100
+ return diagnostics;
101
+ }
102
+ function reachableDecls(module, components) {
103
+ const byName = new Map(module.decls.map(d => [d.name, d]));
104
+ const roots = new Set();
105
+ for (const component of components) {
106
+ for (const stmt of component.setup) {
107
+ if (stmt.kind === "let")
108
+ for (const name of referencedNames(stmt.init))
109
+ roots.add(name);
110
+ }
111
+ for (const index of valueExprIndices(component.template)) {
112
+ for (const name of referencedNames(component.template.exprs[index]))
113
+ roots.add(name);
114
+ }
115
+ }
116
+ const seen = new Set();
117
+ const order = [];
118
+ const visit = (name) => {
119
+ if (seen.has(name))
120
+ return;
121
+ const decl = byName.get(name);
122
+ if (!decl)
123
+ return;
124
+ seen.add(name);
125
+ if (decl.kind === "fn") {
126
+ for (const stmt of decl.body) {
127
+ visitStmt(stmt, e => {
128
+ for (const ref of referencedNames(e))
129
+ visit(ref);
130
+ });
131
+ }
132
+ }
133
+ else if (decl.kind === "table") {
134
+ for (const entry of decl.entries)
135
+ for (const ref of referencedNames(entry.value))
136
+ visit(ref);
137
+ }
138
+ order.push(decl);
139
+ };
140
+ for (const root of roots)
141
+ visit(root);
142
+ // Preserve module source order for readable output.
143
+ const picked = new Set(order.map(d => d.name));
144
+ return module.decls.filter(d => picked.has(d.name));
145
+ }
146
+ // ── Types ────────────────────────────────────────────────────────────────────
147
+ function rustType(type) {
148
+ switch (type.kind) {
149
+ case "string": return "String";
150
+ case "number": return "f64";
151
+ case "boolean": return "bool";
152
+ case "array": return `Vec<${rustType(type.elem)}>`;
153
+ case "fixed": return `[${rustType(type.elem)}; ${type.size}]`;
154
+ case "tuple": return `(${type.elems.map(rustType).join(", ")})`;
155
+ case "struct": return type.name;
156
+ case "option": return `Option<${rustType(type.inner)}>`;
157
+ case "unknown": return "f64";
158
+ }
159
+ }
160
+ function borrowedType(type) {
161
+ switch (type.kind) {
162
+ case "string": return "&str";
163
+ case "array": return `&[${rustType(type.elem)}]`;
164
+ case "fixed": return `&[${rustType(type.elem)}; ${type.size}]`;
165
+ case "struct": return `&${type.name}`;
166
+ default: return rustType(type);
167
+ }
168
+ }
169
+ function isCopy(type) {
170
+ switch (type.kind) {
171
+ case "number":
172
+ case "boolean":
173
+ return true;
174
+ case "tuple": return type.elems.every(isCopy);
175
+ case "fixed": return isCopy(type.elem);
176
+ case "option": return isCopy(type.inner);
177
+ default: return false;
178
+ }
179
+ }
180
+ // A place expression: borrowing it costs nothing, moving it out may not be
181
+ // allowed — owned contexts must clone.
182
+ function isPlace(e) {
183
+ return e.kind === "ref" || e.kind === "member" || e.kind === "index";
184
+ }
185
+ class RustGen {
186
+ module;
187
+ opts;
188
+ out = [];
189
+ fns = new Map();
190
+ // Locals that are usize loop indices and need an `as f64` cast when read.
191
+ usizeVars = new Set();
192
+ // Setup locals holding markup (a JSX string), so reads render raw not escaped.
193
+ markupLocals = new Set();
194
+ // Structs that need a generated argon_json() impl for prop hydration.
195
+ jsonStructs = new Set();
196
+ needsEscape = false;
197
+ needsEscapeText = false;
198
+ needsStrSlice = false;
199
+ needsSlice = false;
200
+ needsPad = false;
201
+ needsJsonStr = false;
202
+ needsJsonStrings = false;
203
+ needsJsonNumbers = false;
204
+ constructor(module, opts = {}) {
205
+ this.module = module;
206
+ this.opts = opts;
207
+ for (const decl of module.decls) {
208
+ if (decl.kind === "fn")
209
+ this.fns.set(decl.name, { params: decl.params, ret: decl.ret });
210
+ }
211
+ }
212
+ generate() {
213
+ for (const struct of this.module.structs.values())
214
+ this.emitStruct(struct);
215
+ for (const decl of reachableDecls(this.module, this.module.components))
216
+ this.emitDecl(decl);
217
+ const impls = this.module.components.map(c => this.emitComponent(c));
218
+ const jsonImpls = this.emitJsonImpls();
219
+ return [
220
+ "// Generated by Argon compiler — do not edit by hand.",
221
+ "// SSR output: deterministic HTML only. Event handlers and browser APIs stay in the CSR bundle.",
222
+ "#[allow(unused_imports)]",
223
+ "use std::fmt::Write as _;",
224
+ "",
225
+ ...this.helpers(),
226
+ ...this.out,
227
+ ...jsonImpls,
228
+ ...impls,
229
+ ].join("\n");
230
+ }
231
+ helpers() {
232
+ const helpers = [];
233
+ if (this.needsEscape) {
234
+ // Inside a double-quoted attribute value only `&` and `"` can break out;
235
+ // leaving `<`/`>` intact keeps hydration JSON and href values readable.
236
+ helpers.push(`fn argon_escape_attr(value: &str) -> String {
237
+ let mut out = String::with_capacity(value.len());
238
+ for c in value.chars() {
239
+ match c {
240
+ '&' => out.push_str("&amp;"),
241
+ '"' => out.push_str("&quot;"),
242
+ c => out.push(c),
243
+ }
244
+ }
245
+ out
246
+ }
247
+ `);
248
+ }
249
+ if (this.needsEscapeText) {
250
+ // Text content: neutralize markup so an interpolated value cannot inject
251
+ // elements. `unsafeHtml(...)` opts a trusted string out of this.
252
+ helpers.push(`fn argon_escape_text(value: &str) -> String {
253
+ let mut out = String::with_capacity(value.len());
254
+ for c in value.chars() {
255
+ match c {
256
+ '&' => out.push_str("&amp;"),
257
+ '<' => out.push_str("&lt;"),
258
+ '>' => out.push_str("&gt;"),
259
+ c => out.push(c),
260
+ }
261
+ }
262
+ out
263
+ }
264
+ `);
265
+ }
266
+ if (this.needsStrSlice) {
267
+ // JS String.slice: indices count from the end when negative and clamp to
268
+ // bounds. We count Unicode scalars (JS counts UTF-16 units).
269
+ helpers.push(`fn argon_str_slice(value: &str, start: f64, end: Option<f64>) -> String {
270
+ let chars: Vec<char> = value.chars().collect();
271
+ let len = chars.len() as isize;
272
+ let norm = |i: f64| -> isize { let i = i as isize; if i < 0 { (len + i).max(0) } else { i.min(len) } };
273
+ let a = norm(start);
274
+ let b = end.map(norm).unwrap_or(len);
275
+ if a >= b { return String::new(); }
276
+ chars[a as usize..b as usize].iter().collect()
277
+ }
278
+ `);
279
+ }
280
+ if (this.needsPad) {
281
+ helpers.push(`fn argon_pad(value: &str, width: f64, pad: &str, start: bool) -> String {
282
+ let len = value.chars().count();
283
+ let target = width as usize;
284
+ let pad_chars: Vec<char> = pad.chars().collect();
285
+ if len >= target || pad_chars.is_empty() { return value.to_string(); }
286
+ let fill: String = (0..target - len).map(|i| pad_chars[i % pad_chars.len()]).collect();
287
+ if start { format!("{}{}", fill, value) } else { format!("{}{}", value, fill) }
288
+ }
289
+ `);
290
+ }
291
+ if (this.needsSlice) {
292
+ // JS Array.slice: end-relative negative indices, clamped to bounds.
293
+ helpers.push(`fn argon_slice<T: Clone>(value: &[T], start: f64, end: Option<f64>) -> Vec<T> {
294
+ let len = value.len() as isize;
295
+ let norm = |i: f64| -> isize { let i = i as isize; if i < 0 { (len + i).max(0) } else { i.min(len) } };
296
+ let a = norm(start);
297
+ let b = end.map(norm).unwrap_or(len);
298
+ if a >= b { return Vec::new(); }
299
+ value[a as usize..b as usize].to_vec()
300
+ }
301
+ `);
302
+ }
303
+ if (this.needsJsonStrings)
304
+ this.needsJsonStr = true;
305
+ if (this.needsJsonStr) {
306
+ helpers.push(String.raw `fn argon_json_str(value: &str) -> String {
307
+ let mut out = String::with_capacity(value.len() + 2);
308
+ out.push('"');
309
+ for c in value.chars() {
310
+ match c {
311
+ '"' => out.push_str("\\\""),
312
+ '\\' => out.push_str("\\\\"),
313
+ '\n' => out.push_str("\\n"),
314
+ c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
315
+ c => out.push(c),
316
+ }
317
+ }
318
+ out.push('"');
319
+ out
320
+ }
321
+ ` + "\n");
322
+ }
323
+ if (this.needsJsonStrings) {
324
+ helpers.push(`fn argon_json_strings(values: &[String]) -> String {
325
+ let parts: Vec<String> = values.iter().map(|v| argon_json_str(v)).collect();
326
+ format!("[{}]", parts.join(","))
327
+ }
328
+ `);
329
+ }
330
+ if (this.needsJsonNumbers) {
331
+ helpers.push(`fn argon_json_numbers(values: &[f64]) -> String {
332
+ let parts: Vec<String> = values.iter().map(|v| v.to_string()).collect();
333
+ format!("[{}]", parts.join(","))
334
+ }
335
+ `);
336
+ }
337
+ return helpers;
338
+ }
339
+ // ── Structs and module declarations ──────────────────────────────────────
340
+ emitStruct(struct) {
341
+ const fields = struct.fields.map(f => ` pub ${snake(f.name)}: ${rustType(f.type)},`);
342
+ this.out.push(`#[derive(Clone, PartialEq)]\npub struct ${struct.name} {\n${fields.join("\n")}\n}\n`);
343
+ }
344
+ emitDecl(decl) {
345
+ if (decl.kind === "css") {
346
+ this.out.push(`const ${snake(decl.name).toUpperCase()}: &str = ${JSON.stringify(decl.text)};\n`);
347
+ }
348
+ else if (decl.kind === "table") {
349
+ const arms = decl.entries.map(e => ` ${JSON.stringify(e.key)} => ${this.owned(e.value)},`);
350
+ arms.push(` _ => ${this.owned(decl.entries[0].value)},`);
351
+ this.out.push(`fn ${snake(decl.name)}(key: &str) -> ${rustType(decl.valueType)} {\n match key {\n${arms.join("\n")}\n }\n}\n`);
352
+ }
353
+ else if (decl.kind === "fn") {
354
+ const params = decl.params.map(p => `${snake(p.name)}: ${borrowedType(p.type)}`);
355
+ const body = decl.body.map(s => " " + this.stmt(s)).join("\n");
356
+ this.out.push(`// argon: ${this.loc(decl.span.start)}\nfn ${snake(decl.name)}(${params.join(", ")}) -> ${rustType(decl.ret)} {\n${body}\n}\n`);
357
+ }
358
+ }
359
+ // ── Component ──────────────────────────────────────────────────────────────
360
+ emitComponent(component) {
361
+ this.markupLocals = new Set();
362
+ for (const stmt of component.setup) {
363
+ if (stmt.kind === "let" && !stmt.tuple && markupValue(stmt.init, this.markupLocals)) {
364
+ this.markupLocals.add(stmt.names[0]);
365
+ }
366
+ }
367
+ const fields = component.props.map(p => ` pub ${snake(p.name)}: ${rustType(p.type)},`);
368
+ const prelude = component.setup
369
+ .filter(s => s.kind === "let")
370
+ .map(s => " " + this.stmt(s))
371
+ .join("\n");
372
+ const segments = this.templateSegments(component);
373
+ const renderBody = segments
374
+ .map(s => ("lit" in s ? (s.lit ? ` __out.push_str("${rustStrLit(s.lit)}");` : "") : s.code))
375
+ .filter(Boolean)
376
+ .join("\n");
377
+ // Optional props contribute their whole ` data-x="..."` chunk dynamically:
378
+ // an absent value omits the attribute, so the client hydrates undefined.
379
+ const dataAttrs = component.props
380
+ .map(p => (p.type.kind === "option" ? "{}" : ` data-${kebab(p.name)}=\\"{}\\"`))
381
+ .join("");
382
+ const attrArgs = component.props.map(p => this.dataAttrValue(p));
383
+ const wrapper = `format!("<${component.tagName}${dataAttrs}>{}</${component.tagName}>", ${[...attrArgs, "self.render_inner()"].join(", ")})`;
384
+ return `// argon: ${this.loc(component.span.start)}
385
+ #[derive(Clone)]
386
+ pub struct ${component.className} {
387
+ ${fields.join("\n")}
388
+ }
389
+
390
+ impl ${component.className} {
391
+ ${this.emitNew(component.props)}
392
+
393
+ // The declarative shadow DOM alone — what a composing parent inlines
394
+ // inside the host tag it renders itself.
395
+ pub fn render_inner(&self) -> String {
396
+ ${prelude}${prelude ? "\n" : ""} let mut __out = String::new();
397
+ ${renderBody}
398
+ format!("<template shadowrootmode=\\"open\\">{}</template>", __out)
399
+ }
400
+
401
+ pub fn render(&self) -> String {
402
+ ${wrapper}
403
+ }
404
+ }
405
+
406
+ impl std::fmt::Display for ${component.className} {
407
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408
+ f.write_str(&self.render())
409
+ }
410
+ }
411
+ `;
412
+ }
413
+ emitNew(props) {
414
+ const params = [];
415
+ const inits = [];
416
+ for (const prop of props) {
417
+ const name = snake(prop.name);
418
+ switch (prop.type.kind) {
419
+ case "string":
420
+ params.push(`${name}: impl Into<String>`);
421
+ inits.push(`${name}: ${name}.into()`);
422
+ break;
423
+ case "struct":
424
+ // `impl Into<T>` lets callers feed their own types (DB rows etc.)
425
+ // through a single From impl.
426
+ params.push(`${name}: impl Into<${prop.type.name}>`);
427
+ inits.push(`${name}: ${name}.into()`);
428
+ break;
429
+ case "array":
430
+ if (prop.type.elem.kind === "string" || prop.type.elem.kind === "struct") {
431
+ params.push(`${name}: impl IntoIterator<Item = impl Into<${rustType(prop.type.elem)}>>`);
432
+ inits.push(`${name}: ${name}.into_iter().map(Into::into).collect()`);
433
+ }
434
+ else {
435
+ params.push(`${name}: impl IntoIterator<Item = ${rustType(prop.type.elem)}>`);
436
+ inits.push(`${name}: ${name}.into_iter().collect()`);
437
+ }
438
+ break;
439
+ default:
440
+ params.push(`${name}: ${rustType(prop.type)}`);
441
+ inits.push(name);
442
+ }
443
+ }
444
+ const body = inits.length > 0 ? `Self { ${inits.join(", ")} }` : "Self {}";
445
+ return ` pub fn new(${params.join(", ")}) -> Self {
446
+ ${body}
447
+ }`;
448
+ }
449
+ dataAttrValue(prop) {
450
+ const field = `self.${snake(prop.name)}`;
451
+ if (prop.type.kind === "option") {
452
+ const value = this.attrValue("v", prop.type.inner);
453
+ return `match &${field} { Some(v) => format!(" data-${kebab(prop.name)}=\\"{}\\"", ${value}), None => String::new() }`;
454
+ }
455
+ return this.attrValue(field, prop.type);
456
+ }
457
+ attrValue(path, type) {
458
+ if (type.kind === "string") {
459
+ this.needsEscape = true;
460
+ return `argon_escape_attr(&${path})`;
461
+ }
462
+ if (type.kind === "number" || type.kind === "boolean") {
463
+ return path;
464
+ }
465
+ this.needsEscape = true;
466
+ return `argon_escape_attr(&${this.jsonValue(path, type)})`;
467
+ }
468
+ // A Rust expression producing the JSON encoding of `path`. The compiler
469
+ // knows every field type, so serializers are generated — no serde needed.
470
+ jsonValue(path, type) {
471
+ switch (type.kind) {
472
+ case "string":
473
+ this.needsJsonStr = true;
474
+ return `argon_json_str(&${path})`;
475
+ case "number":
476
+ case "boolean":
477
+ return `${path}.to_string()`;
478
+ case "struct":
479
+ this.jsonStructs.add(type.name);
480
+ return `${path}.argon_json()`;
481
+ case "array":
482
+ case "fixed":
483
+ if (type.elem.kind === "string") {
484
+ this.needsJsonStrings = true;
485
+ return `argon_json_strings(&${path})`;
486
+ }
487
+ if (type.elem.kind === "number") {
488
+ this.needsJsonNumbers = true;
489
+ return `argon_json_numbers(&${path})`;
490
+ }
491
+ return `format!("[{}]", ${path}.iter().map(|v| ${this.jsonValue("v", type.elem)}).collect::<Vec<String>>().join(","))`;
492
+ case "tuple": {
493
+ const parts = type.elems.map((elem, i) => this.jsonValue(`${path}.${i}`, elem));
494
+ return `format!("[{}]", [${parts.join(", ")}].join(","))`;
495
+ }
496
+ case "option":
497
+ return `${path}.as_ref().map(|v| ${this.jsonValue("v", type.inner)}).unwrap_or_else(|| "null".to_string())`;
498
+ case "unknown":
499
+ return `${path}.to_string()`;
500
+ }
501
+ }
502
+ // fn argon_json(&self) -> String impls for every struct reachable from a
503
+ // component prop, keyed by the original TS field names so the hydrated
504
+ // client sees the same shape it renders with.
505
+ emitJsonImpls() {
506
+ const impls = [];
507
+ // jsonValue() can enqueue nested structs while we emit; drain until stable.
508
+ const done = new Set();
509
+ while (true) {
510
+ const next = [...this.jsonStructs].find(name => !done.has(name));
511
+ if (!next)
512
+ break;
513
+ done.add(next);
514
+ const struct = this.module.structs.get(next);
515
+ if (!struct)
516
+ continue;
517
+ const parts = struct.fields.map(f => `\\"${f.name}\\":{}`).join(",");
518
+ const args = struct.fields.map(f => this.jsonValue(`self.${snake(f.name)}`, f.type));
519
+ impls.push(`impl ${struct.name} {
520
+ fn argon_json(&self) -> String {
521
+ format!("{{${parts}}}"${args.length ? ", " + args.join(", ") : ""})
522
+ }
523
+ }
524
+ `);
525
+ }
526
+ return impls;
527
+ }
528
+ // ── Template → write! sequence ─────────────────────────────────────────────
529
+ //
530
+ // The declarative template is emitted as one `write!` per interpolation
531
+ // (interleaved with `push_str` of the static HTML), each on its own line and
532
+ // preceded by a `// argon: file.tsx:line:col` comment. A rustc error then
533
+ // lands on a meaningful line pointing back at the source expression, instead
534
+ // of somewhere inside a single 700-column format! string.
535
+ templateSegments(component) {
536
+ const NUL = String.fromCharCode(0);
537
+ // Classify each marker by its position in the template: an attribute value
538
+ // (HTML-attribute-escaped), a bound boolean attribute (present-or-absent),
539
+ // or text content (HTML-text-escaped unless it is markup).
540
+ const attrOf = new Map();
541
+ const boolAttr = new Set();
542
+ // 0.3 compat renders every marker verbatim: no attribute/text distinction,
543
+ // no boolean-attribute collapse, no escaping.
544
+ if (!this.opts.compat03) {
545
+ for (const b of component.template.attrBindings) {
546
+ for (const idx of b.exprIndices)
547
+ attrOf.set(idx, b.attr);
548
+ if (isBooleanAttr(b.attr) && b.exprIndices.length === 1 && b.value.replace(MARKER_RE, "") === "") {
549
+ boolAttr.add(b.exprIndices[0]);
550
+ }
551
+ }
552
+ }
553
+ let html = serialize(component.template.nodes);
554
+ // Collapse a bound boolean attribute to a single whole-chunk marker so it
555
+ // can render as the bare ` name` or nothing.
556
+ html = html.replace(new RegExp(`\\s([a-zA-Z][a-zA-Z0-9-]*)="${NUL}(\\d+)${NUL}"`, "g"), (m, _name, idx) => boolAttr.has(Number(idx)) ? `${NUL}${idx}${NUL}` : m);
557
+ const segments = [];
558
+ const re = new RegExp(`${NUL}(\\d+)${NUL}`, "g");
559
+ let last = 0;
560
+ let m;
561
+ while ((m = re.exec(html)) !== null) {
562
+ if (m.index > last)
563
+ segments.push({ lit: html.slice(last, m.index) });
564
+ const index = Number(m[1]);
565
+ const expr = component.template.exprs[index];
566
+ const span = component.template.exprSpans[index];
567
+ const loc = span ? ` // argon: ${this.loc(span.start)}\n` : "";
568
+ let write;
569
+ if (expr.kind === "toFixed") {
570
+ write = `write!(__out, "{:.${expr.digits}}", ${this.expr(expr.value)}).ok();`;
571
+ }
572
+ else if (this.opts.compat03) {
573
+ write = `write!(__out, "{}", ${this.expr(expr)}).ok();`;
574
+ }
575
+ else if (boolAttr.has(index)) {
576
+ write = `write!(__out, "{}", if ${this.truthy(expr)} { " ${attrOf.get(index)}" } else { "" }).ok();`;
577
+ }
578
+ else if (attrOf.has(index)) {
579
+ // A composed child's prop value already encodes and escapes itself.
580
+ write = `write!(__out, "{}", ${expr.kind === "propattr" ? this.expr(expr) : this.attrText(expr)}).ok();`;
581
+ }
582
+ else {
583
+ write = `write!(__out, "{}", ${this.renderText(expr)}).ok();`;
584
+ }
585
+ segments.push({ code: loc + " " + write });
586
+ last = re.lastIndex;
587
+ }
588
+ if (last < html.length)
589
+ segments.push({ lit: html.slice(last) });
590
+ return segments;
591
+ }
592
+ // `file.tsx:line:col` for a source offset.
593
+ loc(pos) {
594
+ const lc = this.module.source.getLineAndCharacterOfPosition(pos);
595
+ return `${basename(this.module.source.fileName)}:${lc.line + 1}:${lc.character + 1}`;
596
+ }
597
+ // A boolean condition for a bound boolean attribute (`disabled={expr}`).
598
+ truthy(e) {
599
+ if (e.type.kind === "option")
600
+ return `${this.operand(e)}.unwrap_or(false)`;
601
+ return this.operand(e);
602
+ }
603
+ // Render a value into HTML text content: markup passes through raw, a scalar
604
+ // string is HTML-text-escaped, numbers/booleans stringify as-is.
605
+ renderText(e) {
606
+ if (markupValue(e, this.markupLocals))
607
+ return this.markup(e, false);
608
+ if (e.type.kind === "string") {
609
+ this.needsEscapeText = true;
610
+ if (e.kind === "str")
611
+ return `argon_escape_text(${JSON.stringify(e.value)})`;
612
+ if (e.kind === "ref" && e.space === "param")
613
+ return `argon_escape_text(${this.expr(e)})`;
614
+ return `argon_escape_text(&${this.operand(e)})`;
615
+ }
616
+ return this.expr(e);
617
+ }
618
+ // Render a value into a double-quoted attribute: strings and JSON payloads
619
+ // are attribute-escaped; numbers/booleans stringify as-is.
620
+ attrText(e) {
621
+ if (e.type.kind === "number" || e.type.kind === "boolean")
622
+ return this.expr(e);
623
+ this.needsEscape = true;
624
+ if (e.type.kind === "string") {
625
+ if (e.kind === "str")
626
+ return `argon_escape_attr(${JSON.stringify(e.value)})`;
627
+ if (e.kind === "ref" && e.space === "param")
628
+ return `argon_escape_attr(${this.expr(e)})`;
629
+ return `argon_escape_attr(&${this.operand(e)})`;
630
+ }
631
+ return `argon_escape_attr(&${this.jsonValue(this.operand(e), e.type)})`;
632
+ }
633
+ // Render markup (already-HTML) raw, escaping only interpolated scalar parts.
634
+ // `owned` forces a String result (needed inside conditional branches).
635
+ markup(e, owned) {
636
+ switch (e.kind) {
637
+ case "str": return owned ? `${JSON.stringify(e.value)}.to_string()` : JSON.stringify(e.value);
638
+ case "template": return this.markupTemplate(e);
639
+ case "construct": return this.construct(e);
640
+ case "raw": return owned ? this.owned(e.inner) : this.expr(e.inner);
641
+ case "cond": {
642
+ // Two static markup-literal branches stay borrowed &str; anything
643
+ // dynamic or escaped forces both to owned String so their types match.
644
+ // Each branch renders by its own kind: markup raw, a plain string escaped.
645
+ const bothStr = isMarkup(e.whenTrue) && isMarkup(e.whenFalse) && e.whenTrue.kind === "str" && e.whenFalse.kind === "str";
646
+ const branch = (b) => {
647
+ if (isMarkup(b))
648
+ return this.markup(b, !bothStr);
649
+ if (b.type.kind === "string") {
650
+ this.needsEscapeText = true;
651
+ return b.kind === "str" ? `argon_escape_text(${JSON.stringify(b.value)})` : `argon_escape_text(&${this.operand(b)})`;
652
+ }
653
+ return this.owned(b);
654
+ };
655
+ return `if ${this.expr(e.cond)} { ${branch(e.whenTrue)} } else { ${branch(e.whenFalse)} }`;
656
+ }
657
+ case "join":
658
+ if (e.array.kind === "map") {
659
+ return `${this.iterChain(e.array, r => this.renderText(r))}.collect::<Vec<String>>().join(${JSON.stringify(e.sep)})`;
660
+ }
661
+ return this.join(e);
662
+ // A ref to a markup local: its value is already HTML.
663
+ default: return owned ? this.owned(e) : this.expr(e);
664
+ }
665
+ }
666
+ markupTemplate(e) {
667
+ const inAttr = attrPositions(e.quasis);
668
+ let fmt = escapeBraces(e.quasis[0]);
669
+ const args = [];
670
+ e.parts.forEach((part, i) => {
671
+ if (part.kind === "toFixed") {
672
+ fmt += `{:.${part.digits}}`;
673
+ args.push(this.expr(part.value));
674
+ }
675
+ else {
676
+ fmt += "{}";
677
+ // A composed child's prop value self-encodes; otherwise escape by
678
+ // position (attribute value vs text content).
679
+ args.push(part.kind === "propattr" ? this.expr(part) : inAttr[i] ? this.attrText(part) : this.renderText(part));
680
+ }
681
+ fmt += escapeBraces(e.quasis[i + 1]);
682
+ });
683
+ return `format!(${JSON.stringify(fmt)}${args.length ? ", " + args.join(", ") : ""})`;
684
+ }
685
+ // ── Statements ─────────────────────────────────────────────────────────────
686
+ stmt(stmt) {
687
+ if (stmt.kind === "let") {
688
+ if (stmt.tuple) {
689
+ return `let (${stmt.names.map(snake).join(", ")}) = ${this.expr(stmt.init)};`;
690
+ }
691
+ // Markup stored in a local escapes its interpolated parts, so reading it
692
+ // raw later cannot inject unescaped values.
693
+ const init = isMarkup(stmt.init) ? this.markup(stmt.init, true) : this.owned(stmt.init);
694
+ return `let ${snake(stmt.names[0])} = ${init};`;
695
+ }
696
+ // A tail return is the block's trailing expression (implicit).
697
+ if (stmt.kind === "return")
698
+ return this.owned(stmt.value);
699
+ if (stmt.kind === "if")
700
+ return this.ifStmt(stmt);
701
+ // Opaque statements are filtered out by analyze() before emission.
702
+ return "";
703
+ }
704
+ ifStmt(stmt) {
705
+ const body = (stmts) => stmts.map(s => this.blockStmt(s)).join(" ");
706
+ const els = stmt.else ? ` else { ${body(stmt.else)} }` : "";
707
+ return `if ${this.expr(stmt.cond)} { ${body(stmt.then)} }${els}`;
708
+ }
709
+ // Inside a control-flow block, a return is an early return and needs the
710
+ // explicit keyword (the tail return at function scope stays implicit).
711
+ blockStmt(stmt) {
712
+ if (stmt.kind === "return")
713
+ return `return ${this.owned(stmt.value)};`;
714
+ return this.stmt(stmt);
715
+ }
716
+ // ── Expressions ────────────────────────────────────────────────────────────
717
+ // Emit a value in display/borrow position: string literals stay &str.
718
+ expr(e) {
719
+ switch (e.kind) {
720
+ case "str": return JSON.stringify(e.value);
721
+ case "num": return e.text.includes(".") || e.text.includes("e") ? e.text : `${e.text}.0`;
722
+ case "bool": return String(e.value);
723
+ case "ref": return this.ref(e.name, e.space);
724
+ case "member": return `${this.operand(e.object)}.${snake(e.name)}`;
725
+ // JS string length counts UTF-16 units; we count Unicode scalars, which
726
+ // agrees for the BMP. Array/slice length is element count.
727
+ case "length":
728
+ return e.object.type.kind === "string"
729
+ ? `(${this.operand(e.object)}.chars().count() as f64)`
730
+ : `(${this.operand(e.object)}.len() as f64)`;
731
+ case "index": return this.index(e.object, e.index);
732
+ case "lookup": return `${snake(e.table)}(${this.borrowStr(e.key)})`;
733
+ case "exists":
734
+ // Definite values compared against undefined fold to a constant.
735
+ if (e.object.type.kind !== "option")
736
+ return String(e.present);
737
+ return `${this.operand(e.object)}.${e.present ? "is_some" : "is_none"}()`;
738
+ case "unwrap":
739
+ // The enclosing presence test guarantees Some; borrow for display.
740
+ return isCopy(e.type)
741
+ ? `${this.expr(e.object)}.unwrap()`
742
+ : `${this.expr(e.object)}.as_ref().unwrap()`;
743
+ case "unary": return `${e.op}(${this.expr(e.operand)})`;
744
+ case "binary": return this.binary(e);
745
+ case "cond": return this.cond(e);
746
+ case "template": return this.template(e);
747
+ case "call": return this.callExpr(e);
748
+ case "map": return `${this.iterChain(e)}.collect::<Vec<${rustType(elemOf(e.type))}>>()`;
749
+ case "join": return this.join(e);
750
+ case "minmax": {
751
+ const bound = e.op === "min" ? "f64::INFINITY" : "f64::NEG_INFINITY";
752
+ return `${this.iterOver(e.array)}.fold(${bound}, f64::${e.op})`;
753
+ }
754
+ case "math": return this.mathExpr(e);
755
+ case "strmethod": return this.strMethod(e);
756
+ case "arrmethod": return this.arrMethod(e);
757
+ case "toFixed": return `format!("{:.${e.digits}}", ${this.expr(e.value)})`;
758
+ case "arr": return this.arrLiteral(e);
759
+ case "obj": {
760
+ const decl = e.struct ? this.module.structs.get(e.struct) : undefined;
761
+ const fields = e.fields.map(f => {
762
+ const declared = decl?.fields.find(df => df.name === f.name)?.type;
763
+ const value = this.owned(f.value);
764
+ // Definite values land in optional fields wrapped in Some(...).
765
+ const wrap = declared?.kind === "option" && f.value.type.kind !== "option";
766
+ return `${snake(f.name)}: ${wrap ? `Some(${value})` : value}`;
767
+ });
768
+ // Omitted optional fields default to None.
769
+ const provided = new Set(e.fields.map(f => f.name));
770
+ const omitted = (decl?.fields ?? [])
771
+ .filter(df => df.type.kind === "option" && !provided.has(df.name))
772
+ .map(df => `${snake(df.name)}: None`);
773
+ return `${e.struct} { ${[...fields, ...omitted].join(", ")} }`;
774
+ }
775
+ case "propattr": return this.propAttr(e);
776
+ case "construct": return this.construct(e);
777
+ case "raw": return this.expr(e.inner);
778
+ case "opaque": return "/* unreachable: opaque */ unreachable!()";
779
+ }
780
+ }
781
+ // A composed child's prop value in attribute position: the same encoding
782
+ // the child's own wrapper uses, so the child hydrates identically.
783
+ propAttr(e) {
784
+ if (e.propType.kind === "number" || e.propType.kind === "boolean")
785
+ return this.expr(e.value);
786
+ this.needsEscape = true;
787
+ if (e.propType.kind === "string")
788
+ return `argon_escape_attr(&${this.operand(e.value)})`;
789
+ return `argon_escape_attr(&${this.jsonValue(this.operand(e.value), e.propType)})`;
790
+ }
791
+ // A composed child's shadow DOM: construct it with the parent's values and
792
+ // inline its declarative template. Cross-file children live in sibling
793
+ // modules named after their source file.
794
+ construct(e) {
795
+ const target = e.module ? `super::${e.module}::${e.className}` : e.className;
796
+ const args = e.props.map(p => {
797
+ if (!p.value)
798
+ return "None";
799
+ const value = this.owned(p.value);
800
+ return p.type.kind === "option" && p.value.type.kind !== "option" ? `Some(${value})` : value;
801
+ });
802
+ return `${target}::new(${args.join(", ")}).render_inner()`;
803
+ }
804
+ // Emit a value in owned position (let binding, struct field, collect item):
805
+ // ensures strings are String and non-Copy places are cloned.
806
+ owned(e) {
807
+ if (e.kind === "str")
808
+ return `${JSON.stringify(e.value)}.to_string()`;
809
+ if (e.kind === "cond" && e.type.kind === "string") {
810
+ return `if ${this.expr(e.cond)} { ${this.owned(e.whenTrue)} } else { ${this.owned(e.whenFalse)} }`;
811
+ }
812
+ if (e.kind === "binary" && e.op === "||" && e.type.kind === "string") {
813
+ return `{ let __v = ${this.owned(e.left)}; if __v.is_empty() { ${this.owned(e.right)} } else { __v } }`;
814
+ }
815
+ if (e.kind === "unwrap") {
816
+ return isCopy(e.type)
817
+ ? `${this.expr(e.object)}.unwrap()`
818
+ : `${this.expr(e.object)}.clone().unwrap()`;
819
+ }
820
+ if (isPlace(e) && !isCopy(e.type)) {
821
+ if (e.kind === "ref" && e.space === "param") {
822
+ // Borrowed params: &str/&[T] → owned.
823
+ return e.type.kind === "string" ? `${this.expr(e)}.to_string()` : `${this.expr(e)}.to_vec()`;
824
+ }
825
+ return `${this.expr(e)}.clone()`;
826
+ }
827
+ return this.expr(e);
828
+ }
829
+ // Emit a &str-typed argument (lookup keys, &str params).
830
+ borrowStr(e) {
831
+ if (e.kind === "str")
832
+ return JSON.stringify(e.value);
833
+ if (e.kind === "ref" && e.space === "param" && e.type.kind === "string")
834
+ return this.expr(e);
835
+ return `&${this.operand(e)}`;
836
+ }
837
+ // Wrap operands whose Rust spelling could bind differently in parens.
838
+ operand(e) {
839
+ const needsParens = e.kind === "binary" || e.kind === "cond" || e.kind === "unary";
840
+ return needsParens ? `(${this.expr(e)})` : this.expr(e);
841
+ }
842
+ ref(name, space) {
843
+ if (space === "prop")
844
+ return `self.${snake(name)}`;
845
+ if (space === "global")
846
+ return snake(name).toUpperCase();
847
+ if (this.usizeVars.has(name))
848
+ return `(${snake(name)} as f64)`;
849
+ return snake(name);
850
+ }
851
+ index(object, index) {
852
+ const idx = index.kind === "num" && !index.text.includes(".")
853
+ ? index.text
854
+ : `(${this.expr(index)}) as usize`;
855
+ return `${this.operand(object)}[${idx}]`;
856
+ }
857
+ binary(e) {
858
+ const { op, left, right } = e;
859
+ if (op === "??") {
860
+ // Lowering guarantees a definite right side.
861
+ if (left.type.kind !== "option")
862
+ return this.expr(left);
863
+ if (isCopy(left.type))
864
+ return `${this.operand(left)}.unwrap_or(${this.expr(right)})`;
865
+ return `${this.operand(left)}.clone().unwrap_or_else(|| ${this.owned(right)})`;
866
+ }
867
+ if (op === "+" && e.type.kind === "string") {
868
+ return `format!("{}{}", ${this.expr(left)}, ${this.expr(right)})`;
869
+ }
870
+ if (op === "||" && left.type.kind === "number") {
871
+ // JS `x || fallback` on numbers: fall back when zero.
872
+ return `{ let __v = ${this.expr(left)}; if __v == 0.0 { ${this.expr(right)} } else { __v } }`;
873
+ }
874
+ if (op === "||" && left.type.kind === "string") {
875
+ return `{ let __v = ${this.owned(left)}; if __v.is_empty() { ${this.owned(right)} } else { __v } }`;
876
+ }
877
+ return `${this.operand(left)} ${op} ${this.operand(right)}`;
878
+ }
879
+ cond(e) {
880
+ let whenTrue = this.expr(e.whenTrue);
881
+ let whenFalse = this.expr(e.whenFalse);
882
+ if (e.type.kind === "string" && !(e.whenTrue.kind === "str" && e.whenFalse.kind === "str")) {
883
+ // Branch types must match; mixed literal/String branches go owned.
884
+ whenTrue = this.owned(e.whenTrue);
885
+ whenFalse = this.owned(e.whenFalse);
886
+ }
887
+ return `if ${this.expr(e.cond)} { ${whenTrue} } else { ${whenFalse} }`;
888
+ }
889
+ template(e) {
890
+ let fmt = escapeBraces(e.quasis[0]);
891
+ const args = [];
892
+ e.parts.forEach((part, i) => {
893
+ if (part.kind === "toFixed") {
894
+ fmt += `{:.${part.digits}}`;
895
+ args.push(this.expr(part.value));
896
+ }
897
+ else {
898
+ fmt += "{}";
899
+ args.push(this.expr(part));
900
+ }
901
+ fmt += escapeBraces(e.quasis[i + 1]);
902
+ });
903
+ return `format!(${JSON.stringify(fmt)}${args.length ? ", " + args.join(", ") : ""})`;
904
+ }
905
+ callExpr(e) {
906
+ const sig = this.fns.get(e.fn);
907
+ const args = e.args.map((arg, i) => {
908
+ const param = sig?.params[i]?.type;
909
+ if (!param)
910
+ return this.expr(arg);
911
+ if (param.kind === "string")
912
+ return this.borrowStr(arg);
913
+ if (param.kind === "array" || param.kind === "fixed" || param.kind === "struct") {
914
+ if (arg.kind === "ref" && arg.space === "param")
915
+ return this.expr(arg);
916
+ return `&${this.expr(arg)}`;
917
+ }
918
+ return this.expr(arg);
919
+ });
920
+ return `${snake(e.fn)}(${args.join(", ")})`;
921
+ }
922
+ // Emitted in `f64::fn(args)` associated-function form so a literal receiver
923
+ // (e.g. Math.max(0, x)) is never an ambiguous numeric type.
924
+ mathExpr(e) {
925
+ const a = e.args.map(x => this.expr(x));
926
+ switch (e.fn) {
927
+ case "min":
928
+ case "max":
929
+ return a.reduce((acc, x, i) => (i === 0 ? x : `f64::${e.fn}(${acc}, ${x})`));
930
+ case "floor":
931
+ case "ceil":
932
+ case "trunc":
933
+ case "abs":
934
+ case "sqrt":
935
+ return `f64::${e.fn}(${a[0]})`;
936
+ // JS Math.round is half-up (toward +∞), which floor(x + 0.5) reproduces
937
+ // exactly; f64::round would round half away from zero.
938
+ case "round":
939
+ return `f64::floor(${a[0]} + 0.5)`;
940
+ // JS Math.sign(0) is 0; f64::signum(0.0) is 1.0, so inline the comparison.
941
+ case "sign":
942
+ return `{ let __s: f64 = ${a[0]}; if __s > 0.0 { 1.0 } else if __s < 0.0 { -1.0 } else { 0.0 } }`;
943
+ case "pow":
944
+ return `f64::powf(${a[0]}, ${a[1]})`;
945
+ default:
946
+ return `0.0 /* unsupported math */`;
947
+ }
948
+ }
949
+ strMethod(e) {
950
+ const recv = this.operand(e.object);
951
+ const arg = (i) => this.strArg(e.args[i]);
952
+ const usize = (i) => `(${this.expr(e.args[i])} as usize)`;
953
+ switch (e.method) {
954
+ case "toUpperCase": return `${recv}.to_uppercase()`;
955
+ case "toLowerCase": return `${recv}.to_lowercase()`;
956
+ case "trim": return `${recv}.trim().to_string()`;
957
+ case "includes": return `${recv}.contains(${arg(0)})`;
958
+ case "startsWith": return `${recv}.starts_with(${arg(0)})`;
959
+ case "endsWith": return `${recv}.ends_with(${arg(0)})`;
960
+ case "repeat": return `${recv}.repeat(${usize(0)})`;
961
+ case "charAt": return `${recv}.chars().nth(${usize(0)}).map(|c| c.to_string()).unwrap_or_default()`;
962
+ case "replaceAll": return `${recv}.replace(${arg(0)}, ${arg(1)})`;
963
+ case "slice": {
964
+ this.needsStrSlice = true;
965
+ const end = e.args[1] ? `Some(${this.expr(e.args[1])})` : "None";
966
+ return `argon_str_slice(${this.borrowStr(e.object)}, ${this.expr(e.args[0])}, ${end})`;
967
+ }
968
+ case "padStart":
969
+ case "padEnd": {
970
+ this.needsPad = true;
971
+ const pad = e.args[1] ? this.strArg(e.args[1]) : `" "`;
972
+ return `argon_pad(${this.borrowStr(e.object)}, ${this.expr(e.args[0])}, ${pad}, ${e.method === "padStart"})`;
973
+ }
974
+ default: return `String::new()`;
975
+ }
976
+ }
977
+ arrMethod(e) {
978
+ const obj = this.operand(e.object);
979
+ const elem = elemOf(e.object.type);
980
+ // Predicate methods: the closure param is &T in every case; a Copy element
981
+ // is deref-bound so numeric/boolean predicates read a value.
982
+ if (e.param && e.body) {
983
+ const p = snake(e.param);
984
+ const body = isCopy(elem) ? `{ let ${p} = *${p}; ${this.expr(e.body)} }` : this.expr(e.body);
985
+ switch (e.method) {
986
+ case "filter": return `${obj}.clone().into_iter().filter(|${p}| ${body}).collect::<Vec<${rustType(elem)}>>()`;
987
+ case "find": return `${obj}.clone().into_iter().find(|${p}| ${body})`;
988
+ case "some": return `${obj}.iter().any(|${p}| ${body})`;
989
+ case "every": return `${obj}.iter().all(|${p}| ${body})`;
990
+ }
991
+ }
992
+ if (e.method === "includes")
993
+ return `${obj}.contains(&${this.owned(e.args[0])})`;
994
+ if (e.method === "indexOf") {
995
+ return `${obj}.iter().position(|__x| __x == &${this.owned(e.args[0])}).map(|__i| __i as f64).unwrap_or(-1.0)`;
996
+ }
997
+ // slice
998
+ this.needsSlice = true;
999
+ const end = e.args[1] ? `Some(${this.expr(e.args[1])})` : "None";
1000
+ return `argon_slice(&${obj}, ${this.expr(e.args[0])}, ${end})`;
1001
+ }
1002
+ // A string method's &str argument (patterns for contains/replace).
1003
+ strArg(e) {
1004
+ if (e.kind === "str")
1005
+ return JSON.stringify(e.value);
1006
+ if (e.kind === "ref" && e.space === "param" && e.type.kind === "string")
1007
+ return this.expr(e);
1008
+ return `${this.operand(e)}.as_str()`;
1009
+ }
1010
+ join(e) {
1011
+ if (e.array.kind === "map") {
1012
+ return `${this.iterChain(e.array)}.collect::<Vec<String>>().join(${JSON.stringify(e.sep)})`;
1013
+ }
1014
+ // A string array joins directly; other element types stringify first
1015
+ // (JS coerces, e.g. [1,2].join('-') === '1-2').
1016
+ if (elemOf(e.array.type).kind === "string") {
1017
+ return `${this.expr(e.array)}.join(${JSON.stringify(e.sep)})`;
1018
+ }
1019
+ return `${this.iterOver(e.array)}.map(|__v| __v.to_string()).collect::<Vec<String>>().join(${JSON.stringify(e.sep)})`;
1020
+ }
1021
+ iterOver(array) {
1022
+ if (array.kind === "map")
1023
+ return this.iterChain(array);
1024
+ const elem = elemOf(array.type);
1025
+ const adapt = isCopy(elem) || elem.kind === "unknown" ? ".copied()" : ".cloned()";
1026
+ return `${this.expr(array)}.iter()${adapt}`;
1027
+ }
1028
+ iterChain(e, renderResult = r => this.owned(r)) {
1029
+ const source = this.iterOver(e.array);
1030
+ let params;
1031
+ if (e.indexVar) {
1032
+ const item = typeof e.item === "string" ? snake(e.item) : `(${e.item.map(snake).join(", ")})`;
1033
+ params = `(${snake(e.indexVar)}, ${item})`;
1034
+ this.usizeVars.add(e.indexVar);
1035
+ }
1036
+ else if (typeof e.item === "string") {
1037
+ params = snake(e.item);
1038
+ }
1039
+ else {
1040
+ params = `(${e.item.map(snake).join(", ")})`;
1041
+ }
1042
+ const stmts = e.body.stmts.map(s => this.stmt(s) + " ");
1043
+ const result = renderResult(e.body.result);
1044
+ const body = stmts.length > 0 ? `{ ${stmts.join("")}${result} }` : result;
1045
+ if (e.indexVar)
1046
+ this.usizeVars.delete(e.indexVar);
1047
+ const enumerate = e.indexVar ? ".enumerate()" : "";
1048
+ return `${source}${enumerate}.map(|${params}| ${body})`;
1049
+ }
1050
+ arrLiteral(e) {
1051
+ if (e.type.kind === "array")
1052
+ return `vec![${e.elems.map(el => this.owned(el)).join(", ")}]`;
1053
+ if (e.type.kind === "fixed")
1054
+ return `[${e.elems.map(el => this.owned(el)).join(", ")}]`;
1055
+ return `(${e.elems.map(el => this.expr(el)).join(", ")})`;
1056
+ }
1057
+ }
1058
+ function elemOf(type) {
1059
+ if (type.kind === "array" || type.kind === "fixed")
1060
+ return type.elem;
1061
+ return { kind: "unknown" };
1062
+ }
1063
+ function escapeBraces(text) {
1064
+ return text.replace(/\{/g, "{{").replace(/\}/g, "}}");
1065
+ }
1066
+ // Escape a static HTML segment for a Rust string literal (push_str). Only the
1067
+ // backslash and double-quote need escaping; newlines are kept verbatim (Rust
1068
+ // string literals may span lines), matching the previous format!-string form.
1069
+ function rustStrLit(text) {
1070
+ return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1071
+ }
1072
+ function basename(file) {
1073
+ const slash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
1074
+ return slash === -1 ? file : file.slice(slash + 1);
1075
+ }
1076
+ //# sourceMappingURL=rust.js.map