@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/build.js CHANGED
@@ -365,7 +365,7 @@ var init_path = __esm(() => {
365
365
  import {
366
366
  BaseAdapter,
367
367
  isBooleanAttr,
368
- parseExpression as parseExpression3,
368
+ parseExpression as parseExpression4,
369
369
  stringifyParsedExpr as stringifyParsedExpr2,
370
370
  parseStyleObjectEntries,
371
371
  isSupported,
@@ -382,10 +382,17 @@ import {
382
382
  collectModuleStringConsts as collectModuleStringConstsShared,
383
383
  prepareLoweringMatchers,
384
384
  envSignalReaderFor,
385
- computeSsrSeedPlan
385
+ computeSsrSeedPlan,
386
+ isStringConcatBinary,
387
+ isDangerousInnerHtmlAttr,
388
+ resolveDangerousInnerHtml,
389
+ dangerousInnerHtmlMetacharViolation,
390
+ dangerousInnerHtmlDiagnostic,
391
+ collectLoopBoundNames as collectLoopBoundNames2,
392
+ evaluateStaticLiteral as evaluateStaticLiteral3
386
393
  } from "@barefootjs/jsx";
387
394
  import { findInterpolationEnd } from "@barefootjs/jsx/scanner";
388
- import { BF_REGION } from "@barefootjs/shared";
395
+ import { BF_REGION, escapeHtml } from "@barefootjs/shared";
389
396
 
390
397
  // src/adapter/lib/go-naming.ts
391
398
  var GO_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -672,6 +679,7 @@ var GO_TEMPLATE_PRIMITIVES = {
672
679
  "Math.floor": { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
673
680
  "Math.ceil": { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
674
681
  "Math.round": { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
682
+ "Math.abs": { arity: 1, emit: (args) => `bf_abs ${wrapGoArg(args[0])}` },
675
683
  "Math.min": { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
676
684
  "Math.max": { arity: 2, emit: (args) => `bf_max ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` }
677
685
  };
@@ -687,6 +695,7 @@ class CompileState {
687
695
  restPropsName = null;
688
696
  moduleStringConsts = new Map;
689
697
  localConstants = [];
698
+ staticLoopSourceBoundNames = new Set;
690
699
  localHelperNames = new Set;
691
700
  currentMemos = [];
692
701
  currentTypeDefinitions = [];
@@ -697,6 +706,7 @@ class CompileState {
697
706
  hoistedMemoLocals = new Map;
698
707
  loweringMatchers = [];
699
708
  nillablePropNames = new Set;
709
+ stringValueNames = new Set;
700
710
  rootScopeNodes = new Set;
701
711
  memoBackedLoopSlice = new Map;
702
712
  usesHtmlTemplate = false;
@@ -854,6 +864,178 @@ function collectNestedComponents(node, result) {
854
864
  }
855
865
  }
856
866
 
867
+ // src/adapter/analysis/static-child-loop-bake.ts
868
+ import { evaluateStaticLiteral, parseExpression, resolveStaticLoopSource } from "@barefootjs/jsx";
869
+ function scalarToGoLiteral(value) {
870
+ if (typeof value === "string")
871
+ return `"${escapeGoString(value)}"`;
872
+ if (typeof value === "number")
873
+ return String(value);
874
+ if (typeof value === "boolean")
875
+ return value ? "true" : "false";
876
+ return null;
877
+ }
878
+ function analyzeBakeableStaticChildLoop(nested, localConstants, opts) {
879
+ if (!nested.loopParam || /^[{[]/.test(nested.loopParam))
880
+ return null;
881
+ const staticItemsResult = resolveStaticLoopSource(nested.loopArrayParsed, localConstants, opts);
882
+ if (staticItemsResult === null)
883
+ return null;
884
+ const items = [];
885
+ for (const item of staticItemsResult) {
886
+ const bindings = new Map([[nested.loopParam, item]]);
887
+ const inputFields = [];
888
+ for (const prop of nested.props) {
889
+ if (prop.isEventHandler)
890
+ continue;
891
+ if (prop.name.includes("-"))
892
+ continue;
893
+ const resolved = resolvePropValue(prop.value, bindings);
894
+ if (resolved === undefined)
895
+ return null;
896
+ const goValue = scalarToGoLiteral(resolved);
897
+ if (goValue === null)
898
+ return null;
899
+ inputFields.push({ goField: capitalizeFieldName(prop.name), goValue });
900
+ }
901
+ let dataKey = null;
902
+ if (nested.loopKey) {
903
+ const keyExpr = parseExpression(nested.loopKey);
904
+ const keyResolved = evaluateStaticLiteral(keyExpr, bindings);
905
+ if (keyResolved === null)
906
+ return null;
907
+ dataKey = String(keyResolved.value);
908
+ }
909
+ items.push({ inputFields, dataKey });
910
+ }
911
+ return { items };
912
+ }
913
+ function resolvePropValue(value, bindings) {
914
+ switch (value.kind) {
915
+ case "literal":
916
+ return value.value;
917
+ case "boolean-shorthand":
918
+ case "boolean-attr":
919
+ return true;
920
+ case "expression": {
921
+ if (!value.parsed)
922
+ return;
923
+ const resolved = evaluateStaticLiteral(value.parsed, bindings);
924
+ return resolved === null ? undefined : resolved.value;
925
+ }
926
+ default:
927
+ return;
928
+ }
929
+ }
930
+
931
+ // src/adapter/analysis/static-element-loop-bake.ts
932
+ import {
933
+ evaluateStaticLiteral as evaluateStaticLiteral2,
934
+ resolveStaticLoopSource as resolveStaticLoopSource2
935
+ } from "@barefootjs/jsx";
936
+ var ALLOWED_ATTR_EXPRESSION_KINDS = new Set([
937
+ "identifier",
938
+ "member",
939
+ "index-access",
940
+ "literal"
941
+ ]);
942
+ function analyzeBakeableStaticElementLoop(loop, localConstants, opts) {
943
+ if (loop.childComponent)
944
+ return null;
945
+ if (loop.method === "flatMap" || loop.flatMapCallback)
946
+ return null;
947
+ if (!loop.param || /^[{[]/.test(loop.param))
948
+ return null;
949
+ if (loop.index && loop.index !== "_")
950
+ return null;
951
+ if (loop.paramBindings && loop.paramBindings.length > 0)
952
+ return null;
953
+ if (loop.filterPredicate || loop.sortComparator)
954
+ return null;
955
+ if (loop.iterationShape || loop.objectIteration)
956
+ return null;
957
+ if (loop.bodyIsMultiRoot || loop.bodyIsItemConditional)
958
+ return null;
959
+ if (!isFoldableTree(loop.children))
960
+ return null;
961
+ const items = resolveStaticLoopSource2(loop.arrayParsed, localConstants, opts);
962
+ if (items === null)
963
+ return null;
964
+ for (const item of items) {
965
+ const bindings = new Map([[loop.param, item]]);
966
+ if (!allExpressionsFoldFor(loop.children, bindings))
967
+ return null;
968
+ }
969
+ return { items };
970
+ }
971
+ function isFoldableTree(nodes) {
972
+ for (const node of nodes) {
973
+ switch (node.type) {
974
+ case "text":
975
+ case "expression":
976
+ continue;
977
+ case "element":
978
+ if (!isFoldableAttrs(node))
979
+ return false;
980
+ if (!isFoldableTree(node.children))
981
+ return false;
982
+ continue;
983
+ default:
984
+ return false;
985
+ }
986
+ }
987
+ return true;
988
+ }
989
+ function isFoldableAttrs(element) {
990
+ for (const attr of element.attrs) {
991
+ if (attr.clientOnly)
992
+ continue;
993
+ switch (attr.value.kind) {
994
+ case "literal":
995
+ case "boolean-attr":
996
+ case "boolean-shorthand":
997
+ continue;
998
+ case "expression":
999
+ if (!attr.value.parsed || !ALLOWED_ATTR_EXPRESSION_KINDS.has(attr.value.parsed.kind))
1000
+ return false;
1001
+ continue;
1002
+ default:
1003
+ return false;
1004
+ }
1005
+ }
1006
+ return true;
1007
+ }
1008
+ function allExpressionsFoldFor(nodes, bindings) {
1009
+ for (const node of nodes) {
1010
+ if (node.type === "expression") {
1011
+ if (node.clientOnly)
1012
+ continue;
1013
+ if (!node.parsed || !resolvesToScalar(node.parsed, bindings))
1014
+ return false;
1015
+ continue;
1016
+ }
1017
+ if (node.type === "element") {
1018
+ for (const attr of node.attrs) {
1019
+ if (attr.clientOnly)
1020
+ continue;
1021
+ if (attr.value.kind !== "expression")
1022
+ continue;
1023
+ if (!attr.value.parsed || !resolvesToScalar(attr.value.parsed, bindings))
1024
+ return false;
1025
+ }
1026
+ if (!allExpressionsFoldFor(node.children, bindings))
1027
+ return false;
1028
+ }
1029
+ }
1030
+ return true;
1031
+ }
1032
+ function resolvesToScalar(expr, bindings) {
1033
+ const resolved = evaluateStaticLiteral2(expr, bindings);
1034
+ if (resolved === null)
1035
+ return false;
1036
+ return scalarToGoLiteral(resolved.value) !== null;
1037
+ }
1038
+
857
1039
  // src/adapter/expr/helper-inline.ts
858
1040
  function inlineLocalHelperCall(ctx, jsExpr, callParsed) {
859
1041
  if (ctx.state.localHelperNames.size === 0)
@@ -1041,7 +1223,7 @@ function forEachValueChild(n, visit) {
1041
1223
 
1042
1224
  // src/adapter/expr/url-builder.ts
1043
1225
  import {
1044
- parseExpression,
1226
+ parseExpression as parseExpression2,
1045
1227
  stringifyParsedExpr,
1046
1228
  isValidHelperId
1047
1229
  } from "@barefootjs/jsx";
@@ -1074,7 +1256,7 @@ function lowerRegisteredCall(ctx, jsExpr, preParsed) {
1074
1256
  if (!call) {
1075
1257
  if (!/^\s*[A-Za-z_$][\w$]*\s*\(/.test(jsExpr))
1076
1258
  return null;
1077
- const parsed = parseExpression(jsExpr);
1259
+ const parsed = parseExpression2(jsExpr);
1078
1260
  if (parsed.kind !== "call")
1079
1261
  return null;
1080
1262
  call = parsed;
@@ -1116,7 +1298,7 @@ function typeInfoToGo(ctx, typeInfo, defaultValue) {
1116
1298
  case "string":
1117
1299
  return "string";
1118
1300
  case "number":
1119
- return "int";
1301
+ return defaultValue !== undefined ? numberPrimitiveGoType(defaultValue) : "int";
1120
1302
  case "boolean":
1121
1303
  return "bool";
1122
1304
  default:
@@ -1167,6 +1349,9 @@ function tsTypeStringToGo(ctx, tsType) {
1167
1349
  return t;
1168
1350
  return "interface{}";
1169
1351
  }
1352
+ function numberPrimitiveGoType(value) {
1353
+ return /^-?\d+\.\d+$/.test(value) ? "float64" : "int";
1354
+ }
1170
1355
  function inferTypeFromValue(value) {
1171
1356
  if (value === "true" || value === "false")
1172
1357
  return "bool";
@@ -1279,9 +1464,9 @@ function convertInitialValue(ctx, value, typeInfo, propsParams, preParsed) {
1279
1464
  return value === "true" ? "true" : "false";
1280
1465
  }
1281
1466
  if (typeInfo.primitive === "number") {
1282
- if (/^\d+$/.test(value))
1467
+ if (/^-?\d+$/.test(value))
1283
1468
  return value;
1284
- if (/^\d+\.\d+$/.test(value))
1469
+ if (/^-?\d+\.\d+$/.test(value))
1285
1470
  return value;
1286
1471
  return "0";
1287
1472
  }
@@ -1306,6 +1491,12 @@ function convertInitialValue(ctx, value, typeInfo, propsParams, preParsed) {
1306
1491
  }
1307
1492
  return '""';
1308
1493
  }
1494
+ if (ctx.state.localStructFields.has(typeInfo.raw)) {
1495
+ const baked = jsLiteralToGo(ctx, typeInfo, preParsed);
1496
+ if (baked !== null)
1497
+ return baked;
1498
+ return `${typeInfo.raw}{}`;
1499
+ }
1309
1500
  }
1310
1501
  return "nil";
1311
1502
  }
@@ -1996,10 +2187,11 @@ function memoInitialFromParsedBody(ctx, body, signals, propsParams, propFallback
1996
2187
  const operand = String(body.right.value);
1997
2188
  const depName = getterCallName(body.left);
1998
2189
  if (depName) {
1999
- const signal = signals.find((s) => s.getter === depName);
2000
- if (signal) {
2001
- const signalInitial = getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars);
2002
- return `${signalInitial} ${operator} ${operand}`;
2190
+ const depInitial = resolveGetterValueAsGo(ctx, depName, signals, propsParams, propFallbackVars, resolving);
2191
+ const isArithmeticSafe = depInitial !== null && !depInitial.startsWith("func(");
2192
+ if (isArithmeticSafe) {
2193
+ const wrapped = /\s/.test(depInitial) ? `(${depInitial})` : depInitial;
2194
+ return `${wrapped} ${operator} ${operand}`;
2003
2195
  }
2004
2196
  }
2005
2197
  const propName = propsMemberName(body.left);
@@ -2034,9 +2226,9 @@ function memoInitialFromParsedBody(ctx, body, signals, propsParams, propFallback
2034
2226
  }
2035
2227
  const simpleDep = getterCallName(body);
2036
2228
  if (simpleDep) {
2037
- const signal = signals.find((s) => s.getter === simpleDep);
2038
- if (signal) {
2039
- return getSignalInitialValueAsGo(ctx, signal.initialValue, propsParams, propFallbackVars);
2229
+ const depInitial = resolveGetterValueAsGo(ctx, simpleDep, signals, propsParams, propFallbackVars, resolving);
2230
+ if (depInitial !== null) {
2231
+ return depInitial;
2040
2232
  }
2041
2233
  }
2042
2234
  const simpleProp = propsMemberName(body);
@@ -2158,7 +2350,7 @@ function propsAccessNameFromParsed2(ctx, node) {
2158
2350
 
2159
2351
  // src/adapter/spread/spread-codegen.ts
2160
2352
  import ts2 from "typescript";
2161
- import { parseExpression as parseExpression2, parseRecordIndexAccess } from "@barefootjs/jsx";
2353
+ import { parseExpression as parseExpression3, parseRecordIndexAccess } from "@barefootjs/jsx";
2162
2354
  function collectSpreadSlots(ctx, node) {
2163
2355
  const result = [];
2164
2356
  collectSpreadSlotsRecursive(ctx, node, result);
@@ -2265,7 +2457,7 @@ function parsedObjectLiteralToGoMap(parsed) {
2265
2457
  }
2266
2458
  function buildSpreadInitializer(ctx, spreadExpr, ir, parsed) {
2267
2459
  const trimmed = spreadExpr.trim();
2268
- const conditionalTree = parsed ?? parseExpression2(trimmed);
2460
+ const conditionalTree = parsed ?? parseExpression3(trimmed);
2269
2461
  const conditional = buildConditionalSpreadInitializer(ctx, conditionalTree, ir);
2270
2462
  if (conditional !== undefined)
2271
2463
  return conditional;
@@ -2296,7 +2488,7 @@ function buildSpreadInitializer(ctx, spreadExpr, ir, parsed) {
2296
2488
  if (localConst?.value !== undefined) {
2297
2489
  const initTrimmed = localConst.value.trim();
2298
2490
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
2299
- const resolved = buildConditionalSpreadInitializer(ctx, parseExpression2(initTrimmed), ir);
2491
+ const resolved = buildConditionalSpreadInitializer(ctx, parseExpression3(initTrimmed), ir);
2300
2492
  if (resolved)
2301
2493
  return resolved;
2302
2494
  if (resolved === null)
@@ -2427,8 +2619,47 @@ function buildPropTypeOverrides(ctx, ir) {
2427
2619
  }
2428
2620
  }
2429
2621
  }
2622
+ for (const propName of collectToFixedPropNames(ir.root)) {
2623
+ const param = ir.metadata.propsParams.find((p) => p.name === propName);
2624
+ if (!param)
2625
+ continue;
2626
+ const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue);
2627
+ if (resolved === "int") {
2628
+ overrides.set(propName, "float64");
2629
+ }
2630
+ }
2430
2631
  return overrides;
2431
2632
  }
2633
+ function collectToFixedPropNames(root) {
2634
+ const names = new Set;
2635
+ const checkExpr = (expr) => {
2636
+ if (expr?.kind === "array-method" && expr.method === "toFixed" && expr.object.kind === "identifier") {
2637
+ names.add(expr.object.name);
2638
+ }
2639
+ };
2640
+ const walk = (node) => {
2641
+ if (!node)
2642
+ return;
2643
+ if (node.type === "expression")
2644
+ checkExpr(node.parsed);
2645
+ if (node.type === "conditional") {
2646
+ checkExpr(node.parsedCondition);
2647
+ walk(node.whenTrue);
2648
+ walk(node.whenFalse);
2649
+ }
2650
+ if (node.type === "element") {
2651
+ for (const attr of node.attrs) {
2652
+ if (attr.value.kind === "expression")
2653
+ checkExpr(attr.value.parsed);
2654
+ }
2655
+ }
2656
+ if ("children" in node && Array.isArray(node.children)) {
2657
+ node.children.forEach(walk);
2658
+ }
2659
+ };
2660
+ walk(root);
2661
+ return names;
2662
+ }
2432
2663
  function resolvePropGoType(ctx, param, propTypeOverrides) {
2433
2664
  const base = propTypeOverrides.get(param.name) ?? typeInfoToGo(ctx, param.type, param.defaultValue);
2434
2665
  if (param.optional && ctx.state.localStructFields.has(base)) {
@@ -2447,6 +2678,38 @@ function collectNillablePropNames(ctx, ir) {
2447
2678
  return nillable;
2448
2679
  }
2449
2680
 
2681
+ // src/adapter/props/prop-classes.ts
2682
+ import { collectLoopBoundNames } from "@barefootjs/jsx";
2683
+ function isStringTypeInfo(type) {
2684
+ return type.kind === "primitive" && type.primitive === "string";
2685
+ }
2686
+ function isBareStringLiteral(initialValue) {
2687
+ if (!initialValue)
2688
+ return false;
2689
+ const v = initialValue.trim();
2690
+ return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
2691
+ }
2692
+ function collectStringValueNames(ir) {
2693
+ const names = new Set;
2694
+ for (const s of ir.metadata.signals) {
2695
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
2696
+ names.add(s.getter);
2697
+ }
2698
+ }
2699
+ for (const p of ir.metadata.propsParams) {
2700
+ if (isStringTypeInfo(p.type))
2701
+ names.add(p.name);
2702
+ }
2703
+ for (const c of ir.metadata.localConstants) {
2704
+ if (c.type !== null && isStringTypeInfo(c.type) || isBareStringLiteral(c.value)) {
2705
+ names.add(c.name);
2706
+ }
2707
+ }
2708
+ for (const bound of collectLoopBoundNames(ir))
2709
+ names.delete(bound);
2710
+ return names;
2711
+ }
2712
+
2450
2713
  // src/adapter/go-template-adapter.ts
2451
2714
  var STRING_METHODS = new Set([
2452
2715
  "replace",
@@ -2488,12 +2751,16 @@ class GoTemplateAdapter extends BaseAdapter {
2488
2751
  return this.state.errors;
2489
2752
  }
2490
2753
  inLoop = false;
2754
+ bakedStaticChildLoopCache = new Map;
2491
2755
  loopParamStack = [];
2756
+ loopKeyDepthStack = [];
2492
2757
  loopScalarItemStack = [];
2493
2758
  loopWrapperStack = [];
2494
2759
  loopVarRefCount = new Map;
2495
2760
  loopBindingStack = [];
2496
2761
  loopRestExcludeStack = [];
2762
+ staticLoopItemStack = [];
2763
+ staticLoopBakeFailed = false;
2497
2764
  childComponentShapes = new Map;
2498
2765
  childContextConsumers = new Map;
2499
2766
  constructor(options = {}) {
@@ -2509,6 +2776,8 @@ class GoTemplateAdapter extends BaseAdapter {
2509
2776
  this.state.restPropsName = ir.metadata.restPropsName ?? null;
2510
2777
  this.state.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
2511
2778
  this.state.localConstants = ir.metadata.localConstants ?? [];
2779
+ this.state.staticLoopSourceBoundNames = collectLoopBoundNames2(ir);
2780
+ this.bakedStaticChildLoopCache = new Map;
2512
2781
  this.state.localHelperNames = new Set(this.state.localConstants.filter((c) => !c.isModule && c.containsArrow).map((c) => c.name));
2513
2782
  this.state.currentMemos = ir.metadata.memos ?? [];
2514
2783
  this.state.currentTypeDefinitions = ir.metadata.typeDefinitions ?? [];
@@ -2536,6 +2805,7 @@ class GoTemplateAdapter extends BaseAdapter {
2536
2805
  this.state.pendingChildrenDefines = [];
2537
2806
  this.primeCompileState(ir);
2538
2807
  this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir);
2808
+ this.state.stringValueNames = collectStringValueNames(ir);
2539
2809
  if (!options?.siblingTemplatesRegistered) {
2540
2810
  this.checkImportedLoopChildComponents(ir);
2541
2811
  }
@@ -2889,6 +3159,8 @@ ${goFields.join(`
2889
3159
  lines.push(` ${fieldName} ${goType}`);
2890
3160
  }
2891
3161
  for (const nested of inputNested) {
3162
+ if (nested.loopMarkerId && this.getBakedStaticChildLoop(nested.loopMarkerId, nested, nested.loopArrayParsed, nested.loopParam, nested.loopKey))
3163
+ continue;
2892
3164
  lines.push(` ${nested.name}s []${nested.name}Input`);
2893
3165
  }
2894
3166
  const takenInput = new Set(ir.metadata.propsParams.map((p) => capitalizeFieldName(p.name)));
@@ -3020,6 +3292,21 @@ ${goFields.join(`
3020
3292
  const staticWithoutBody = staticNested.filter((n) => !n.bodyChildren || n.bodyChildren.length === 0);
3021
3293
  for (const nested of staticWithoutBody) {
3022
3294
  const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`;
3295
+ const baked = nested.loopMarkerId ? this.getBakedStaticChildLoop(nested.loopMarkerId, nested, nested.loopArrayParsed, nested.loopParam, nested.loopKey) : null;
3296
+ if (baked) {
3297
+ lines.push(` ${varName} := make([]${nested.name}Props, ${baked.items.length})`);
3298
+ baked.items.forEach((item, i) => {
3299
+ const fields = item.inputFields.map((f) => `${f.goField}: ${f.goValue}`).join(", ");
3300
+ lines.push(` ${varName}[${i}] = New${nested.name}Props(${nested.name}Input{${fields}})`);
3301
+ lines.push(` ${varName}[${i}].BfParent = scopeID`);
3302
+ lines.push(` ${varName}[${i}].BfMount = "${nested.slotId}"`);
3303
+ if (item.dataKey !== null) {
3304
+ lines.push(` ${varName}[${i}].BfDataKey = ${JSON.stringify(item.dataKey)}`);
3305
+ }
3306
+ });
3307
+ lines.push("");
3308
+ continue;
3309
+ }
3023
3310
  lines.push(` ${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`);
3024
3311
  lines.push(` for i, item := range in.${nested.name}s {`);
3025
3312
  lines.push(` ${varName}[i] = New${nested.name}Props(item)`);
@@ -3233,6 +3520,13 @@ ${goFields.join(`
3233
3520
  break;
3234
3521
  }
3235
3522
  }
3523
+ if (parsedValue) {
3524
+ const goVal = parsedLiteralToGo(this.emitCtx, parsedValue);
3525
+ if (goVal !== null) {
3526
+ emitChildField(prop.name, goVal);
3527
+ break;
3528
+ }
3529
+ }
3236
3530
  const resolvedValue = this.resolveDynamicPropValue(exprText, ir.metadata.signals, ir.metadata.memos, ir.metadata.propsParams);
3237
3531
  if (resolvedValue !== null) {
3238
3532
  emitChildField(prop.name, resolvedValue);
@@ -3240,6 +3534,24 @@ ${goFields.join(`
3240
3534
  break;
3241
3535
  }
3242
3536
  case "jsx-children":
3537
+ if (prop.name !== "children") {
3538
+ const text = this.extractTextChildren(prop.value.children);
3539
+ if (text !== null) {
3540
+ emitChildField(prop.name, JSON.stringify(text));
3541
+ } else {
3542
+ const html = this.extractHtmlChildren(prop.value.children);
3543
+ if (html !== null) {
3544
+ this.state.usesHtmlTemplate = true;
3545
+ emitChildField(prop.name, `template.HTML(${JSON.stringify(html)})`);
3546
+ } else {
3547
+ const scopedHtml = this.extractScopedHtmlChildren(prop.value.children);
3548
+ if (scopedHtml !== null) {
3549
+ this.state.usesHtmlTemplate = true;
3550
+ emitChildField(prop.name, `template.HTML(${scopedHtml})`);
3551
+ }
3552
+ }
3553
+ }
3554
+ }
3243
3555
  break;
3244
3556
  }
3245
3557
  }
@@ -3881,6 +4193,17 @@ ${goFields.join(`
3881
4193
  return computeMemoInitialValueOrNull(this.emitCtx, memo, signals, propsParams, undefined, new Set([memo.name]));
3882
4194
  }
3883
4195
  }
4196
+ const propsObjectName = this.state.propsObjectName;
4197
+ const identifierPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
4198
+ const bareIdentifier = identifierPattern.test(expr) ? expr : null;
4199
+ const barePropAccess = propsObjectName && expr.startsWith(`${propsObjectName}.`) && identifierPattern.test(expr.slice(propsObjectName.length + 1)) ? expr.slice(propsObjectName.length + 1) : null;
4200
+ const passthroughName = bareIdentifier ?? barePropAccess;
4201
+ const localConst = this.state.localConstants.find((c) => c.name === passthroughName);
4202
+ const isPropsDestructureAlias = localConst !== undefined && propsObjectName !== null && localConst.value === `${propsObjectName}.${passthroughName}`;
4203
+ const shadowedByLocal = passthroughName !== null && (localConst !== undefined && !isPropsDestructureAlias || this.state.localHelperNames.has(passthroughName));
4204
+ if (passthroughName && !shadowedByLocal && propsParams.some((p) => p.name === passthroughName)) {
4205
+ return `in.${capitalizeFieldName(passthroughName)}`;
4206
+ }
3884
4207
  return null;
3885
4208
  }
3886
4209
  inferMemoType(memo, signals, propsParamMap) {
@@ -4005,7 +4328,7 @@ ${goFields.join(`
4005
4328
  return this.renderElement(node);
4006
4329
  }
4007
4330
  emitText(node) {
4008
- return node.value;
4331
+ return escapeHtml(node.value);
4009
4332
  }
4010
4333
  emitExpression(node) {
4011
4334
  return this.renderExpression(node);
@@ -4037,7 +4360,8 @@ ${goFields.join(`
4037
4360
  renderElement(element) {
4038
4361
  const tag = element.tag;
4039
4362
  const attrs = this.renderAttributes(element);
4040
- const children = this.renderChildren(element.children);
4363
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
4364
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
4041
4365
  let hydrationAttrs = "";
4042
4366
  if (element.needsScope) {
4043
4367
  hydrationAttrs += ` ${this.renderScopeMarker(".ScopeID")}`;
@@ -4072,6 +4396,22 @@ ${goFields.join(`
4072
4396
  }
4073
4397
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
4074
4398
  }
4399
+ renderDangerousInnerHtml(element) {
4400
+ const resolution = resolveDangerousInnerHtml(element);
4401
+ if (!resolution)
4402
+ return null;
4403
+ if (resolution.kind === "dynamic") {
4404
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
4405
+ return "";
4406
+ }
4407
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
4408
+ if (violation) {
4409
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
4410
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
4411
+ return "";
4412
+ }
4413
+ return resolution.html;
4414
+ }
4075
4415
  renderExpression(expr) {
4076
4416
  if (expr.clientOnly) {
4077
4417
  if (expr.slotId) {
@@ -4265,7 +4605,7 @@ ${goFields.join(`
4265
4605
  const argsStr = args.map(emit).join(" ");
4266
4606
  return `${calleeStr} ${argsStr}`;
4267
4607
  }
4268
- member(object, property, _computed, emit) {
4608
+ member(object, property, _computed, optional, emit) {
4269
4609
  const objHO = this.higherOrderShapeOf(object);
4270
4610
  if (property === "length" && objHO) {
4271
4611
  const result = this.renderFilterLengthExpr(objHO, emit);
@@ -4307,6 +4647,9 @@ ${goFields.join(`
4307
4647
  const obj = emit(object);
4308
4648
  if (property === "length")
4309
4649
  return `len ${obj}`;
4650
+ if (optional) {
4651
+ return `bf_get ${wrapIfMultiToken(obj)} ${JSON.stringify(goFieldNameForKey(property))}`;
4652
+ }
4310
4653
  return `${obj}.${goFieldNameForKey(property)}`;
4311
4654
  }
4312
4655
  indexAccess(object, index, emit) {
@@ -4337,7 +4680,7 @@ ${goFields.join(`
4337
4680
  case "<=":
4338
4681
  return `le ${wl} ${wr}`;
4339
4682
  case "+":
4340
- return `bf_add ${wl} ${wr}`;
4683
+ return this._emitPlus(left, right, wl, wr);
4341
4684
  case "-":
4342
4685
  return `bf_sub ${wl} ${wr}`;
4343
4686
  case "*":
@@ -4350,6 +4693,15 @@ ${goFields.join(`
4350
4693
  return `${l} ${op} ${r}`;
4351
4694
  }
4352
4695
  }
4696
+ _emitPlus(leftExpr, rightExpr, leftRendered, rightRendered) {
4697
+ if (isStringConcatBinary("+", leftExpr, rightExpr, (n) => this._isStringValueName(n))) {
4698
+ return `bf_concat_str ${leftRendered} ${rightRendered}`;
4699
+ }
4700
+ return `bf_add ${leftRendered} ${rightRendered}`;
4701
+ }
4702
+ _isStringValueName(name) {
4703
+ return this.state.stringValueNames.has(name);
4704
+ }
4353
4705
  unary(op, argument, emit) {
4354
4706
  const arg = emit(argument);
4355
4707
  if (op === "!")
@@ -4561,6 +4913,12 @@ ${goFields.join(`
4561
4913
  const recv = emit(object);
4562
4914
  return `bf_trim ${wrapIfMultiToken(recv)}`;
4563
4915
  }
4916
+ case "trimStart":
4917
+ case "trimEnd": {
4918
+ const fn = method === "trimStart" ? "bf_trim_start" : "bf_trim_end";
4919
+ const recv = emit(object);
4920
+ return `${fn} ${wrapIfMultiToken(recv)}`;
4921
+ }
4564
4922
  case "toFixed": {
4565
4923
  const recv = emit(object);
4566
4924
  const digits = args.length >= 1 ? emit(args[0]) : "0";
@@ -4595,6 +4953,12 @@ ${goFields.join(`
4595
4953
  const newS = emit(args[1]);
4596
4954
  return `bf_replace ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`;
4597
4955
  }
4956
+ case "replaceAll": {
4957
+ const recv = emit(object);
4958
+ const oldS = emit(args[0]);
4959
+ const newS = emit(args[1]);
4960
+ return `bf_replace_all ${wrapIfMultiToken(recv)} ${wrapIfMultiToken(oldS)} ${wrapIfMultiToken(newS)}`;
4961
+ }
4598
4962
  case "repeat": {
4599
4963
  const recv = emit(object);
4600
4964
  const count = args.length === 0 ? "0" : emit(args[0]);
@@ -4781,8 +5145,8 @@ ${goFields.join(`
4781
5145
  const value = negated ? "false" : "true";
4782
5146
  return `len (bf_filter ${arrayExpr} "${field}" ${value})`;
4783
5147
  }
4784
- renderPredicateCondition(pred, param) {
4785
- return this.renderFilterExpr(pred, param);
5148
+ renderPredicateCondition(pred, param, datumField) {
5149
+ return this.renderFilterExpr(pred, param, new Map, datumField ?? undefined);
4786
5150
  }
4787
5151
  needsParens(expr) {
4788
5152
  return expr.kind === "logical" || expr.kind === "unary" || expr.kind === "conditional";
@@ -4803,21 +5167,23 @@ ${goFields.join(`
4803
5167
  }
4804
5168
  return null;
4805
5169
  }
4806
- renderFilterExpr(expr, param, localVarMap = new Map) {
5170
+ renderFilterExpr(expr, param, localVarMap = new Map, datumField) {
4807
5171
  if (this.filterExprDepth === 0)
4808
5172
  this.filterExprUnsupported = false;
4809
5173
  this.filterExprDepth++;
4810
5174
  try {
4811
- return this.renderFilterExprNode(expr, param, localVarMap);
5175
+ return this.renderFilterExprNode(expr, param, localVarMap, datumField);
4812
5176
  } finally {
4813
5177
  this.filterExprDepth--;
4814
5178
  }
4815
5179
  }
4816
- renderFilterExprNode(expr, param, localVarMap) {
5180
+ renderFilterExprNode(expr, param, localVarMap, datumField) {
5181
+ const paramPrefix = datumField ? `.${datumField}` : "";
5182
+ const paramDot = paramPrefix || ".";
4817
5183
  switch (expr.kind) {
4818
5184
  case "identifier": {
4819
5185
  if (expr.name === param) {
4820
- return ".";
5186
+ return paramDot;
4821
5187
  }
4822
5188
  const signal = localVarMap.get(expr.name);
4823
5189
  if (signal) {
@@ -4835,24 +5201,24 @@ ${goFields.join(`
4835
5201
  return String(expr.value);
4836
5202
  case "member": {
4837
5203
  if (expr.object.kind === "identifier" && expr.object.name === param) {
4838
- return `.${capitalizeFieldName(expr.property)}`;
5204
+ return `${paramPrefix}.${capitalizeFieldName(expr.property)}`;
4839
5205
  }
4840
5206
  if (expr.property === "length") {
4841
5207
  const innerHO = this.higherOrderShapeOf(expr.object);
4842
5208
  if (innerHO && innerHO.method === "filter") {
4843
- const lenExpr = this.renderFilterLengthExpr(innerHO, (e) => this.renderFilterExpr(e, param, localVarMap));
5209
+ const lenExpr = this.renderFilterLengthExpr(innerHO, (e) => this.renderFilterExpr(e, param, localVarMap, datumField));
4844
5210
  if (lenExpr)
4845
5211
  return `(${lenExpr})`;
4846
5212
  }
4847
5213
  }
4848
- const obj = this.renderFilterExpr(expr.object, param, localVarMap);
5214
+ const obj = this.renderFilterExpr(expr.object, param, localVarMap, datumField);
4849
5215
  if (this.filterExprUnsupported)
4850
5216
  return "false";
4851
5217
  return `${obj}.${capitalizeFieldName(expr.property)}`;
4852
5218
  }
4853
5219
  case "call": {
4854
5220
  if (expr.callee.kind === "member" && expr.callee.object.kind === "identifier" && expr.callee.object.name === param) {
4855
- return `.${capitalizeFieldName(expr.callee.property)}`;
5221
+ return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`;
4856
5222
  }
4857
5223
  if (expr.callee.kind === "identifier" && expr.args.length === 0) {
4858
5224
  return `$.${capitalizeFieldName(expr.callee.name)}`;
@@ -4860,13 +5226,13 @@ ${goFields.join(`
4860
5226
  if (asCallbackMethodCall3(expr) !== null) {
4861
5227
  return this.refuseFilterExprNode(expr);
4862
5228
  }
4863
- const result = this.renderFilterExpr(expr.callee, param, localVarMap);
5229
+ const result = this.renderFilterExpr(expr.callee, param, localVarMap, datumField);
4864
5230
  if (this.filterExprUnsupported)
4865
5231
  return "false";
4866
5232
  return result;
4867
5233
  }
4868
5234
  case "unary": {
4869
- const arg = this.renderFilterExpr(expr.argument, param, localVarMap);
5235
+ const arg = this.renderFilterExpr(expr.argument, param, localVarMap, datumField);
4870
5236
  if (this.filterExprUnsupported)
4871
5237
  return "false";
4872
5238
  if (expr.op === "!") {
@@ -4879,10 +5245,10 @@ ${goFields.join(`
4879
5245
  return arg;
4880
5246
  }
4881
5247
  case "binary": {
4882
- const left = this.renderFilterExpr(expr.left, param, localVarMap);
5248
+ const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField);
4883
5249
  if (this.filterExprUnsupported)
4884
5250
  return "false";
4885
- const right = this.renderFilterExpr(expr.right, param, localVarMap);
5251
+ const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField);
4886
5252
  if (this.filterExprUnsupported)
4887
5253
  return "false";
4888
5254
  switch (expr.op) {
@@ -4901,7 +5267,7 @@ ${goFields.join(`
4901
5267
  case "<=":
4902
5268
  return `le ${left} ${right}`;
4903
5269
  case "+":
4904
- return `bf_add ${left} ${right}`;
5270
+ return this._emitPlus(expr.left, expr.right, left, right);
4905
5271
  case "-":
4906
5272
  return `bf_sub ${left} ${right}`;
4907
5273
  case "*":
@@ -4913,10 +5279,10 @@ ${goFields.join(`
4913
5279
  }
4914
5280
  }
4915
5281
  case "logical": {
4916
- const left = this.renderFilterExpr(expr.left, param, localVarMap);
5282
+ const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField);
4917
5283
  if (this.filterExprUnsupported)
4918
5284
  return "false";
4919
- const right = this.renderFilterExpr(expr.right, param, localVarMap);
5285
+ const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField);
4920
5286
  if (this.filterExprUnsupported)
4921
5287
  return "false";
4922
5288
  if (expr.op === "&&") {
@@ -4984,11 +5350,22 @@ ${goFields.join(`
4984
5350
  if (trimmed === "null" || trimmed === "undefined") {
4985
5351
  return '""';
4986
5352
  }
5353
+ if (this.staticLoopItemStack.length > 0) {
5354
+ const top = this.staticLoopItemStack[this.staticLoopItemStack.length - 1];
5355
+ const parsedForBake = preParsed ?? parseExpression4(trimmed);
5356
+ const resolved = evaluateStaticLiteral3(parsedForBake, new Map([[top.param, top.item]]));
5357
+ const literal = resolved !== null ? scalarToGoLiteral(resolved.value) : null;
5358
+ if (literal !== null) {
5359
+ return literal;
5360
+ }
5361
+ this.staticLoopBakeFailed = true;
5362
+ return '""';
5363
+ }
4987
5364
  const staticIndexed = this.resolveStaticRecordLiteralIndex(trimmed);
4988
5365
  if (staticIndexed !== null) {
4989
5366
  return staticIndexed;
4990
5367
  }
4991
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
5368
+ if (!this.isLoopShadowedName(trimmed) && /^[A-Za-z_$][\w$]*$/.test(trimmed)) {
4992
5369
  const litConst = (this.state.localConstants ?? []).find((c) => c.name === trimmed);
4993
5370
  if (litConst?.value !== undefined) {
4994
5371
  const v = litConst.value.trim();
@@ -5006,7 +5383,7 @@ ${goFields.join(`
5006
5383
  if (inlined !== null) {
5007
5384
  return this.convertExpressionToGo(stringifyParsedExpr2(inlined), out, inlined);
5008
5385
  }
5009
- const parsed = preParsed ?? parseExpression3(trimmed);
5386
+ const parsed = preParsed ?? parseExpression4(trimmed);
5010
5387
  const support = isSupported(parsed);
5011
5388
  if (!support.supported) {
5012
5389
  this.state.errors.push({
@@ -5024,10 +5401,15 @@ ${goFields.join(`
5024
5401
  out.parsed = parsed;
5025
5402
  return this.renderParsedExpr(parsed);
5026
5403
  }
5404
+ isLoopShadowedName(name) {
5405
+ 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));
5406
+ }
5027
5407
  resolveStaticRecordLiteralIndex(jsExpr) {
5028
5408
  const m = /^([A-Za-z_$][\w$]*)\[\s*(?:'([^']*)'|"([^"]*)")\s*\]$/.exec(jsExpr) ?? /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(jsExpr);
5029
5409
  if (!m)
5030
5410
  return null;
5411
+ if (this.isLoopShadowedName(m[1]))
5412
+ return null;
5031
5413
  const key = m[2] ?? m[3];
5032
5414
  const constInfo = (this.state.localConstants ?? []).find((c) => c.name === m[1] && c.isModule);
5033
5415
  if (constInfo?.value === undefined)
@@ -5133,7 +5515,7 @@ ${goFields.join(`
5133
5515
  }
5134
5516
  convertConditionToGo(jsCondition, preParsed) {
5135
5517
  const trimmed = jsCondition.trim();
5136
- const parsed = preParsed ?? parseExpression3(trimmed);
5518
+ const parsed = preParsed ?? parseExpression4(trimmed);
5137
5519
  const support = isSupported(parsed);
5138
5520
  if (!support.supported) {
5139
5521
  this.state.errors.push({
@@ -5270,7 +5652,7 @@ ${goFields.join(`
5270
5652
  result = `le ${left} ${right}`;
5271
5653
  break;
5272
5654
  case "+":
5273
- result = `bf_add ${left} ${right}`;
5655
+ result = this._emitPlus(expr.left, expr.right, left, right);
5274
5656
  break;
5275
5657
  case "-":
5276
5658
  result = `bf_sub ${left} ${right}`;
@@ -5354,6 +5736,29 @@ ${goFields.join(`
5354
5736
  }
5355
5737
  return;
5356
5738
  }
5739
+ wrapperDatumField(loop) {
5740
+ if (!loop.childComponent)
5741
+ return null;
5742
+ for (const prop of loop.childComponent.props) {
5743
+ if (prop.isEventHandler)
5744
+ continue;
5745
+ if (prop.value.kind !== "expression")
5746
+ continue;
5747
+ const parsed = prop.value.parsed;
5748
+ const isBareParamRef = parsed ? parsed.kind === "identifier" && parsed.name === loop.param : prop.value.expr.trim() === loop.param;
5749
+ if (isBareParamRef)
5750
+ return capitalizeFieldName(prop.name);
5751
+ }
5752
+ return null;
5753
+ }
5754
+ getBakedStaticChildLoop(markerId, childComponent, arrayParsed, param, key) {
5755
+ if (this.bakedStaticChildLoopCache.has(markerId)) {
5756
+ return this.bakedStaticChildLoopCache.get(markerId) ?? null;
5757
+ }
5758
+ const result = analyzeBakeableStaticChildLoop({ props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key }, this.state.localConstants, { isNameShadowed: (name) => this.state.staticLoopSourceBoundNames.has(name) });
5759
+ this.bakedStaticChildLoopCache.set(markerId, result);
5760
+ return result;
5761
+ }
5357
5762
  renderLoop(loop) {
5358
5763
  if (loop.clientOnly) {
5359
5764
  return `{{bfComment "loop:${loop.markerId}"}}{{bfComment "/loop:${loop.markerId}"}}`;
@@ -5374,8 +5779,13 @@ ${goFields.join(`
5374
5779
  }
5375
5780
  });
5376
5781
  }
5782
+ const bakedChildLoop = loop.childComponent ? this.getBakedStaticChildLoop(loop.markerId, loop.childComponent, loop.arrayParsed, loop.param, loop.key ?? undefined) : null;
5783
+ const bakedElementLoop = loop.childComponent ? null : analyzeBakeableStaticElementLoop(loop, this.state.localConstants, { isNameShadowed: (name) => this.state.staticLoopSourceBoundNames.has(name) });
5784
+ if (bakedElementLoop) {
5785
+ return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items);
5786
+ }
5377
5787
  const arrayName = loop.array.trim();
5378
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
5788
+ if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
5379
5789
  const arrayConst = this.state.localConstants.find((c) => c.name === arrayName);
5380
5790
  if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set)) {
5381
5791
  this.state.errors.push({
@@ -5389,7 +5799,7 @@ ${goFields.join(`
5389
5799
  });
5390
5800
  }
5391
5801
  }
5392
- let goArray = this.convertExpressionToGo(loop.array);
5802
+ let goArray = loop.childComponent ? "" : this.convertExpressionToGo(loop.array);
5393
5803
  const param = loop.param;
5394
5804
  let index = loop.index || "_";
5395
5805
  let rangeIndex = index;
@@ -5427,7 +5837,9 @@ ${goFields.join(`
5427
5837
  }
5428
5838
  this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null);
5429
5839
  this.loopWrapperStack.push(!!loop.childComponent);
5840
+ this.loopKeyDepthStack.push(loop.depth);
5430
5841
  const children = this.renderChildren(loop.children);
5842
+ this.loopKeyDepthStack.pop();
5431
5843
  this.loopWrapperStack.pop();
5432
5844
  this.loopScalarItemStack.pop();
5433
5845
  const itemMarker = this.loopItemMarker(loop);
@@ -5455,7 +5867,8 @@ ${goFields.join(`
5455
5867
  if (loop.filterPredicate) {
5456
5868
  let filterCond;
5457
5869
  if (loop.filterPredicate.predicate) {
5458
- filterCond = this.renderPredicateCondition(loop.filterPredicate.predicate, loop.filterPredicate.param);
5870
+ const datumField = this.wrapperDatumField(loop);
5871
+ filterCond = this.renderPredicateCondition(loop.filterPredicate.predicate, loop.filterPredicate.param, datumField);
5459
5872
  } else {
5460
5873
  filterCond = "true";
5461
5874
  }
@@ -5463,6 +5876,38 @@ ${goFields.join(`
5463
5876
  }
5464
5877
  return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`;
5465
5878
  }
5879
+ renderUnrolledStaticElementLoop(loop, items) {
5880
+ this.inLoop = true;
5881
+ this.loopWrapperStack.push(false);
5882
+ this.loopKeyDepthStack.push(loop.depth);
5883
+ this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null);
5884
+ this.loopParamStack.push(loop.param);
5885
+ let body = "";
5886
+ for (const item of items) {
5887
+ this.staticLoopItemStack.push({ param: loop.param, item });
5888
+ body += this.renderChildren(loop.children);
5889
+ this.staticLoopItemStack.pop();
5890
+ if (this.staticLoopBakeFailed) {
5891
+ this.staticLoopBakeFailed = false;
5892
+ this.state.errors.push({
5893
+ code: "BF101",
5894
+ severity: "error",
5895
+ 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.`,
5896
+ loc: loop.loc ?? this.makeLoc(),
5897
+ suggestion: {
5898
+ 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."
5899
+ }
5900
+ });
5901
+ break;
5902
+ }
5903
+ }
5904
+ this.loopParamStack.pop();
5905
+ this.loopScalarItemStack.pop();
5906
+ this.loopKeyDepthStack.pop();
5907
+ this.loopWrapperStack.pop();
5908
+ this.inLoop = false;
5909
+ return `{{bfComment "loop:${loop.markerId}"}}${body}{{bfComment "/loop:${loop.markerId}"}}`;
5910
+ }
5466
5911
  loopItemMarker(loop) {
5467
5912
  if (loop.bodyIsMultiRoot)
5468
5913
  return `{{bfComment "bf-loop-i"}}`;
@@ -5570,7 +6015,7 @@ ${goFields.join(`
5570
6015
  ${children}`;
5571
6016
  }
5572
6017
  elementAttrEmitter = {
5573
- emitLiteral: (value, name) => `${name}="${value.value}"`,
6018
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
5574
6019
  emitExpression: (value, name) => {
5575
6020
  if (name === "style") {
5576
6021
  const css = this.tryLowerStyleObject(value.expr);
@@ -5582,7 +6027,7 @@ ${children}`;
5582
6027
  const body = name.startsWith("aria-") ? `${name}="true"` : name;
5583
6028
  return `${preamble}{{if ${goCond}}}${body}{{end}}`;
5584
6029
  }
5585
- const parsed = value.parsed ?? parseExpression3(value.expr.trim());
6030
+ const parsed = value.parsed ?? parseExpression4(value.expr.trim());
5586
6031
  if (parsed.kind === "conditional") {
5587
6032
  const undef = (e) => e.kind === "identifier" && (e.name === "undefined" || e.name === "null") || e.kind === "literal" && (e.value === null || e.value === undefined);
5588
6033
  const test = parsed.test;
@@ -5647,7 +6092,7 @@ ${children}`;
5647
6092
  if (!entries)
5648
6093
  return null;
5649
6094
  for (const e of entries) {
5650
- if (e.kind === "expr" && !isSupported(parseExpression3(e.expr)).supported)
6095
+ if (e.kind === "expr" && !isSupported(parseExpression4(e.expr)).supported)
5651
6096
  return null;
5652
6097
  }
5653
6098
  return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:{{${this.convertExpressionToGo(e.expr)}}}`).join(";");
@@ -5657,12 +6102,15 @@ ${children}`;
5657
6102
  for (const attr of element.attrs) {
5658
6103
  if (attr.clientOnly)
5659
6104
  continue;
6105
+ if (isDangerousInnerHtmlAttr(attr))
6106
+ continue;
5660
6107
  let attrName;
5661
6108
  if (attr.name === "className")
5662
6109
  attrName = "class";
5663
- else if (attr.name === "key")
5664
- attrName = "data-key";
5665
- else
6110
+ else if (attr.name === "key") {
6111
+ const depth = this.loopKeyDepthStack.at(-1) ?? 0;
6112
+ attrName = depth > 0 ? `data-key-${depth}` : "data-key";
6113
+ } else
5666
6114
  attrName = attr.name;
5667
6115
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
5668
6116
  if (lowered)