gofront 0.0.6 → 0.0.7

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.
package/README.md CHANGED
@@ -184,7 +184,7 @@ The type checker enforces correctness; JavaScript doesn't need to know.
184
184
 
185
185
  ## Examples — Todo App
186
186
 
187
- There are two example apps — both implement the same todo app to show different aspects
187
+ There are three example apps — all implement the same todo app to show different aspects
188
188
  of GoFront.
189
189
 
190
190
  ### Simple (vanilla DOM)
@@ -202,7 +202,7 @@ example/simple/
202
202
  styles.go ← injectStyles() creates <style> element with all CSS
203
203
  main.go ← createApp() builds DOM shell · setupEvents() · event delegation
204
204
  utils/
205
- utils.go ← Plural() · generic Filter/Map — cross-package import demo
205
+ utils.go ← Plural() · generic Filter — cross-package import demo
206
206
  browser.d.ts ← minimal external type declarations (sleep)
207
207
  index.html ← bare HTML shell, loads app.js as ES module
208
208
  app.js ← generated output
@@ -212,41 +212,99 @@ example/simple/
212
212
 
213
213
  Same app rebuilt with [reactive.js](https://github.com/seriva/microtastic), a tiny
214
214
  signals-based reactive framework. Demonstrates how GoFront integrates with external JS
215
- libraries via `.d.ts` type declarations — the GoFront code uses typed `Signal` values,
216
- `Signals.create()`, `Signals.computed()`, `Signals.batch()`, and `Reactive.bind()`,
217
- all described in a hand-written `browser.d.ts` shim.
215
+ libraries via `.d.ts` type declarations — the entire reactive API surface is typed via a
216
+ hand-written `browser.d.ts` shim and exercised from GoFront source.
218
217
 
219
218
  ```
220
219
  example/reactive/
221
220
  src/
222
221
  types.go ← Todo, Stats, AppElements structs
223
- store.go ← Signal-typed state · Signals.create/computed/batch
224
- render.go ← createAppShell() returns AppElements · ctx.computed/ctx.bind
225
- styles.go ← injectStyles()
226
- main.go ← wires everything together
222
+ store.go ← reactive state: Signals.create/computed/computedAsync/batch/update
223
+ render.go ← Reactive.Component lifecycle · ctx.bind* · setupStatsBar
224
+ styles.go ← per-section cssClass() scoped styles
225
+ main.go ← async boot, Reactive.mount loading placeholder, signal.subscribe/once
227
226
  utils/
228
- utils.go ← Plural() · generic Filter/Map
229
- browser.d.ts ← Signal interface · Signals/Reactive/ComponentContext namespaces
227
+ utils.go ← Plural() · generic Filter
228
+ browser.d.ts ← Signal · ComponentContext · Signals/Reactive namespaces · htmlTag
230
229
  reactive.js ← signals framework (from microtastic)
231
- index.html ← loads reactive.js + app.js
230
+ index.html ← loads reactive.js, exposes helpers, imports app.js
232
231
  app.js ← generated output
233
232
  ```
234
233
 
234
+ The reactive example covers the full reactive.js API surface:
235
+
236
+ | Category | Features used |
237
+ |---|---|
238
+ | HTML helpers | `html` tagged template (via `htmlTag`), `trusted`, `join` |
239
+ | CSS | `cssClass` scoped styles with nested `&` selectors |
240
+ | Signals | `create`, `computed`, `computedAsync`, `batch`, `get`, `peek`, `set`, `update`, `subscribe`, `once` |
241
+ | Reactive namespace | `mount` (loading placeholder), `createComponent`, `scan` |
242
+ | Component context | `bind`, `bindAttr`, `bindBoolAttr`, `bindClass`, `bindText`, `bindStyle`, `bindMultiple`, `computed` |
243
+ | Component lifecycle | `state`, `template`, `styles`, `mount`, `mountTo`, `appendTo`, `refs` (via `data-ref`) |
244
+ | Component instance | `signal`, `effect`, `on` (auto-cleanup event listeners) |
245
+ | Scan attributes | `data-model`, `data-text`, `data-html`, `data-if`, `data-visible`, `data-class-*`, `data-attr-*`, `data-bool-*`, `data-on-*`, `data-ref` |
246
+
247
+ ### gom (stdlib component library + todo app)
248
+
249
+ Browser-native declarative DOM components inspired by
250
+ [gomponents](https://www.gomponents.com). `gom` is a **built-in stdlib package** — no
251
+ import path needed, available in every GoFront program just like `fmt` or `strings`. It
252
+ uses **methods on named non-struct types** so that plain functions and slices can satisfy
253
+ the `Node` interface without any struct boilerplate.
254
+
255
+ ```
256
+ example/gom/
257
+ src/
258
+ types.go ← Todo struct, filter/priority constants
259
+ store.go ← state, mutations, localStorage persistence
260
+ render.go ← gom node builders (pure node tree, no innerHTML)
261
+ styles.go ← appStyles() CSS string
262
+ main.go ← event setup, submitInput, entry point
263
+ utils/ ← Filter[T], Plural, HasText
264
+ app.js ← generated output
265
+ index.html ← HTML shell
266
+ ```
267
+
268
+ Key types and functions (all accessed as `gom.*`, no import needed):
269
+ - `gom.Node` — interface with a single `Mount(parent any)` method
270
+ - `gom.NodeFunc` — `type NodeFunc func(parent any)` with `Mount` method; any function becomes a `Node`
271
+ - `gom.Group` — `type Group []Node` with `Mount` method; composes children in order
272
+ - `gom.El(tag, children...)` — creates an element node
273
+ - `gom.Text(s)` — creates a text node
274
+ - `gom.Attr(name, val)` — generic attribute node; named shortcuts: `gom.Class`, `gom.Href`, `gom.Type`, etc.
275
+ - `gom.If(cond, node)` — conditionally renders a node
276
+ - `gom.Map(slice, fn)` — maps a slice to a `Group` of nodes
277
+ - `gom.Style(css)` — returns a `Node` that injects a `<style>` element; styles are part of the node tree
278
+ - `gom.Mount(selector, n)` — mounts a node into the DOM, replacing the element's content
279
+ - `gom.MountTo(selector, n)` — appends a node without clearing; used to inject styles into `<head>`
280
+ - HTML element helpers: `gom.Div`, `gom.Span`, `gom.Button`, `gom.Input`, `gom.Ul`, `gom.Li`, … (full HTML element set)
281
+
235
282
  ### Features demonstrated
236
283
 
237
- Both apps cover: structs & methods, iota constants, named return values, closures,
238
- slices, `for range`, `switch`, cross-package imports, multi-file same-package compilation,
239
- `async`/`await`, `defer`/`recover`, localStorage persistence, DOM APIs, and generic
240
- utility functions (`Filter[T]`, `Map[T, U]`).
284
+ The simple and reactive examples cover: structs & methods, iota constants, named return
285
+ values, closures, slices, `for range`, `switch`, cross-package imports, multi-file
286
+ same-package compilation, `async`/`await`, `defer`/`recover`, localStorage persistence,
287
+ DOM APIs, and generic utility functions (`Filter[T]`, `Map[T, U]`).
241
288
 
242
289
  The reactive example additionally demonstrates: external `.d.ts` type imports,
243
- `declare namespace` patterns for typing JS libraries, and reactive UI updates via signals.
290
+ `declare namespace` patterns for typing JS libraries, the full reactive signal graph
291
+ (source signals → computed signals → async computed → DOM bindings), reactive UI with
292
+ no `querySelector` or `getElementById` in application code, and `Reactive.Component`
293
+ as the primary composition unit (state, template, styles, mount lifecycle, `data-ref`
294
+ element refs, auto-cleanup event listeners).
295
+
296
+ The gom example additionally demonstrates: **the `gom` built-in stdlib package**,
297
+ styles as nodes (`gom.Style` + `gom.MountTo`), `gom.Map` for list rendering, the
298
+ declarative node-tree pattern (no `innerHTML`, no `querySelector`), and full feature
299
+ parity with the other examples (priority mode, validation, localStorage persistence,
300
+ sync status, drag-and-drop reordering).
244
301
 
245
302
  ### Build and run
246
303
 
247
304
  ```sh
248
305
  npm run build:simple # → example/simple/app.js
249
306
  npm run build:reactive # → example/reactive/app.js
307
+ npm run build:gom # → example/gom/app.js
250
308
  # open the respective index.html in a browser
251
309
  ```
252
310
 
@@ -367,6 +425,7 @@ signatures into GoFront's internal type representation.
367
425
  | `error` type | ✓ — interface `{ Error() string }`; custom error types, `errors.Is`/`Unwrap`, `%w` wrapping |
368
426
  | Complex numbers (`complex64`, `complex128`, `3i`) | ✓ — `complex()`, `real()`, `imag()` builtins; `__cmul`/`__cdiv` helpers |
369
427
  | Type definitions, type aliases (`type A = B`) | ✓ |
428
+ | Methods on named non-struct types | ✓ — `type T func(...)` or `type T []E` with methods; emitted as ES6 wrapper classes; satisfies interfaces |
370
429
  | Type conversions, type assertions (plain & comma-ok) | ✓ |
371
430
  | Type switch (`switch v := x.(type)`) | ✓ — compiles to `if/else if` with `typeof` / `instanceof` |
372
431
  | Sized integers (`int8`–`int64`, `uint8`–`uint64`, `float32`) | ✓ — mapped to `number` at runtime |
@@ -419,6 +478,8 @@ signatures into GoFront's internal type representation.
419
478
  | `regexp` | `MustCompile`, `Compile`, `MatchString`, `QuoteMeta`; **`*Regexp`** methods: `MatchString`, `FindString`, `FindStringIndex`, `FindAllString`, `FindStringSubmatch`, `FindAllStringSubmatch`, `ReplaceAllString`, `ReplaceAllLiteralString`, `Split`, `String`. Inline flags (`(?i)`, `(?m)`, `(?s)`) are extracted automatically into the JS `RegExp` constructor. |
420
479
  | `unicode` | `IsLetter`, `IsDigit`, `IsSpace`, `IsUpper`, `IsLower`, `IsPunct`, `IsControl`, `IsPrint`, `IsGraphic`, `ToUpper`, `ToLower` |
421
480
  | `os` | `Exit`, `Args`, `Getenv` |
481
+ | `io` | `Writer`, `Reader`, `ReadWriter`, `Closer` interface types — used for accepting `*strings.Builder`, `*bytes.Buffer`, or any compatible writer/reader |
482
+ | `gom` | Browser-native declarative DOM component library. **Types**: `Node` (interface), `NodeFunc`, `Group`. **Core**: `El(tag, children...)`, `Text(s)`, `Mount(sel, node)`, `MountTo(sel, node)`. **Attributes**: `Attr`, `Class`, `Href`, `Type`, `Src`, `Placeholder`, `DataAttr`, `Style`, `For`, `Name`, `Value`, `Target`, `Rel`, `Alt`, `Title`, `Draggable`, `Role`, `AriaLabel`, `StyleAttr`; boolean: `Disabled`, `Checked`, `Selected`, `Readonly`. **Logic**: `If(cond, node)`, `Map(slice, fn)`. **Elements**: full HTML element set (`Div`, `Span`, `Button`, `Input`, `Ul`, `Li`, `Table`, `Form`, `Img`, `A`, `H1`–`H6`, …) |
422
483
 
423
484
  ### Packages & imports
424
485
 
@@ -490,7 +551,7 @@ you know exactly what to expect.
490
551
 
491
552
  See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap and release history.
492
553
  Design documents for planned features are organised by release under `docs/v*/`
493
- (e.g. [`docs/v0.0.5/`](docs/v0.0.5/)).
554
+ (e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v0.0.9/`](docs/v0.0.9/)).
494
555
 
495
556
  ---
496
557
 
@@ -500,7 +561,7 @@ Design documents for planned features are organised by release under `docs/v*/`
500
561
  npm test
501
562
  ```
502
563
 
503
- 940 tests covering language features, type errors, edge cases, DOM (jsdom), external
564
+ 990 tests covering language features, type errors, edge cases, DOM (jsdom), external
504
565
  `.d.ts`, npm resolver, multi-file compilation, embedded structs, string formatting, map
505
566
  iteration order, integer overflow semantics, unused variable detection, unused import
506
567
  detection, semantic difference verification, stdlib shim packages, generics (type params,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,12 +33,15 @@
33
33
  "scripts": {
34
34
  "build:simple": "node src/index.js example/simple/src -o example/simple/app.js",
35
35
  "build:reactive": "node src/index.js example/reactive/src -o example/reactive/app.js",
36
- "test": "node test/run.js",
36
+ "build:gom": "node src/index.js example/gom/src -o example/gom/app.js",
37
+ "test": "node test/unit/run.js",
37
38
  "format": "biome format --write .",
38
- "check": "biome check ."
39
+ "check": "biome check .",
40
+ "prepare": "husky"
39
41
  },
40
42
  "devDependencies": {
41
- "@biomejs/biome": "^2.4.11",
43
+ "@biomejs/biome": "^2.4.12",
44
+ "husky": "^9.1.7",
42
45
  "jsdom": "^29.0.2"
43
46
  }
44
47
  }
@@ -147,6 +147,15 @@ export const expressionGenMethods = {
147
147
  return "Number.MIN_SAFE_INTEGER";
148
148
  }
149
149
  }
150
+ // io constants
151
+ if (expr.expr.kind === "Ident" && expr.expr.name === "io") {
152
+ switch (expr.field) {
153
+ case "EOF":
154
+ return '"EOF"';
155
+ case "Discard":
156
+ return "{ WriteString(s) { return s.length; }, Write(b) { return b.length; } }";
157
+ }
158
+ }
150
159
  // os constants/vars
151
160
  if (expr.expr.kind === "Ident" && expr.expr.name === "os") {
152
161
  switch (expr.field) {
@@ -184,7 +193,11 @@ export const expressionGenMethods = {
184
193
  }
185
194
 
186
195
  case "IndexExpr": {
187
- const base = this.genExpr(expr.expr);
196
+ const exprType = expr.expr._type;
197
+ const wrapField = this._namedWrapperField(exprType, expr.expr);
198
+ const base = wrapField
199
+ ? `${this.genExpr(expr.expr)}.${wrapField}`
200
+ : this.genExpr(expr.expr);
188
201
  const idx = this.genExpr(expr.index);
189
202
  if (expr._mapValueType && !expr._lvalue) {
190
203
  const zero = this.zeroValueForType(expr._mapValueType);
@@ -196,7 +209,6 @@ export const expressionGenMethods = {
196
209
  return `(${base}[${idx}] ?? ${zero})`;
197
210
  }
198
211
  // String indexing returns a byte (charCodeAt), not a JS character
199
- const exprType = expr.expr._type;
200
212
  const isStr =
201
213
  (exprType?.kind === "basic" && exprType.name === "string") ||
202
214
  (exprType?.kind === "untyped" && exprType.base === "string");
@@ -212,6 +224,7 @@ export const expressionGenMethods = {
212
224
  const hi = expr.high ? this.genExpr(expr.high) : "";
213
225
  if (!lo && !hi) return `${base}.slice()`;
214
226
  if (!hi) return `${base}.slice(${lo})`;
227
+ if (!lo) return `${base}.slice(0, ${hi})`;
215
228
  return `${base}.slice(${lo}, ${hi})`;
216
229
  }
217
230
 
@@ -284,6 +297,10 @@ export const expressionGenMethods = {
284
297
  this._usesError = true;
285
298
  return `__error(${inner})`;
286
299
  }
300
+ // Named wrapper type: NodeFunc(fn) → new NodeFunc(fn)
301
+ if (target && this.namedWrapperNames.has(target)) {
302
+ return `new ${target}(${inner})`;
303
+ }
287
304
  switch (target) {
288
305
  case "string": {
289
306
  // Go string(65) → "A" (Unicode code point), not "65"
@@ -339,6 +356,16 @@ export const expressionGenMethods = {
339
356
  },
340
357
 
341
358
  genCall(expr) {
359
+ // Named type constructor call: Greeter(fn), NodeFunc(fn) → new Greeter(fn)
360
+ if (expr._isTypeConversion) {
361
+ const name = expr._conversionTargetType?.name;
362
+ const arg = this.genExpr(expr.args[0]);
363
+ if (name && this.namedWrapperNames.has(name)) {
364
+ return `new ${name}(${arg})`;
365
+ }
366
+ return arg; // non-wrapper named types: identity
367
+ }
368
+
342
369
  // Unwrap InstantiationExpr for function calls: Foo[int](42) → Foo(42)
343
370
  const funcExpr =
344
371
  expr.func.kind === "InstantiationExpr" ? expr.func.expr : expr.func;
@@ -351,6 +378,10 @@ export const expressionGenMethods = {
351
378
  if (expr._constLen != null) return String(expr._constLen);
352
379
  const arg = expr.args[0];
353
380
  const t = arg?._type;
381
+ const wrapField = this._namedWrapperField(t, arg);
382
+ if (wrapField) {
383
+ return `${this.genExpr(arg)}.${wrapField}.length`;
384
+ }
354
385
  const js = this.genExpr(arg);
355
386
  if (t?.kind === "map") return `Object.keys(${js}).length`;
356
387
  this._usesLen = true;
@@ -472,6 +503,18 @@ export const expressionGenMethods = {
472
503
  },
473
504
 
474
505
  genAppend(expr) {
506
+ const retType = expr._type;
507
+ const wrapField = this._namedWrapperField(retType, expr.args[0]);
508
+ if (wrapField) {
509
+ // Named slice wrapper: append(g, x) → new G(__append(g._items, x))
510
+ const sliceJS = `${this.genExpr(expr.args[0])}.${wrapField}`;
511
+ const elems = expr.args
512
+ .slice(1)
513
+ .map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)));
514
+ if (elems.length === 0) return this.genExpr(expr.args[0]);
515
+ this._usesAppend = true;
516
+ return `new ${retType.name}(__append(${sliceJS}, ${elems.join(", ")}))`;
517
+ }
475
518
  const slice = this.genExpr(expr.args[0]);
476
519
  const elems = expr.args
477
520
  .slice(1)
@@ -556,6 +599,34 @@ export const expressionGenMethods = {
556
599
  return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
557
600
  }
558
601
 
602
+ // Named wrapper type: Group{a, b} → new Group([a, b])
603
+ if (typeName && this.namedWrapperNames.has(typeName)) {
604
+ const namedType = this.checker?.types.get(typeName);
605
+ const u = namedType?.underlying;
606
+ if (u?.kind === "map") {
607
+ const entries = expr.elems
608
+ .map((e) => {
609
+ if (e.kind === "KeyValueExpr") {
610
+ const k =
611
+ e.key.litKind === "STRING"
612
+ ? JSON.stringify(e.key.value)
613
+ : `[${this.genExpr(e.key)}]`;
614
+ return `${k}: ${this.genExpr(e.value)}`;
615
+ }
616
+ return this.genExpr(e);
617
+ })
618
+ .join(", ");
619
+ return `new ${typeName}({ ${entries} })`;
620
+ }
621
+ // Default: slice
622
+ const elems = expr.elems
623
+ .map((e) =>
624
+ e.kind === "KeyValueExpr" ? this.genExpr(e.value) : this.genExpr(e),
625
+ )
626
+ .join(", ");
627
+ return `new ${typeName}([${elems}])`;
628
+ }
629
+
559
630
  // Fallback: named type that's not a struct but has positional elements → treat as struct
560
631
  if (expr.elems.some((e) => e._positionalField)) {
561
632
  if (typeName) {
@@ -617,7 +688,8 @@ export const expressionGenMethods = {
617
688
  if (typeNode.kind === "Ident") return typeNode.name;
618
689
  if (typeNode.kind === "InstantiationExpr")
619
690
  return this.getTypeName(typeNode.expr);
620
- if (typeNode.kind === "SelectorExpr") return typeNode.field;
691
+ if (typeNode.kind === "SelectorExpr")
692
+ return `${this.getTypeName(typeNode.expr)}.${typeNode.field}`;
621
693
  return null;
622
694
  },
623
695
 
@@ -1002,6 +1074,32 @@ export const expressionGenMethods = {
1002
1074
  return undefined;
1003
1075
  },
1004
1076
 
1077
+ _genIo(fn, _a, expr) {
1078
+ switch (fn) {
1079
+ case "WriteString": {
1080
+ const writerArg = expr.args[0];
1081
+ const writerType = writerArg._type;
1082
+ const typeName =
1083
+ writerType?.name ??
1084
+ (writerType?.kind === "pointer" ? writerType.base?.name : null);
1085
+ const w = this.genExpr(writerArg);
1086
+ const s = this.genExpr(expr.args[1]);
1087
+ const isPtr = writerType?.kind === "pointer";
1088
+ const base = isPtr ? `${w}.value` : w;
1089
+ if (typeName === "strings.Builder") {
1090
+ return `((b,s) => { b._buf += s; return [s.length, null]; })(${base}, ${s})`;
1091
+ }
1092
+ if (typeName === "bytes.Buffer") {
1093
+ return `((b,s) => { b._buf.push(...new TextEncoder().encode(s)); return [s.length, null]; })(${base}, ${s})`;
1094
+ }
1095
+ // Generic: auto-dereference pointer, then dispatch on concrete writer type.
1096
+ // strings.Builder has { _buf: string }, bytes.Buffer has { _buf: [] }.
1097
+ return `((b,s) => { const w = b?.value ?? b; if (typeof w.WriteString === "function") return [w.WriteString(s), null]; if (typeof w._buf === "string") { w._buf += s; return [s.length, null]; } if (Array.isArray(w._buf)) { w._buf.push(...new TextEncoder().encode(s)); return [s.length, null]; } return [0, "io.WriteString: unsupported writer"]; })(${w}, ${s})`;
1098
+ }
1099
+ }
1100
+ return undefined;
1101
+ },
1102
+
1005
1103
  _genHtml(fn, a) {
1006
1104
  switch (fn) {
1007
1105
  case "EscapeString": {
@@ -1102,11 +1200,162 @@ export const expressionGenMethods = {
1102
1200
  return this._genMaps(fn, a);
1103
1201
  case "html":
1104
1202
  return this._genHtml(fn, a);
1203
+ case "io":
1204
+ return this._genIo(fn, a, expr);
1205
+ case "gom":
1206
+ return this._genGom(fn, expr);
1105
1207
  default:
1106
1208
  return undefined;
1107
1209
  }
1108
1210
  },
1109
1211
 
1212
+ _genGom(fn, expr) {
1213
+ const a = () =>
1214
+ expr.args.map((e) =>
1215
+ e._spread ? `...${this.genExpr(e)}` : this.genExpr(e),
1216
+ );
1217
+ const elementTags = {
1218
+ Div: "div",
1219
+ Section: "section",
1220
+ Article: "article",
1221
+ Aside: "aside",
1222
+ Header: "header",
1223
+ Footer: "footer",
1224
+ Main: "main",
1225
+ Nav: "nav",
1226
+ Figure: "figure",
1227
+ H1: "h1",
1228
+ H2: "h2",
1229
+ H3: "h3",
1230
+ H4: "h4",
1231
+ H5: "h5",
1232
+ H6: "h6",
1233
+ Span: "span",
1234
+ A: "a",
1235
+ Strong: "strong",
1236
+ Em: "em",
1237
+ Code: "code",
1238
+ Pre: "pre",
1239
+ Small: "small",
1240
+ Mark: "mark",
1241
+ P: "p",
1242
+ Br: "br",
1243
+ Hr: "hr",
1244
+ Ul: "ul",
1245
+ Ol: "ol",
1246
+ Li: "li",
1247
+ Dl: "dl",
1248
+ Dt: "dt",
1249
+ Dd: "dd",
1250
+ Form: "form",
1251
+ Input: "input",
1252
+ Button: "button",
1253
+ Textarea: "textarea",
1254
+ Select: "select",
1255
+ Option: "option",
1256
+ Label: "label",
1257
+ Fieldset: "fieldset",
1258
+ Legend: "legend",
1259
+ Img: "img",
1260
+ Video: "video",
1261
+ Audio: "audio",
1262
+ Canvas: "canvas",
1263
+ Table: "table",
1264
+ Thead: "thead",
1265
+ Tbody: "tbody",
1266
+ Tfoot: "tfoot",
1267
+ Tr: "tr",
1268
+ Th: "th",
1269
+ Td: "td",
1270
+ };
1271
+ if (elementTags[fn]) {
1272
+ const tag = elementTags[fn];
1273
+ const args = a();
1274
+ if (args.length === 0)
1275
+ return `(()=>({Mount(p){const e=document.createElement("${tag}");p.appendChild(e);}}))()`;
1276
+ return (
1277
+ `((...c)=>({Mount(p){const e=document.createElement("${tag}");c.forEach(n=>n?.Mount?.(e));p.appendChild(e);}}))` +
1278
+ `(${args.join(",")})`
1279
+ );
1280
+ }
1281
+ const attrHelpers = {
1282
+ For: "for",
1283
+ Name: "name",
1284
+ Value: "value",
1285
+ Target: "target",
1286
+ Rel: "rel",
1287
+ Alt: "alt",
1288
+ Title: "title",
1289
+ Lang: "lang",
1290
+ Action: "action",
1291
+ Method: "method",
1292
+ AutoComplete: "autocomplete",
1293
+ Draggable: "draggable",
1294
+ Role: "role",
1295
+ StyleAttr: "style",
1296
+ };
1297
+ if (attrHelpers[fn]) {
1298
+ const [v] = a();
1299
+ return `((v)=>({Mount(e){e.setAttribute("${attrHelpers[fn]}",v)}}))(${v})`;
1300
+ }
1301
+ if (fn === "AriaLabel") {
1302
+ const [v] = a();
1303
+ return `((v)=>({Mount(e){e.setAttribute("aria-label",v)}}))(${v})`;
1304
+ }
1305
+ const boolAttrs = {
1306
+ Disabled: "disabled",
1307
+ Checked: "checked",
1308
+ Selected: "selected",
1309
+ Readonly: "readonly",
1310
+ };
1311
+ if (boolAttrs[fn])
1312
+ return `({Mount(e){e.setAttribute("${boolAttrs[fn]}","")}})`;
1313
+
1314
+ const args = a();
1315
+ switch (fn) {
1316
+ case "El": {
1317
+ const [tag, ...children] = args;
1318
+ if (children.length === 0)
1319
+ return `((t)=>({Mount(p){const e=document.createElement(t);p.appendChild(e);}})) (${tag})`;
1320
+ return (
1321
+ `((t,...c)=>({Mount(p){const e=document.createElement(t);c.forEach(n=>n?.Mount?.(e));p.appendChild(e);}}))` +
1322
+ `(${tag},${children.join(",")})`
1323
+ );
1324
+ }
1325
+ case "Text": {
1326
+ const [s] = args;
1327
+ return `((s)=>({Mount(p){p.appendChild(document.createTextNode(s))}}))(${s})`;
1328
+ }
1329
+ case "Attr": {
1330
+ const [name, value] = args;
1331
+ return `((n,v)=>({Mount(e){e.setAttribute(n,v)}}))(${name},${value})`;
1332
+ }
1333
+ case "Class":
1334
+ return `((v)=>({Mount(e){e.className=v}}))(${args[0]})`;
1335
+ case "Type":
1336
+ return `((v)=>({Mount(e){e.type=v}}))(${args[0]})`;
1337
+ case "Href":
1338
+ return `((v)=>({Mount(e){e.href=v}}))(${args[0]})`;
1339
+ case "Src":
1340
+ return `((v)=>({Mount(e){e.src=v}}))(${args[0]})`;
1341
+ case "Placeholder":
1342
+ return `((v)=>({Mount(e){e.placeholder=v}}))(${args[0]})`;
1343
+ case "DataAttr":
1344
+ return `((k,v)=>({Mount(e){e.setAttribute("data-"+k,v)}}))(${args[0]},${args[1]})`;
1345
+ case "If":
1346
+ return `((c,n)=>c?n:{Mount(){}})(${args[0]},${args[1]})`;
1347
+ case "Map":
1348
+ return `((s,f)=>{const c=s.map(f);return{_items:c,Mount(p){c.forEach(n=>n.Mount(p))}}})(${args[0]},${args[1]})`;
1349
+ case "Style":
1350
+ return `((s)=>({Mount(p){const e=document.createElement("style");e.textContent=s;p.appendChild(e);}})) (${args[0]})`;
1351
+ case "Mount":
1352
+ return `((sel,n)=>{const e=document.querySelector(sel);e.innerHTML="";n.Mount(e)})(${args[0]},${args[1]})`;
1353
+ case "MountTo":
1354
+ return `((sel,n)=>{const e=document.querySelector(sel);n.Mount(e)})(${args[0]},${args[1]})`;
1355
+ }
1356
+ return undefined;
1357
+ },
1358
+
1110
1359
  _genFmt(fn, _a, expr) {
1111
1360
  const fmtArgs = expr.args.map((e) => this.genExpr(e)).join(", ");
1112
1361
  switch (fn) {
@@ -7,6 +7,30 @@ export const statementGenMethods = {
7
7
  for (const stmt of block.stmts) this.genStmt(stmt);
8
8
  },
9
9
 
10
+ // Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
11
+ // isDefine=true → emit `let v = m[k]; let ok = k in m;`
12
+ // isDefine=false → emit `v = k in m ? m[k] : zero; ok = k in m;`
13
+ _genCommaOkMapIndex(stmt, rhsNode, isDefine) {
14
+ const mapExpr = this.genExpr(rhsNode.expr);
15
+ const keyExpr = this.genExpr(rhsNode.index);
16
+ const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
17
+ const decl = isDefine ? "let " : "";
18
+ if (vName !== "_") {
19
+ if (isDefine) {
20
+ this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
21
+ } else {
22
+ const zero = rhsNode._mapValueType
23
+ ? this.zeroValueForType(rhsNode._mapValueType)
24
+ : "undefined";
25
+ this.line(
26
+ `${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
27
+ );
28
+ }
29
+ }
30
+ if (okName !== "_")
31
+ this.line(`${decl}${okName} = (${keyExpr}) in ${mapExpr};`);
32
+ },
33
+
10
34
  genStmt(stmt) {
11
35
  // Record source mapping for the first line this statement produces
12
36
  const srcLine = stmt._line ?? null;
@@ -31,15 +55,7 @@ export const statementGenMethods = {
31
55
  rhsNode?.kind === "IndexExpr" &&
32
56
  rhsNode.expr?._type?.kind === "map"
33
57
  ) {
34
- const mapExpr = this.genExpr(rhsNode.expr);
35
- const keyExpr = this.genExpr(rhsNode.index);
36
- const [vName, okName] = stmt.lhs.map(
37
- (e) => e.name ?? this.genExpr(e),
38
- );
39
- if (vName !== "_")
40
- this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
41
- if (okName !== "_")
42
- this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
58
+ this._genCommaOkMapIndex(stmt, rhsNode, true);
43
59
  break;
44
60
  }
45
61
  const rhs = stmt.rhs.map((e) => this.genExpr(e));
@@ -117,22 +133,7 @@ export const statementGenMethods = {
117
133
  }
118
134
  // comma-ok map index: v, ok = m["key"]
119
135
  if (stmt._commaOkMap) {
120
- const rhsNode = stmt.rhs[0];
121
- const mapExpr = this.genExpr(rhsNode.expr);
122
- const keyExpr = this.genExpr(rhsNode.index);
123
- const [vName, okName] = stmt.lhs.map(
124
- (e) => e.name ?? this.genExpr(e),
125
- );
126
- if (vName !== "_") {
127
- const zero = rhsNode._mapValueType
128
- ? this.zeroValueForType(rhsNode._mapValueType)
129
- : "undefined";
130
- this.line(
131
- `${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
132
- );
133
- }
134
- if (okName !== "_")
135
- this.line(`${okName} = (${keyExpr}) in ${mapExpr};`);
136
+ this._genCommaOkMapIndex(stmt, stmt.rhs[0], false);
136
137
  break;
137
138
  }
138
139
  // comma-ok type assertion: v, ok = x.(T)
@@ -383,8 +384,11 @@ export const statementGenMethods = {
383
384
  const init = stmt.init;
384
385
  const range = init.rhs[0];
385
386
  const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
386
- const iteree = this.genExpr(range.expr);
387
387
  const iterType = range.expr._type;
388
+ const wrapField = this._namedWrapperField(iterType, range.expr);
389
+ const iteree = wrapField
390
+ ? `${this.genExpr(range.expr)}.${wrapField}`
391
+ : this.genExpr(range.expr);
388
392
 
389
393
  let iterExpr;
390
394
  if (