@barefootjs/go-template 0.18.4 → 0.18.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.
Files changed (37) hide show
  1. package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
  2. package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
  3. package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
  4. package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
  5. package/dist/adapter/go-template-adapter.d.ts +151 -3
  6. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  7. package/dist/adapter/index.js +500 -52
  8. package/dist/adapter/lib/compile-state.d.ts +15 -0
  9. package/dist/adapter/lib/compile-state.d.ts.map +1 -1
  10. package/dist/adapter/lib/constants.d.ts.map +1 -1
  11. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  12. package/dist/adapter/props/prop-classes.d.ts +40 -0
  13. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  14. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  15. package/dist/adapter/type/type-codegen.d.ts +5 -1
  16. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  18. package/dist/build.js +500 -52
  19. package/dist/conformance-pins.d.ts.map +1 -1
  20. package/dist/index.js +503 -79
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/test-render.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/__tests__/go-template-adapter.test.ts +708 -4
  25. package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
  26. package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
  27. package/src/adapter/go-template-adapter.ts +661 -34
  28. package/src/adapter/lib/compile-state.ts +17 -0
  29. package/src/adapter/lib/constants.ts +1 -0
  30. package/src/adapter/memo/memo-compute.ts +37 -9
  31. package/src/adapter/props/prop-classes.ts +70 -0
  32. package/src/adapter/props/prop-types.ts +69 -1
  33. package/src/adapter/type/type-codegen.ts +19 -2
  34. package/src/adapter/value/value-lowering.ts +27 -2
  35. package/src/conformance-pins.ts +30 -36
  36. package/src/render-divergences.ts +12 -30
  37. package/src/test-render.ts +131 -13
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
25
25
  import {
26
26
  BaseAdapter,
27
27
  isBooleanAttr,
28
- parseExpression as parseExpression3,
28
+ parseExpression as parseExpression4,
29
29
  stringifyParsedExpr as stringifyParsedExpr2,
30
30
  parseStyleObjectEntries,
31
31
  isSupported,
@@ -42,10 +42,17 @@ import {
42
42
  collectModuleStringConsts as collectModuleStringConstsShared,
43
43
  prepareLoweringMatchers,
44
44
  envSignalReaderFor,
45
- computeSsrSeedPlan
45
+ computeSsrSeedPlan,
46
+ isStringConcatBinary,
47
+ isDangerousInnerHtmlAttr,
48
+ resolveDangerousInnerHtml,
49
+ dangerousInnerHtmlMetacharViolation,
50
+ dangerousInnerHtmlDiagnostic,
51
+ collectLoopBoundNames as collectLoopBoundNames2,
52
+ evaluateStaticLiteral as evaluateStaticLiteral3
46
53
  } from "@barefootjs/jsx";
47
54
  import { findInterpolationEnd } from "@barefootjs/jsx/scanner";
48
- import { BF_REGION } from "@barefootjs/shared";
55
+ import { BF_REGION, escapeHtml } from "@barefootjs/shared";
49
56
 
50
57
  // src/adapter/lib/go-naming.ts
51
58
  var GO_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -332,6 +339,7 @@ var GO_TEMPLATE_PRIMITIVES = {
332
339
  "Math.floor": { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
333
340
  "Math.ceil": { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
334
341
  "Math.round": { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
342
+ "Math.abs": { arity: 1, emit: (args) => `bf_abs ${wrapGoArg(args[0])}` },
335
343
  "Math.min": { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
336
344
  "Math.max": { arity: 2, emit: (args) => `bf_max ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` }
337
345
  };
@@ -347,6 +355,7 @@ class CompileState {
347
355
  restPropsName = null;
348
356
  moduleStringConsts = new Map;
349
357
  localConstants = [];
358
+ staticLoopSourceBoundNames = new Set;
350
359
  localHelperNames = new Set;
351
360
  currentMemos = [];
352
361
  currentTypeDefinitions = [];
@@ -357,6 +366,7 @@ class CompileState {
357
366
  hoistedMemoLocals = new Map;
358
367
  loweringMatchers = [];
359
368
  nillablePropNames = new Set;
369
+ stringValueNames = new Set;
360
370
  rootScopeNodes = new Set;
361
371
  memoBackedLoopSlice = new Map;
362
372
  usesHtmlTemplate = false;
@@ -514,6 +524,178 @@ function collectNestedComponents(node, result) {
514
524
  }
515
525
  }
516
526
 
527
+ // src/adapter/analysis/static-child-loop-bake.ts
528
+ import { evaluateStaticLiteral, parseExpression, resolveStaticLoopSource } from "@barefootjs/jsx";
529
+ function scalarToGoLiteral(value) {
530
+ if (typeof value === "string")
531
+ return `"${escapeGoString(value)}"`;
532
+ if (typeof value === "number")
533
+ return String(value);
534
+ if (typeof value === "boolean")
535
+ return value ? "true" : "false";
536
+ return null;
537
+ }
538
+ function analyzeBakeableStaticChildLoop(nested, localConstants, opts) {
539
+ if (!nested.loopParam || /^[{[]/.test(nested.loopParam))
540
+ return null;
541
+ const staticItemsResult = resolveStaticLoopSource(nested.loopArrayParsed, localConstants, opts);
542
+ if (staticItemsResult === null)
543
+ return null;
544
+ const items = [];
545
+ for (const item of staticItemsResult) {
546
+ const bindings = new Map([[nested.loopParam, item]]);
547
+ const inputFields = [];
548
+ for (const prop of nested.props) {
549
+ if (prop.isEventHandler)
550
+ continue;
551
+ if (prop.name.includes("-"))
552
+ continue;
553
+ const resolved = resolvePropValue(prop.value, bindings);
554
+ if (resolved === undefined)
555
+ return null;
556
+ const goValue = scalarToGoLiteral(resolved);
557
+ if (goValue === null)
558
+ return null;
559
+ inputFields.push({ goField: capitalizeFieldName(prop.name), goValue });
560
+ }
561
+ let dataKey = null;
562
+ if (nested.loopKey) {
563
+ const keyExpr = parseExpression(nested.loopKey);
564
+ const keyResolved = evaluateStaticLiteral(keyExpr, bindings);
565
+ if (keyResolved === null)
566
+ return null;
567
+ dataKey = String(keyResolved.value);
568
+ }
569
+ items.push({ inputFields, dataKey });
570
+ }
571
+ return { items };
572
+ }
573
+ function resolvePropValue(value, bindings) {
574
+ switch (value.kind) {
575
+ case "literal":
576
+ return value.value;
577
+ case "boolean-shorthand":
578
+ case "boolean-attr":
579
+ return true;
580
+ case "expression": {
581
+ if (!value.parsed)
582
+ return;
583
+ const resolved = evaluateStaticLiteral(value.parsed, bindings);
584
+ return resolved === null ? undefined : resolved.value;
585
+ }
586
+ default:
587
+ return;
588
+ }
589
+ }
590
+
591
+ // src/adapter/analysis/static-element-loop-bake.ts
592
+ import {
593
+ evaluateStaticLiteral as evaluateStaticLiteral2,
594
+ resolveStaticLoopSource as resolveStaticLoopSource2
595
+ } from "@barefootjs/jsx";
596
+ var ALLOWED_ATTR_EXPRESSION_KINDS = new Set([
597
+ "identifier",
598
+ "member",
599
+ "index-access",
600
+ "literal"
601
+ ]);
602
+ function analyzeBakeableStaticElementLoop(loop, localConstants, opts) {
603
+ if (loop.childComponent)
604
+ return null;
605
+ if (loop.method === "flatMap" || loop.flatMapCallback)
606
+ return null;
607
+ if (!loop.param || /^[{[]/.test(loop.param))
608
+ return null;
609
+ if (loop.index && loop.index !== "_")
610
+ return null;
611
+ if (loop.paramBindings && loop.paramBindings.length > 0)
612
+ return null;
613
+ if (loop.filterPredicate || loop.sortComparator)
614
+ return null;
615
+ if (loop.iterationShape || loop.objectIteration)
616
+ return null;
617
+ if (loop.bodyIsMultiRoot || loop.bodyIsItemConditional)
618
+ return null;
619
+ if (!isFoldableTree(loop.children))
620
+ return null;
621
+ const items = resolveStaticLoopSource2(loop.arrayParsed, localConstants, opts);
622
+ if (items === null)
623
+ return null;
624
+ for (const item of items) {
625
+ const bindings = new Map([[loop.param, item]]);
626
+ if (!allExpressionsFoldFor(loop.children, bindings))
627
+ return null;
628
+ }
629
+ return { items };
630
+ }
631
+ function isFoldableTree(nodes) {
632
+ for (const node of nodes) {
633
+ switch (node.type) {
634
+ case "text":
635
+ case "expression":
636
+ continue;
637
+ case "element":
638
+ if (!isFoldableAttrs(node))
639
+ return false;
640
+ if (!isFoldableTree(node.children))
641
+ return false;
642
+ continue;
643
+ default:
644
+ return false;
645
+ }
646
+ }
647
+ return true;
648
+ }
649
+ function isFoldableAttrs(element) {
650
+ for (const attr of element.attrs) {
651
+ if (attr.clientOnly)
652
+ continue;
653
+ switch (attr.value.kind) {
654
+ case "literal":
655
+ case "boolean-attr":
656
+ case "boolean-shorthand":
657
+ continue;
658
+ case "expression":
659
+ if (!attr.value.parsed || !ALLOWED_ATTR_EXPRESSION_KINDS.has(attr.value.parsed.kind))
660
+ return false;
661
+ continue;
662
+ default:
663
+ return false;
664
+ }
665
+ }
666
+ return true;
667
+ }
668
+ function allExpressionsFoldFor(nodes, bindings) {
669
+ for (const node of nodes) {
670
+ if (node.type === "expression") {
671
+ if (node.clientOnly)
672
+ continue;
673
+ if (!node.parsed || !resolvesToScalar(node.parsed, bindings))
674
+ return false;
675
+ continue;
676
+ }
677
+ if (node.type === "element") {
678
+ for (const attr of node.attrs) {
679
+ if (attr.clientOnly)
680
+ continue;
681
+ if (attr.value.kind !== "expression")
682
+ continue;
683
+ if (!attr.value.parsed || !resolvesToScalar(attr.value.parsed, bindings))
684
+ return false;
685
+ }
686
+ if (!allExpressionsFoldFor(node.children, bindings))
687
+ return false;
688
+ }
689
+ }
690
+ return true;
691
+ }
692
+ function resolvesToScalar(expr, bindings) {
693
+ const resolved = evaluateStaticLiteral2(expr, bindings);
694
+ if (resolved === null)
695
+ return false;
696
+ return scalarToGoLiteral(resolved.value) !== null;
697
+ }
698
+
517
699
  // src/adapter/expr/helper-inline.ts
518
700
  function inlineLocalHelperCall(ctx, jsExpr, callParsed) {
519
701
  if (ctx.state.localHelperNames.size === 0)
@@ -701,7 +883,7 @@ function forEachValueChild(n, visit) {
701
883
 
702
884
  // src/adapter/expr/url-builder.ts
703
885
  import {
704
- parseExpression,
886
+ parseExpression as parseExpression2,
705
887
  stringifyParsedExpr,
706
888
  isValidHelperId
707
889
  } from "@barefootjs/jsx";
@@ -734,7 +916,7 @@ function lowerRegisteredCall(ctx, jsExpr, preParsed) {
734
916
  if (!call) {
735
917
  if (!/^\s*[A-Za-z_$][\w$]*\s*\(/.test(jsExpr))
736
918
  return null;
737
- const parsed = parseExpression(jsExpr);
919
+ const parsed = parseExpression2(jsExpr);
738
920
  if (parsed.kind !== "call")
739
921
  return null;
740
922
  call = parsed;
@@ -776,7 +958,7 @@ function typeInfoToGo(ctx, typeInfo, defaultValue) {
776
958
  case "string":
777
959
  return "string";
778
960
  case "number":
779
- return "int";
961
+ return defaultValue !== undefined ? numberPrimitiveGoType(defaultValue) : "int";
780
962
  case "boolean":
781
963
  return "bool";
782
964
  default:
@@ -827,6 +1009,9 @@ function tsTypeStringToGo(ctx, tsType) {
827
1009
  return t;
828
1010
  return "interface{}";
829
1011
  }
1012
+ function numberPrimitiveGoType(value) {
1013
+ return /^-?\d+\.\d+$/.test(value) ? "float64" : "int";
1014
+ }
830
1015
  function inferTypeFromValue(value) {
831
1016
  if (value === "true" || value === "false")
832
1017
  return "bool";
@@ -939,9 +1124,9 @@ function convertInitialValue(ctx, value, typeInfo, propsParams, preParsed) {
939
1124
  return value === "true" ? "true" : "false";
940
1125
  }
941
1126
  if (typeInfo.primitive === "number") {
942
- if (/^\d+$/.test(value))
1127
+ if (/^-?\d+$/.test(value))
943
1128
  return value;
944
- if (/^\d+\.\d+$/.test(value))
1129
+ if (/^-?\d+\.\d+$/.test(value))
945
1130
  return value;
946
1131
  return "0";
947
1132
  }
@@ -966,6 +1151,12 @@ function convertInitialValue(ctx, value, typeInfo, propsParams, preParsed) {
966
1151
  }
967
1152
  return '""';
968
1153
  }
1154
+ if (ctx.state.localStructFields.has(typeInfo.raw)) {
1155
+ const baked = jsLiteralToGo(ctx, typeInfo, preParsed);
1156
+ if (baked !== null)
1157
+ return baked;
1158
+ return `${typeInfo.raw}{}`;
1159
+ }
969
1160
  }
970
1161
  return "nil";
971
1162
  }
@@ -1656,10 +1847,11 @@ function memoInitialFromParsedBody(ctx, body, signals, propsParams, propFallback
1656
1847
  const operand = String(body.right.value);
1657
1848
  const depName = getterCallName(body.left);
1658
1849
  if (depName) {
1659
- const signal = signals.find((s) => s.getter === depName);
1660
- if (signal) {
1661
- const signalInitial = getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars);
1662
- return `${signalInitial} ${operator} ${operand}`;
1850
+ const depInitial = resolveGetterValueAsGo(ctx, depName, signals, propsParams, propFallbackVars, resolving);
1851
+ const isArithmeticSafe = depInitial !== null && !depInitial.startsWith("func(");
1852
+ if (isArithmeticSafe) {
1853
+ const wrapped = /\s/.test(depInitial) ? `(${depInitial})` : depInitial;
1854
+ return `${wrapped} ${operator} ${operand}`;
1663
1855
  }
1664
1856
  }
1665
1857
  const propName = propsMemberName(body.left);
@@ -1694,9 +1886,9 @@ function memoInitialFromParsedBody(ctx, body, signals, propsParams, propFallback
1694
1886
  }
1695
1887
  const simpleDep = getterCallName(body);
1696
1888
  if (simpleDep) {
1697
- const signal = signals.find((s) => s.getter === simpleDep);
1698
- if (signal) {
1699
- return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars);
1889
+ const depInitial = resolveGetterValueAsGo(ctx, simpleDep, signals, propsParams, propFallbackVars, resolving);
1890
+ if (depInitial !== null) {
1891
+ return depInitial;
1700
1892
  }
1701
1893
  }
1702
1894
  const simpleProp = propsMemberName(body);
@@ -1818,7 +2010,7 @@ function propsAccessNameFromParsed2(ctx, node) {
1818
2010
 
1819
2011
  // src/adapter/spread/spread-codegen.ts
1820
2012
  import ts2 from "typescript";
1821
- import { parseExpression as parseExpression2, parseRecordIndexAccess } from "@barefootjs/jsx";
2013
+ import { parseExpression as parseExpression3, parseRecordIndexAccess } from "@barefootjs/jsx";
1822
2014
  function collectSpreadSlots(ctx, node) {
1823
2015
  const result = [];
1824
2016
  collectSpreadSlotsRecursive(ctx, node, result);
@@ -1925,7 +2117,7 @@ function parsedObjectLiteralToGoMap(parsed) {
1925
2117
  }
1926
2118
  function buildSpreadInitializer(ctx, spreadExpr, ir, parsed) {
1927
2119
  const trimmed = spreadExpr.trim();
1928
- const conditionalTree = parsed ?? parseExpression2(trimmed);
2120
+ const conditionalTree = parsed ?? parseExpression3(trimmed);
1929
2121
  const conditional = buildConditionalSpreadInitializer(ctx, conditionalTree, ir);
1930
2122
  if (conditional !== undefined)
1931
2123
  return conditional;
@@ -1956,7 +2148,7 @@ function buildSpreadInitializer(ctx, spreadExpr, ir, parsed) {
1956
2148
  if (localConst?.value !== undefined) {
1957
2149
  const initTrimmed = localConst.value.trim();
1958
2150
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
1959
- const resolved = buildConditionalSpreadInitializer(ctx, parseExpression2(initTrimmed), ir);
2151
+ const resolved = buildConditionalSpreadInitializer(ctx, parseExpression3(initTrimmed), ir);
1960
2152
  if (resolved)
1961
2153
  return resolved;
1962
2154
  if (resolved === null)
@@ -2087,8 +2279,47 @@ function buildPropTypeOverrides(ctx, ir) {
2087
2279
  }
2088
2280
  }
2089
2281
  }
2282
+ for (const propName of collectToFixedPropNames(ir.root)) {
2283
+ const param = ir.metadata.propsParams.find((p) => p.name === propName);
2284
+ if (!param)
2285
+ continue;
2286
+ const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue);
2287
+ if (resolved === "int") {
2288
+ overrides.set(propName, "float64");
2289
+ }
2290
+ }
2090
2291
  return overrides;
2091
2292
  }
2293
+ function collectToFixedPropNames(root) {
2294
+ const names = new Set;
2295
+ const checkExpr = (expr) => {
2296
+ if (expr?.kind === "array-method" && expr.method === "toFixed" && expr.object.kind === "identifier") {
2297
+ names.add(expr.object.name);
2298
+ }
2299
+ };
2300
+ const walk = (node) => {
2301
+ if (!node)
2302
+ return;
2303
+ if (node.type === "expression")
2304
+ checkExpr(node.parsed);
2305
+ if (node.type === "conditional") {
2306
+ checkExpr(node.parsedCondition);
2307
+ walk(node.whenTrue);
2308
+ walk(node.whenFalse);
2309
+ }
2310
+ if (node.type === "element") {
2311
+ for (const attr of node.attrs) {
2312
+ if (attr.value.kind === "expression")
2313
+ checkExpr(attr.value.parsed);
2314
+ }
2315
+ }
2316
+ if ("children" in node && Array.isArray(node.children)) {
2317
+ node.children.forEach(walk);
2318
+ }
2319
+ };
2320
+ walk(root);
2321
+ return names;
2322
+ }
2092
2323
  function resolvePropGoType(ctx, param, propTypeOverrides) {
2093
2324
  const base = propTypeOverrides.get(param.name) ?? typeInfoToGo(ctx, param.type, param.defaultValue);
2094
2325
  if (param.optional && ctx.state.localStructFields.has(base)) {
@@ -2107,6 +2338,38 @@ function collectNillablePropNames(ctx, ir) {
2107
2338
  return nillable;
2108
2339
  }
2109
2340
 
2341
+ // src/adapter/props/prop-classes.ts
2342
+ import { collectLoopBoundNames } from "@barefootjs/jsx";
2343
+ function isStringTypeInfo(type) {
2344
+ return type.kind === "primitive" && type.primitive === "string";
2345
+ }
2346
+ function isBareStringLiteral(initialValue) {
2347
+ if (!initialValue)
2348
+ return false;
2349
+ const v = initialValue.trim();
2350
+ return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
2351
+ }
2352
+ function collectStringValueNames(ir) {
2353
+ const names = new Set;
2354
+ for (const s of ir.metadata.signals) {
2355
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
2356
+ names.add(s.getter);
2357
+ }
2358
+ }
2359
+ for (const p of ir.metadata.propsParams) {
2360
+ if (isStringTypeInfo(p.type))
2361
+ names.add(p.name);
2362
+ }
2363
+ for (const c of ir.metadata.localConstants) {
2364
+ if (c.type !== null && isStringTypeInfo(c.type) || isBareStringLiteral(c.value)) {
2365
+ names.add(c.name);
2366
+ }
2367
+ }
2368
+ for (const bound of collectLoopBoundNames(ir))
2369
+ names.delete(bound);
2370
+ return names;
2371
+ }
2372
+
2110
2373
  // src/adapter/go-template-adapter.ts
2111
2374
  var STRING_METHODS = new Set([
2112
2375
  "replace",
@@ -2148,12 +2411,16 @@ class GoTemplateAdapter extends BaseAdapter {
2148
2411
  return this.state.errors;
2149
2412
  }
2150
2413
  inLoop = false;
2414
+ bakedStaticChildLoopCache = new Map;
2151
2415
  loopParamStack = [];
2416
+ loopKeyDepthStack = [];
2152
2417
  loopScalarItemStack = [];
2153
2418
  loopWrapperStack = [];
2154
2419
  loopVarRefCount = new Map;
2155
2420
  loopBindingStack = [];
2156
2421
  loopRestExcludeStack = [];
2422
+ staticLoopItemStack = [];
2423
+ staticLoopBakeFailed = false;
2157
2424
  childComponentShapes = new Map;
2158
2425
  childContextConsumers = new Map;
2159
2426
  constructor(options = {}) {
@@ -2169,6 +2436,8 @@ class GoTemplateAdapter extends BaseAdapter {
2169
2436
  this.state.restPropsName = ir.metadata.restPropsName ?? null;
2170
2437
  this.state.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
2171
2438
  this.state.localConstants = ir.metadata.localConstants ?? [];
2439
+ this.state.staticLoopSourceBoundNames = collectLoopBoundNames2(ir);
2440
+ this.bakedStaticChildLoopCache = new Map;
2172
2441
  this.state.localHelperNames = new Set(this.state.localConstants.filter((c) => !c.isModule && c.containsArrow).map((c) => c.name));
2173
2442
  this.state.currentMemos = ir.metadata.memos ?? [];
2174
2443
  this.state.currentTypeDefinitions = ir.metadata.typeDefinitions ?? [];
@@ -2196,6 +2465,7 @@ class GoTemplateAdapter extends BaseAdapter {
2196
2465
  this.state.pendingChildrenDefines = [];
2197
2466
  this.primeCompileState(ir);
2198
2467
  this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir);
2468
+ this.state.stringValueNames = collectStringValueNames(ir);
2199
2469
  if (!options?.siblingTemplatesRegistered) {
2200
2470
  this.checkImportedLoopChildComponents(ir);
2201
2471
  }
@@ -2549,6 +2819,8 @@ ${goFields.join(`
2549
2819
  lines.push(` ${fieldName} ${goType}`);
2550
2820
  }
2551
2821
  for (const nested of inputNested) {
2822
+ if (nested.loopMarkerId && this.getBakedStaticChildLoop(nested.loopMarkerId, nested, nested.loopArrayParsed, nested.loopParam, nested.loopKey))
2823
+ continue;
2552
2824
  lines.push(` ${nested.name}s []${nested.name}Input`);
2553
2825
  }
2554
2826
  const takenInput = new Set(ir.metadata.propsParams.map((p) => capitalizeFieldName(p.name)));
@@ -2680,6 +2952,21 @@ ${goFields.join(`
2680
2952
  const staticWithoutBody = staticNested.filter((n) => !n.bodyChildren || n.bodyChildren.length === 0);
2681
2953
  for (const nested of staticWithoutBody) {
2682
2954
  const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
2955
+ const baked = nested.loopMarkerId ? this.getBakedStaticChildLoop(nested.loopMarkerId, nested, nested.loopArrayParsed, nested.loopParam, nested.loopKey) : null;
2956
+ if (baked) {
2957
+ lines.push(` ${varName} := make([]${nested.name}Props, ${baked.items.length})`);
2958
+ baked.items.forEach((item, i) => {
2959
+ const fields = item.inputFields.map((f) => `${f.goField}: ${f.goValue}`).join(", ");
2960
+ lines.push(` ${varName}[${i}] = New${nested.name}Props(${nested.name}Input{${fields}})`);
2961
+ lines.push(` ${varName}[${i}].BfParent = scopeID`);
2962
+ lines.push(` ${varName}[${i}].BfMount = "${nested.slotId}"`);
2963
+ if (item.dataKey !== null) {
2964
+ lines.push(` ${varName}[${i}].BfDataKey = ${JSON.stringify(item.dataKey)}`);
2965
+ }
2966
+ });
2967
+ lines.push("");
2968
+ continue;
2969
+ }
2683
2970
  lines.push(` ${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`);
2684
2971
  lines.push(` for i, item := range in.${nested.name}s {`);
2685
2972
  lines.push(` ${varName}[i] = New${nested.name}Props(item)`);
@@ -2893,6 +3180,13 @@ ${goFields.join(`
2893
3180
  break;
2894
3181
  }
2895
3182
  }
3183
+ if (parsedValue) {
3184
+ const goVal = parsedLiteralToGo(this.emitCtx, parsedValue);
3185
+ if (goVal !== null) {
3186
+ emitChildField(prop.name, goVal);
3187
+ break;
3188
+ }
3189
+ }
2896
3190
  const resolvedValue = this.resolveDynamicPropValue(exprText, ir.metadata.signals, ir.metadata.memos, ir.metadata.propsParams);
2897
3191
  if (resolvedValue !== null) {
2898
3192
  emitChildField(prop.name, resolvedValue);
@@ -2900,6 +3194,24 @@ ${goFields.join(`
2900
3194
  break;
2901
3195
  }
2902
3196
  case "jsx-children":
3197
+ if (prop.name !== "children") {
3198
+ const text = this.extractTextChildren(prop.value.children);
3199
+ if (text !== null) {
3200
+ emitChildField(prop.name, JSON.stringify(text));
3201
+ } else {
3202
+ const html = this.extractHtmlChildren(prop.value.children);
3203
+ if (html !== null) {
3204
+ this.state.usesHtmlTemplate = true;
3205
+ emitChildField(prop.name, `template.HTML(${JSON.stringify(html)})`);
3206
+ } else {
3207
+ const scopedHtml = this.extractScopedHtmlChildren(prop.value.children);
3208
+ if (scopedHtml !== null) {
3209
+ this.state.usesHtmlTemplate = true;
3210
+ emitChildField(prop.name, `template.HTML(${scopedHtml})`);
3211
+ }
3212
+ }
3213
+ }
3214
+ }
2903
3215
  break;
2904
3216
  }
2905
3217
  }
@@ -3541,6 +3853,17 @@ ${goFields.join(`
3541
3853
  return computeMemoInitialValueOrNull(this.emitCtx, memo, signals, propsParams, undefined, new Set([memo.name]));
3542
3854
  }
3543
3855
  }
3856
+ const propsObjectName = this.state.propsObjectName;
3857
+ const identifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
3858
+ const bareIdentifier = identifierPattern.test(expr) ? expr : null;
3859
+ const barePropAccess = propsObjectName && expr.startsWith(`${propsObjectName}.`) && identifierPattern.test(expr.slice(propsObjectName.length + 1)) ? expr.slice(propsObjectName.length + 1) : null;
3860
+ const passthroughName = bareIdentifier ?? barePropAccess;
3861
+ const localConst = this.state.localConstants.find((c) => c.name === passthroughName);
3862
+ const isPropsDestructureAlias = localConst !== undefined && propsObjectName !== null && localConst.value === `${propsObjectName}.${passthroughName}`;
3863
+ const shadowedByLocal = passthroughName !== null && (localConst !== undefined && !isPropsDestructureAlias || this.state.localHelperNames.has(passthroughName));
3864
+ if (passthroughName && !shadowedByLocal && propsParams.some((p) => p.name === passthroughName)) {
3865
+ return `in.${capitalizeFieldName(passthroughName)}`;
3866
+ }
3544
3867
  return null;
3545
3868
  }
3546
3869
  inferMemoType(memo, signals, propsParamMap) {
@@ -3665,7 +3988,7 @@ ${goFields.join(`
3665
3988
  return this.renderElement(node);
3666
3989
  }
3667
3990
  emitText(node) {
3668
- return node.value;
3991
+ return escapeHtml(node.value);
3669
3992
  }
3670
3993
  emitExpression(node) {
3671
3994
  return this.renderExpression(node);
@@ -3697,7 +4020,8 @@ ${goFields.join(`
3697
4020
  renderElement(element) {
3698
4021
  const tag = element.tag;
3699
4022
  const attrs = this.renderAttributes(element);
3700
- const children = this.renderChildren(element.children);
4023
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
4024
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
3701
4025
  let hydrationAttrs = "";
3702
4026
  if (element.needsScope) {
3703
4027
  hydrationAttrs += ` ${this.renderScopeMarker(".ScopeID")}`;
@@ -3732,6 +4056,22 @@ ${goFields.join(`
3732
4056
  }
3733
4057
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
3734
4058
  }
4059
+ renderDangerousInnerHtml(element) {
4060
+ const resolution = resolveDangerousInnerHtml(element);
4061
+ if (!resolution)
4062
+ return null;
4063
+ if (resolution.kind === "dynamic") {
4064
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
4065
+ return "";
4066
+ }
4067
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
4068
+ if (violation) {
4069
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
4070
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
4071
+ return "";
4072
+ }
4073
+ return resolution.html;
4074
+ }
3735
4075
  renderExpression(expr) {
3736
4076
  if (expr.clientOnly) {
3737
4077
  if (expr.slotId) {
@@ -3925,7 +4265,7 @@ ${goFields.join(`
3925
4265
  const argsStr = args.map(emit).join(" ");
3926
4266
  return `${calleeStr} ${argsStr}`;
3927
4267
  }
3928
- member(object, property, _computed, emit) {
4268
+ member(object, property, _computed, optional, emit) {
3929
4269
  const objHO = this.higherOrderShapeOf(object);
3930
4270
  if (property === "length" && objHO) {
3931
4271
  const result = this.renderFilterLengthExpr(objHO, emit);
@@ -3967,6 +4307,9 @@ ${goFields.join(`
3967
4307
  const obj = emit(object);
3968
4308
  if (property === "length")
3969
4309
  return `len ${obj}`;
4310
+ if (optional) {
4311
+ return `bf_get ${wrapIfMultiToken(obj)} ${JSON.stringify(goFieldNameForKey(property))}`;
4312
+ }
3970
4313
  return `${obj}.${goFieldNameForKey(property)}`;
3971
4314
  }
3972
4315
  indexAccess(object, index, emit) {
@@ -3997,7 +4340,7 @@ ${goFields.join(`
3997
4340
  case "<=":
3998
4341
  return `le ${wl} ${wr}`;
3999
4342
  case "+":
4000
- return `bf_add ${wl} ${wr}`;
4343
+ return this._emitPlus(left, right, wl, wr);
4001
4344
  case "-":
4002
4345
  return `bf_sub ${wl} ${wr}`;
4003
4346
  case "*":
@@ -4010,6 +4353,15 @@ ${goFields.join(`
4010
4353
  return `${l} ${op} ${r}`;
4011
4354
  }
4012
4355
  }
4356
+ _emitPlus(leftExpr, rightExpr, leftRendered, rightRendered) {
4357
+ if (isStringConcatBinary("+", leftExpr, rightExpr, (n) => this._isStringValueName(n))) {
4358
+ return `bf_concat_str ${leftRendered} ${rightRendered}`;
4359
+ }
4360
+ return `bf_add ${leftRendered} ${rightRendered}`;
4361
+ }
4362
+ _isStringValueName(name) {
4363
+ return this.state.stringValueNames.has(name);
4364
+ }
4013
4365
  unary(op, argument, emit) {
4014
4366
  const arg = emit(argument);
4015
4367
  if (op === "!")
@@ -4221,6 +4573,12 @@ ${goFields.join(`
4221
4573
  const recv = emit(object);
4222
4574
  return `bf_trim ${wrapIfMultiToken(recv)}`;
4223
4575
  }
4576
+ case "trimStart":
4577
+ case "trimEnd": {
4578
+ const fn = method === "trimStart" ? "bf_trim_start" : "bf_trim_end";
4579
+ const recv = emit(object);
4580
+ return `${fn} ${wrapIfMultiToken(recv)}`;
4581
+ }
4224
4582
  case "toFixed": {
4225
4583
  const recv = emit(object);
4226
4584
  const digits = args.length >= 1 ? emit(args[0]) : "0";
@@ -4255,6 +4613,12 @@ ${goFields.join(`
4255
4613
  const newS = emit(args[1]);
4256
4614
  return `bf_replace ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`;
4257
4615
  }
4616
+ case "replaceAll": {
4617
+ const recv = emit(object);
4618
+ const oldS = emit(args[0]);
4619
+ const newS = emit(args[1]);
4620
+ return `bf_replace_all ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`;
4621
+ }
4258
4622
  case "repeat": {
4259
4623
  const recv = emit(object);
4260
4624
  const count = args.length === 0 ? "0" : emit(args[0]);
@@ -4441,8 +4805,8 @@ ${goFields.join(`
4441
4805
  const value = negated ? "false" : "true";
4442
4806
  return `len (bf_filter ${arrayExpr} "${field}" ${value})`;
4443
4807
  }
4444
- renderPredicateCondition(pred, param) {
4445
- return this.renderFilterExpr(pred, param);
4808
+ renderPredicateCondition(pred, param, datumField) {
4809
+ return this.renderFilterExpr(pred, param, new Map, datumField ?? undefined);
4446
4810
  }
4447
4811
  needsParens(expr) {
4448
4812
  return expr.kind === "logical" || expr.kind === "unary" || expr.kind === "conditional";
@@ -4463,21 +4827,23 @@ ${goFields.join(`
4463
4827
  }
4464
4828
  return null;
4465
4829
  }
4466
- renderFilterExpr(expr, param, localVarMap = new Map) {
4830
+ renderFilterExpr(expr, param, localVarMap = new Map, datumField) {
4467
4831
  if (this.filterExprDepth === 0)
4468
4832
  this.filterExprUnsupported = false;
4469
4833
  this.filterExprDepth++;
4470
4834
  try {
4471
- return this.renderFilterExprNode(expr, param, localVarMap);
4835
+ return this.renderFilterExprNode(expr, param, localVarMap, datumField);
4472
4836
  } finally {
4473
4837
  this.filterExprDepth--;
4474
4838
  }
4475
4839
  }
4476
- renderFilterExprNode(expr, param, localVarMap) {
4840
+ renderFilterExprNode(expr, param, localVarMap, datumField) {
4841
+ const paramPrefix = datumField ? `.${datumField}` : "";
4842
+ const paramDot = paramPrefix || ".";
4477
4843
  switch (expr.kind) {
4478
4844
  case "identifier": {
4479
4845
  if (expr.name === param) {
4480
- return ".";
4846
+ return paramDot;
4481
4847
  }
4482
4848
  const signal = localVarMap.get(expr.name);
4483
4849
  if (signal) {
@@ -4495,24 +4861,24 @@ ${goFields.join(`
4495
4861
  return String(expr.value);
4496
4862
  case "member": {
4497
4863
  if (expr.object.kind === "identifier" && expr.object.name === param) {
4498
- return `.${capitalizeFieldName(expr.property)}`;
4864
+ return `${paramPrefix}.${capitalizeFieldName(expr.property)}`;
4499
4865
  }
4500
4866
  if (expr.property === "length") {
4501
4867
  const innerHO = this.higherOrderShapeOf(expr.object);
4502
4868
  if (innerHO && innerHO.method === "filter") {
4503
- const lenExpr = this.renderFilterLengthExpr(innerHO, (e) => this.renderFilterExpr(e, param, localVarMap));
4869
+ const lenExpr = this.renderFilterLengthExpr(innerHO, (e) => this.renderFilterExpr(e, param, localVarMap, datumField));
4504
4870
  if (lenExpr)
4505
4871
  return `(${lenExpr})`;
4506
4872
  }
4507
4873
  }
4508
- const obj = this.renderFilterExpr(expr.object, param, localVarMap);
4874
+ const obj = this.renderFilterExpr(expr.object, param, localVarMap, datumField);
4509
4875
  if (this.filterExprUnsupported)
4510
4876
  return "false";
4511
4877
  return `${obj}.${capitalizeFieldName(expr.property)}`;
4512
4878
  }
4513
4879
  case "call": {
4514
4880
  if (expr.callee.kind === "member" && expr.callee.object.kind === "identifier" && expr.callee.object.name === param) {
4515
- return `.${capitalizeFieldName(expr.callee.property)}`;
4881
+ return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`;
4516
4882
  }
4517
4883
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
4518
4884
  return `$.${capitalizeFieldName(expr.callee.name)}`;
@@ -4520,13 +4886,13 @@ ${goFields.join(`
4520
4886
  if (asCallbackMethodCall3(expr) !== null) {
4521
4887
  return this.refuseFilterExprNode(expr);
4522
4888
  }
4523
- const result = this.renderFilterExpr(expr.callee, param, localVarMap);
4889
+ const result = this.renderFilterExpr(expr.callee, param, localVarMap, datumField);
4524
4890
  if (this.filterExprUnsupported)
4525
4891
  return "false";
4526
4892
  return result;
4527
4893
  }
4528
4894
  case "unary": {
4529
- const arg = this.renderFilterExpr(expr.argument, param, localVarMap);
4895
+ const arg = this.renderFilterExpr(expr.argument, param, localVarMap, datumField);
4530
4896
  if (this.filterExprUnsupported)
4531
4897
  return "false";
4532
4898
  if (expr.op === "!") {
@@ -4539,10 +4905,10 @@ ${goFields.join(`
4539
4905
  return arg;
4540
4906
  }
4541
4907
  case "binary": {
4542
- const left = this.renderFilterExpr(expr.left, param, localVarMap);
4908
+ const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField);
4543
4909
  if (this.filterExprUnsupported)
4544
4910
  return "false";
4545
- const right = this.renderFilterExpr(expr.right, param, localVarMap);
4911
+ const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField);
4546
4912
  if (this.filterExprUnsupported)
4547
4913
  return "false";
4548
4914
  switch (expr.op) {
@@ -4561,7 +4927,7 @@ ${goFields.join(`
4561
4927
  case "<=":
4562
4928
  return `le ${left} ${right}`;
4563
4929
  case "+":
4564
- return `bf_add ${left} ${right}`;
4930
+ return this._emitPlus(expr.left, expr.right, left, right);
4565
4931
  case "-":
4566
4932
  return `bf_sub ${left} ${right}`;
4567
4933
  case "*":
@@ -4573,10 +4939,10 @@ ${goFields.join(`
4573
4939
  }
4574
4940
  }
4575
4941
  case "logical": {
4576
- const left = this.renderFilterExpr(expr.left, param, localVarMap);
4942
+ const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField);
4577
4943
  if (this.filterExprUnsupported)
4578
4944
  return "false";
4579
- const right = this.renderFilterExpr(expr.right, param, localVarMap);
4945
+ const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField);
4580
4946
  if (this.filterExprUnsupported)
4581
4947
  return "false";
4582
4948
  if (expr.op === "&&") {
@@ -4644,11 +5010,22 @@ ${goFields.join(`
4644
5010
  if (trimmed === "null" || trimmed === "undefined") {
4645
5011
  return '""';
4646
5012
  }
5013
+ if (this.staticLoopItemStack.length > 0) {
5014
+ const top = this.staticLoopItemStack[this.staticLoopItemStack.length - 1];
5015
+ const parsedForBake = preParsed ?? parseExpression4(trimmed);
5016
+ const resolved = evaluateStaticLiteral3(parsedForBake, new Map([[top.param, top.item]]));
5017
+ const literal = resolved !== null ? scalarToGoLiteral(resolved.value) : null;
5018
+ if (literal !== null) {
5019
+ return literal;
5020
+ }
5021
+ this.staticLoopBakeFailed = true;
5022
+ return '""';
5023
+ }
4647
5024
  const staticIndexed = this.resolveStaticRecordLiteralIndex(trimmed);
4648
5025
  if (staticIndexed !== null) {
4649
5026
  return staticIndexed;
4650
5027
  }
4651
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
5028
+ if (!this.isLoopShadowedName(trimmed) && /^[A-Za-z_$][\w$]*$/.test(trimmed)) {
4652
5029
  const litConst = (this.state.localConstants ?? []).find((c) => c.name === trimmed);
4653
5030
  if (litConst?.value !== undefined) {
4654
5031
  const v = litConst.value.trim();
@@ -4666,7 +5043,7 @@ ${goFields.join(`
4666
5043
  if (inlined !== null) {
4667
5044
  return this.convertExpressionToGo(stringifyParsedExpr2(inlined), out, inlined);
4668
5045
  }
4669
- const parsed = preParsed ?? parseExpression3(trimmed);
5046
+ const parsed = preParsed ?? parseExpression4(trimmed);
4670
5047
  const support = isSupported(parsed);
4671
5048
  if (!support.supported) {
4672
5049
  this.state.errors.push({
@@ -4684,10 +5061,15 @@ ${goFields.join(`
4684
5061
  out.parsed = parsed;
4685
5062
  return this.renderParsedExpr(parsed);
4686
5063
  }
5064
+ isLoopShadowedName(name) {
5065
+ return this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name || this.loopVarRefCount.has(name) || this.isOuterLoopParam(name) || this.loopBindingStack.some((bindings) => bindings.has(name));
5066
+ }
4687
5067
  resolveStaticRecordLiteralIndex(jsExpr) {
4688
5068
  const m = /^([A-Za-z_$][\w$]*)\[\s*(?:'([^']*)'|"([^"]*)")\s*\]$/.exec(jsExpr) ?? /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(jsExpr);
4689
5069
  if (!m)
4690
5070
  return null;
5071
+ if (this.isLoopShadowedName(m[1]))
5072
+ return null;
4691
5073
  const key = m[2] ?? m[3];
4692
5074
  const constInfo = (this.state.localConstants ?? []).find((c) => c.name === m[1] && c.isModule);
4693
5075
  if (constInfo?.value === undefined)
@@ -4793,7 +5175,7 @@ ${goFields.join(`
4793
5175
  }
4794
5176
  convertConditionToGo(jsCondition, preParsed) {
4795
5177
  const trimmed = jsCondition.trim();
4796
- const parsed = preParsed ?? parseExpression3(trimmed);
5178
+ const parsed = preParsed ?? parseExpression4(trimmed);
4797
5179
  const support = isSupported(parsed);
4798
5180
  if (!support.supported) {
4799
5181
  this.state.errors.push({
@@ -4930,7 +5312,7 @@ ${goFields.join(`
4930
5312
  result = `le ${left} ${right}`;
4931
5313
  break;
4932
5314
  case "+":
4933
- result = `bf_add ${left} ${right}`;
5315
+ result = this._emitPlus(expr.left, expr.right, left, right);
4934
5316
  break;
4935
5317
  case "-":
4936
5318
  result = `bf_sub ${left} ${right}`;
@@ -5014,6 +5396,29 @@ ${goFields.join(`
5014
5396
  }
5015
5397
  return;
5016
5398
  }
5399
+ wrapperDatumField(loop) {
5400
+ if (!loop.childComponent)
5401
+ return null;
5402
+ for (const prop of loop.childComponent.props) {
5403
+ if (prop.isEventHandler)
5404
+ continue;
5405
+ if (prop.value.kind !== "expression")
5406
+ continue;
5407
+ const parsed = prop.value.parsed;
5408
+ const isBareParamRef = parsed ? parsed.kind === "identifier" && parsed.name === loop.param : prop.value.expr.trim() === loop.param;
5409
+ if (isBareParamRef)
5410
+ return capitalizeFieldName(prop.name);
5411
+ }
5412
+ return null;
5413
+ }
5414
+ getBakedStaticChildLoop(markerId, childComponent, arrayParsed, param, key) {
5415
+ if (this.bakedStaticChildLoopCache.has(markerId)) {
5416
+ return this.bakedStaticChildLoopCache.get(markerId) ?? null;
5417
+ }
5418
+ const result = analyzeBakeableStaticChildLoop({ props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key }, this.state.localConstants, { isNameShadowed: (name) => this.state.staticLoopSourceBoundNames.has(name) });
5419
+ this.bakedStaticChildLoopCache.set(markerId, result);
5420
+ return result;
5421
+ }
5017
5422
  renderLoop(loop) {
5018
5423
  if (loop.clientOnly) {
5019
5424
  return `{{bfComment "loop:${loop.markerId}"}}{{bfComment "/loop:${loop.markerId}"}}`;
@@ -5034,8 +5439,13 @@ ${goFields.join(`
5034
5439
  }
5035
5440
  });
5036
5441
  }
5442
+ const bakedChildLoop = loop.childComponent ? this.getBakedStaticChildLoop(loop.markerId, loop.childComponent, loop.arrayParsed, loop.param, loop.key ?? undefined) : null;
5443
+ const bakedElementLoop = loop.childComponent ? null : analyzeBakeableStaticElementLoop(loop, this.state.localConstants, { isNameShadowed: (name) => this.state.staticLoopSourceBoundNames.has(name) });
5444
+ if (bakedElementLoop) {
5445
+ return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items);
5446
+ }
5037
5447
  const arrayName = loop.array.trim();
5038
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
5448
+ if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
5039
5449
  const arrayConst = this.state.localConstants.find((c) => c.name === arrayName);
5040
5450
  if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set)) {
5041
5451
  this.state.errors.push({
@@ -5049,7 +5459,7 @@ ${goFields.join(`
5049
5459
  });
5050
5460
  }
5051
5461
  }
5052
- let goArray = this.convertExpressionToGo(loop.array);
5462
+ let goArray = loop.childComponent ? "" : this.convertExpressionToGo(loop.array);
5053
5463
  const param = loop.param;
5054
5464
  let index = loop.index || "_";
5055
5465
  let rangeIndex = index;
@@ -5087,7 +5497,9 @@ ${goFields.join(`
5087
5497
  }
5088
5498
  this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null);
5089
5499
  this.loopWrapperStack.push(!!loop.childComponent);
5500
+ this.loopKeyDepthStack.push(loop.depth);
5090
5501
  const children = this.renderChildren(loop.children);
5502
+ this.loopKeyDepthStack.pop();
5091
5503
  this.loopWrapperStack.pop();
5092
5504
  this.loopScalarItemStack.pop();
5093
5505
  const itemMarker = this.loopItemMarker(loop);
@@ -5115,7 +5527,8 @@ ${goFields.join(`
5115
5527
  if (loop.filterPredicate) {
5116
5528
  let filterCond;
5117
5529
  if (loop.filterPredicate.predicate) {
5118
- filterCond = this.renderPredicateCondition(loop.filterPredicate.predicate, loop.filterPredicate.param);
5530
+ const datumField = this.wrapperDatumField(loop);
5531
+ filterCond = this.renderPredicateCondition(loop.filterPredicate.predicate, loop.filterPredicate.param, datumField);
5119
5532
  } else {
5120
5533
  filterCond = "true";
5121
5534
  }
@@ -5123,6 +5536,38 @@ ${goFields.join(`
5123
5536
  }
5124
5537
  return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
5125
5538
  }
5539
+ renderUnrolledStaticElementLoop(loop, items) {
5540
+ this.inLoop = true;
5541
+ this.loopWrapperStack.push(false);
5542
+ this.loopKeyDepthStack.push(loop.depth);
5543
+ this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null);
5544
+ this.loopParamStack.push(loop.param);
5545
+ let body = "";
5546
+ for (const item of items) {
5547
+ this.staticLoopItemStack.push({ param: loop.param, item });
5548
+ body += this.renderChildren(loop.children);
5549
+ this.staticLoopItemStack.pop();
5550
+ if (this.staticLoopBakeFailed) {
5551
+ this.staticLoopBakeFailed = false;
5552
+ this.state.errors.push({
5553
+ code: "BF101",
5554
+ severity: "error",
5555
+ message: `Loop array \`${loop.array.trim()}\` could not be fully unrolled — an expression in the loop body did not resolve against every item as the compile-time analysis expected.`,
5556
+ loc: loop.loc ?? this.makeLoc(),
5557
+ suggestion: {
5558
+ message: "This indicates a bug in the Go adapter's static-loop unrolling (#2224) rather than an unsupported source pattern; please file a bug with a reproduction."
5559
+ }
5560
+ });
5561
+ break;
5562
+ }
5563
+ }
5564
+ this.loopParamStack.pop();
5565
+ this.loopScalarItemStack.pop();
5566
+ this.loopKeyDepthStack.pop();
5567
+ this.loopWrapperStack.pop();
5568
+ this.inLoop = false;
5569
+ return `{{bfComment "loop:${loop.markerId}"}}${body}{{bfComment "/loop:${loop.markerId}"}}`;
5570
+ }
5126
5571
  loopItemMarker(loop) {
5127
5572
  if (loop.bodyIsMultiRoot)
5128
5573
  return `{{bfComment "bf-loop-i"}}`;
@@ -5230,7 +5675,7 @@ ${goFields.join(`
5230
5675
  ${children}`;
5231
5676
  }
5232
5677
  elementAttrEmitter = {
5233
- emitLiteral: (value, name) => `${name}="${value.value}"`,
5678
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
5234
5679
  emitExpression: (value, name) => {
5235
5680
  if (name === "style") {
5236
5681
  const css = this.tryLowerStyleObject(value.expr);
@@ -5242,7 +5687,7 @@ ${children}`;
5242
5687
  const body = name.startsWith("aria-") ? `${name}="true"` : name;
5243
5688
  return `${preamble}{{if ${goCond}}}${body}{{end}}`;
5244
5689
  }
5245
- const parsed = value.parsed ?? parseExpression3(value.expr.trim());
5690
+ const parsed = value.parsed ?? parseExpression4(value.expr.trim());
5246
5691
  if (parsed.kind === "conditional") {
5247
5692
  const undef = (e) => e.kind === "identifier" && (e.name === "undefined" || e.name === "null") || e.kind === "literal" && (e.value === null || e.value === undefined);
5248
5693
  const test = parsed.test;
@@ -5307,7 +5752,7 @@ ${children}`;
5307
5752
  if (!entries)
5308
5753
  return null;
5309
5754
  for (const e of entries) {
5310
- if (e.kind === "expr" && !isSupported(parseExpression3(e.expr)).supported)
5755
+ if (e.kind === "expr" && !isSupported(parseExpression4(e.expr)).supported)
5311
5756
  return null;
5312
5757
  }
5313
5758
  return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:{{${this.convertExpressionToGo(e.expr)}}}`).join(";");
@@ -5317,12 +5762,15 @@ ${children}`;
5317
5762
  for (const attr of element.attrs) {
5318
5763
  if (attr.clientOnly)
5319
5764
  continue;
5765
+ if (isDangerousInnerHtmlAttr(attr))
5766
+ continue;
5320
5767
  let attrName;
5321
5768
  if (attr.name === "className")
5322
5769
  attrName = "class";
5323
- else if (attr.name === "key")
5324
- attrName = "data-key";
5325
- else
5770
+ else if (attr.name === "key") {
5771
+ const depth = this.loopKeyDepthStack.at(-1) ?? 0;
5772
+ attrName = depth > 0 ? `data-key-${depth}` : "data-key";
5773
+ } else
5326
5774
  attrName = attr.name;
5327
5775
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
5328
5776
  if (lowered)
@@ -5422,42 +5870,18 @@ ${children}`;
5422
5870
  var goTemplateAdapter = new GoTemplateAdapter;
5423
5871
  // src/conformance-pins.ts
5424
5872
  var conformancePins = {
5425
- "static-array-children": [{ code: "BF103", severity: "error" }],
5426
- "todo-app": [{ code: "BF103", severity: "error" }],
5427
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
5428
5873
  "static-array-from-props": [{ code: "BF101", severity: "error" }],
5429
- "static-array-from-props-with-component": [
5430
- { code: "BF103", severity: "error" },
5431
- { code: "BF101", severity: "error" }
5432
- ],
5874
+ "static-array-from-props-with-component": [{ code: "BF101", severity: "error" }],
5433
5875
  "filter-nested-callback-predicate": [
5434
5876
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
5435
5877
  ],
5436
5878
  "filter-nested-find-predicate": [
5437
5879
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
5438
5880
  ],
5439
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
5440
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }],
5441
- "string-replaceall": [{ code: "BF101", severity: "error" }]
5881
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
5442
5882
  };
5443
5883
  // src/render-divergences.ts
5444
- var renderDivergences = {
5445
- "html-entity-text": "`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
5446
- "string-concat-plus": "`'Hello, ' + name` renders \"0\" — JS string-concat `+` lowered through numeric addition",
5447
- "optional-chaining-prop": "`user?.name ?? …` on an object prop: generated Go fails to run (exit 1) — optional chaining into a struct/map prop has no lowering",
5448
- "number-tofixed": "`.toFixed(2)` on a number PROP: generated Go fails to run (exit 1)",
5449
- "math-methods": "Math.min/max/abs over a signal: generated Go fails to run (exit 1) — only Math.floor is registered",
5450
- "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
5451
- "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)`: generated Go fails to run (exit 1) — no object-iteration loop lowering",
5452
- "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
5453
- "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
5454
- "grandchild-composition": "three-level composition: the grandchild's threaded prop renders EMPTY — prop forwarding through two template-render layers loses the value",
5455
- "child-primitive-props": "numeric/boolean LITERAL props on a child (`count={5}` `active={true}`) render as Go zero values (0 / false)",
5456
- "memo-chain": "a memo derived from another memo renders EMPTY for the second layer — the constructor folds only one derivation level",
5457
- "signal-object-field": "object-valued signal (`user().name`): generated Go fails to run (exit 1) — no struct synthesis outside loops",
5458
- "string-slice": '`.slice()` on a STRING routes through the array `bf_slice` helper and renders "[]" instead of the substring',
5459
- "string-trim-sided": "`.trimStart()` / `.trimEnd()`: generated Go fails to run (exit 1) — only both-sides `bf_trim` exists"
5460
- };
5884
+ var renderDivergences = {};
5461
5885
  export {
5462
5886
  renderDivergences,
5463
5887
  goTemplateAdapter,