@barefootjs/go-template 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/go-template-adapter.d.ts +16 -9
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +158 -90
- package/dist/build.js +158 -90
- package/dist/index.js +158 -90
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +145 -10
- package/src/adapter/go-template-adapter.ts +191 -144
|
@@ -71,6 +71,7 @@ type GoRenderCtx = {
|
|
|
71
71
|
*/
|
|
72
72
|
interface NestedComponentInfo extends IRLoopChildComponent {
|
|
73
73
|
isDynamic: boolean
|
|
74
|
+
isPropDerived: boolean
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
interface StaticChildInstance {
|
|
@@ -298,6 +299,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
298
299
|
private options: Required<GoTemplateAdapterOptions>
|
|
299
300
|
private inLoop: boolean = false
|
|
300
301
|
private loopParamStack: string[] = []
|
|
302
|
+
private loopVarRefCount: Map<string, number> = new Map()
|
|
301
303
|
private errors: CompilerError[] = []
|
|
302
304
|
private propsObjectName: string | null = null
|
|
303
305
|
/**
|
|
@@ -308,6 +310,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
308
310
|
* follow-up).
|
|
309
311
|
*/
|
|
310
312
|
private restPropsName: string | null = null
|
|
313
|
+
private templateVarCounter: number = 0
|
|
311
314
|
/** Local type names resolved from typeDefinitions (populated during generateTypes) */
|
|
312
315
|
private localTypeNames: Set<string> = new Set()
|
|
313
316
|
/** Local type aliases mapping type name to base type (e.g., Filter → 'string') */
|
|
@@ -334,6 +337,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
334
337
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
|
|
335
338
|
this.componentName = ir.metadata.componentName
|
|
336
339
|
this.errors = []
|
|
340
|
+
this.templateVarCounter = 0
|
|
337
341
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
338
342
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
339
343
|
|
|
@@ -807,8 +811,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
807
811
|
lines.push('\tBfParent string // Optional: parent scope id')
|
|
808
812
|
lines.push('\tBfMount string // Optional: slot id in parent')
|
|
809
813
|
|
|
810
|
-
// Static nested components appear in Input;
|
|
811
|
-
|
|
814
|
+
// Static + prop-derived nested components appear in Input;
|
|
815
|
+
// signal-backed dynamic ones are template-only
|
|
816
|
+
const inputNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
812
817
|
|
|
813
818
|
// Collect nested component array field names to skip from propsParams
|
|
814
819
|
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
@@ -821,8 +826,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
821
826
|
lines.push(`\t${fieldName} ${goType}`)
|
|
822
827
|
}
|
|
823
828
|
|
|
824
|
-
// Add nested component input arrays
|
|
825
|
-
for (const nested of
|
|
829
|
+
// Add nested component input arrays
|
|
830
|
+
for (const nested of inputNested) {
|
|
826
831
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
827
832
|
}
|
|
828
833
|
|
|
@@ -953,10 +958,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
953
958
|
|
|
954
959
|
// Add array fields for nested components (for template rendering)
|
|
955
960
|
for (const nested of nestedComponents) {
|
|
956
|
-
if (nested.isDynamic) {
|
|
957
|
-
// Dynamic
|
|
961
|
+
if (nested.isDynamic && !nested.isPropDerived) {
|
|
962
|
+
// Dynamic signal array loops: template-only, not in JSON
|
|
958
963
|
lines.push(`\t${nested.name}s []${nested.name}Props \`json:"-"\``)
|
|
959
964
|
} else {
|
|
965
|
+
// Static arrays and prop-derived dynamic arrays: include in JSON
|
|
966
|
+
// so the client can hydrate via mapArray or forEach
|
|
960
967
|
const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
|
|
961
968
|
lines.push(`\t${nested.name}s []${nested.name}Props \`json:"${jsonTag}"\``)
|
|
962
969
|
}
|
|
@@ -1005,9 +1012,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1005
1012
|
// field, the SSR template iterates over it, but
|
|
1006
1013
|
// `NewTodoAppProps(TodoAppInput{Initial: ...})` returns it empty
|
|
1007
1014
|
// and the page renders a blank list (#1442 echo TodoApp repro).
|
|
1008
|
-
const
|
|
1015
|
+
const signalDynamicNested = nestedComponents.filter(n => n.isDynamic && !n.isPropDerived)
|
|
1009
1016
|
lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
|
|
1010
|
-
for (const nested of
|
|
1017
|
+
for (const nested of signalDynamicNested) {
|
|
1011
1018
|
const arrayField = `${nested.name}s`
|
|
1012
1019
|
lines.push(`//`)
|
|
1013
1020
|
lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`)
|
|
@@ -1030,8 +1037,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1030
1037
|
lines.push('\t}')
|
|
1031
1038
|
lines.push('')
|
|
1032
1039
|
|
|
1033
|
-
// Static nested components
|
|
1034
|
-
|
|
1040
|
+
// Static + prop-derived nested components: auto-populate from input.
|
|
1041
|
+
// Signal-backed dynamic arrays are set manually by the handler.
|
|
1042
|
+
const staticNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
1035
1043
|
|
|
1036
1044
|
// Handle nested components
|
|
1037
1045
|
if (staticNested.length > 0) {
|
|
@@ -1266,6 +1274,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1266
1274
|
result.push({
|
|
1267
1275
|
...loop.childComponent,
|
|
1268
1276
|
isDynamic: !loop.isStaticArray,
|
|
1277
|
+
isPropDerived: !!loop.isPropDerivedArray,
|
|
1269
1278
|
})
|
|
1270
1279
|
}
|
|
1271
1280
|
}
|
|
@@ -2466,12 +2475,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2466
2475
|
// ===========================================================================
|
|
2467
2476
|
|
|
2468
2477
|
identifier(name: string): string {
|
|
2478
|
+
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
2479
|
+
if (currentLoopParam && name === currentLoopParam) return '.'
|
|
2480
|
+
if (this.loopVarRefCount.has(name)) return `$${name}`
|
|
2469
2481
|
return `.${this.capitalizeFieldName(name)}`
|
|
2470
2482
|
}
|
|
2471
2483
|
|
|
2472
2484
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2473
2485
|
if (literalType === 'string') return `"${value}"`
|
|
2474
|
-
if (literalType === 'null') return '
|
|
2486
|
+
if (literalType === 'null') return 'nil'
|
|
2475
2487
|
return String(value)
|
|
2476
2488
|
}
|
|
2477
2489
|
|
|
@@ -2530,8 +2542,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2530
2542
|
if (result) return result
|
|
2531
2543
|
}
|
|
2532
2544
|
|
|
2533
|
-
// find().property → {{with bf_find ...}}{{.Property}}{{end}}
|
|
2534
|
-
if (object.kind === 'higher-order' && object.method === 'find') {
|
|
2545
|
+
// find().property / findLast().property → {{with bf_find ...}}{{.Property}}{{end}}
|
|
2546
|
+
if (object.kind === 'higher-order' && (object.method === 'find' || object.method === 'findLast')) {
|
|
2535
2547
|
const findResult = this.renderHigherOrderExpr(object, emit)
|
|
2536
2548
|
if (findResult) {
|
|
2537
2549
|
return `{{with ${findResult}}}{{.${this.capitalizeFieldName(property)}}}{{end}}`
|
|
@@ -2613,6 +2625,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2613
2625
|
return `or ${wrapLeft} ${wrapRight}`
|
|
2614
2626
|
}
|
|
2615
2627
|
|
|
2628
|
+
// Note: JSX-level ternaries (`{expr ? a : b}`) are handled at the
|
|
2629
|
+
// IR level as IRConditional, which goes through convertConditionToGo
|
|
2630
|
+
// → renderConditionExpr (preamble-aware). This emitter method is
|
|
2631
|
+
// only reached for ternaries nested inside other ParsedExpr trees
|
|
2632
|
+
// (e.g. template-literal interpolation), where the test is always a
|
|
2633
|
+
// simple pipeline expression (runtime helpers, not template blocks).
|
|
2616
2634
|
conditional(
|
|
2617
2635
|
test: ParsedExpr,
|
|
2618
2636
|
consequent: ParsedExpr,
|
|
@@ -2620,8 +2638,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2620
2638
|
emit: (e: ParsedExpr) => string,
|
|
2621
2639
|
): string {
|
|
2622
2640
|
const t = emit(test)
|
|
2623
|
-
// Nested conditionals already return complete {{if}}...{{end}} blocks;
|
|
2624
|
-
// literals return bare text (used within attributes).
|
|
2625
2641
|
const c = this.renderConditionalBranch(consequent)
|
|
2626
2642
|
const a = this.renderConditionalBranch(alternate)
|
|
2627
2643
|
return `{{if ${t}}}${c}{{else}}${a}{{end}}`
|
|
@@ -2672,7 +2688,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2672
2688
|
const reconstructed = { kind: 'higher-order' as const, method, object, param, predicate }
|
|
2673
2689
|
const result = this.renderHigherOrderExpr(reconstructed, emit)
|
|
2674
2690
|
if (result) return result
|
|
2675
|
-
if (method === 'find' || method === 'findIndex') {
|
|
2691
|
+
if (method === 'find' || method === 'findIndex' || method === 'findLast' || method === 'findLastIndex') {
|
|
2676
2692
|
const templateBlock = this.renderFindTemplateBlock(reconstructed, emit)
|
|
2677
2693
|
if (templateBlock) return templateBlock
|
|
2678
2694
|
}
|
|
@@ -2941,26 +2957,29 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2941
2957
|
return `bf_filter ${arrayExpr} "${field}" ${value}`
|
|
2942
2958
|
}
|
|
2943
2959
|
|
|
2944
|
-
if (expr.method === 'find' || expr.method === 'findIndex') {
|
|
2960
|
+
if (expr.method === 'find' || expr.method === 'findIndex' || expr.method === 'findLast' || expr.method === 'findLastIndex') {
|
|
2945
2961
|
const eqPred = this.extractEqualityPredicate(
|
|
2946
2962
|
expr.predicate, expr.param, e => this.renderParsedExpr(e)
|
|
2947
2963
|
)
|
|
2948
2964
|
if (!eqPred) return null
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2965
|
+
const funcMap: Record<string, string> = {
|
|
2966
|
+
find: 'bf_find', findIndex: 'bf_find_index',
|
|
2967
|
+
findLast: 'bf_find_last', findLastIndex: 'bf_find_last_index',
|
|
2968
|
+
}
|
|
2969
|
+
return `${funcMap[expr.method]} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`
|
|
2951
2970
|
}
|
|
2952
2971
|
|
|
2953
2972
|
return null
|
|
2954
2973
|
}
|
|
2955
2974
|
|
|
2956
2975
|
/**
|
|
2957
|
-
* Render find
|
|
2958
|
-
* Falls back from bf_find/
|
|
2959
|
-
*
|
|
2976
|
+
* Render find/findIndex/findLast/findLastIndex with complex predicates
|
|
2977
|
+
* using range/if blocks. Falls back from bf_find/bf_find_last helpers
|
|
2978
|
+
* when extractEqualityPredicate returns null.
|
|
2960
2979
|
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
*
|
|
2980
|
+
* find/findIndex use break on first match (forward scan).
|
|
2981
|
+
* findLast/findLastIndex iterate forward and keep overwriting a result
|
|
2982
|
+
* variable; the final value is the last match.
|
|
2964
2983
|
*/
|
|
2965
2984
|
private renderFindTemplateBlock(
|
|
2966
2985
|
expr: Extract<ParsedExpr, { kind: 'higher-order' }>,
|
|
@@ -2980,6 +2999,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2980
2999
|
return `{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{$i}}{{break}}{{end}}{{end}}`
|
|
2981
3000
|
}
|
|
2982
3001
|
|
|
3002
|
+
if (expr.method === 'findLast') {
|
|
3003
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3004
|
+
const capture = propertyAccess ? `.${propertyAccess}` : '.'
|
|
3005
|
+
return `{{${v} := ""}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = ${capture}}}{{end}}{{end}}{{${v}}}`
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
if (expr.method === 'findLastIndex') {
|
|
3009
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3010
|
+
return `{{${v} := -1}}{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{${v} = $i}}{{end}}{{end}}{{${v}}}`
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2983
3013
|
return null
|
|
2984
3014
|
}
|
|
2985
3015
|
|
|
@@ -3003,13 +3033,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3003
3033
|
if (condition.includes('[UNSUPPORTED')) return null
|
|
3004
3034
|
|
|
3005
3035
|
if (expr.method === 'every') {
|
|
3006
|
-
|
|
3036
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3007
3037
|
const negated = this.negateGoCondition(condition)
|
|
3008
|
-
return `{{$
|
|
3038
|
+
return `{{${v} := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{${v} = false}}{{break}}{{end}}{{end}}{{${v}}}`
|
|
3009
3039
|
}
|
|
3010
3040
|
|
|
3011
3041
|
if (expr.method === 'some') {
|
|
3012
|
-
|
|
3042
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3043
|
+
return `{{${v} := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = true}}{{break}}{{end}}{{end}}{{${v}}}`
|
|
3013
3044
|
}
|
|
3014
3045
|
|
|
3015
3046
|
return null
|
|
@@ -3068,6 +3099,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3068
3099
|
return expr.kind === 'logical' || expr.kind === 'unary' || expr.kind === 'conditional'
|
|
3069
3100
|
}
|
|
3070
3101
|
|
|
3102
|
+
/**
|
|
3103
|
+
* Split a rendered template block into preamble + final expression.
|
|
3104
|
+
* The last `{{...}}` must be a variable reference (`$bf_rN` or
|
|
3105
|
+
* `$bf_result`). Control tokens like `{{end}}` or `{{break}}` are
|
|
3106
|
+
* rejected — those template blocks (e.g. find's range/break form)
|
|
3107
|
+
* can't be composed in binary/logical expressions.
|
|
3108
|
+
*/
|
|
3109
|
+
private splitPreamble(rendered: string): { preamble: string; expr: string } | null {
|
|
3110
|
+
if (!rendered.includes('{{')) return null
|
|
3111
|
+
const lastOpen = rendered.lastIndexOf('{{')
|
|
3112
|
+
const lastClose = rendered.lastIndexOf('}}')
|
|
3113
|
+
if (lastOpen >= 0 && lastClose > lastOpen) {
|
|
3114
|
+
const candidate = rendered.substring(lastOpen + 2, lastClose)
|
|
3115
|
+
if (!candidate.startsWith('$')) return null
|
|
3116
|
+
return {
|
|
3117
|
+
preamble: rendered.substring(0, lastOpen),
|
|
3118
|
+
expr: candidate,
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
return null
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3071
3124
|
// =============================================================================
|
|
3072
3125
|
// Block Body Condition Rendering
|
|
3073
3126
|
// =============================================================================
|
|
@@ -3310,7 +3363,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3310
3363
|
return `"${expr.value}"`
|
|
3311
3364
|
}
|
|
3312
3365
|
if (expr.literalType === 'null') {
|
|
3313
|
-
return '
|
|
3366
|
+
return 'nil'
|
|
3314
3367
|
}
|
|
3315
3368
|
return String(expr.value)
|
|
3316
3369
|
|
|
@@ -3682,196 +3735,152 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3682
3735
|
return { condition: `false`, preamble: '' }
|
|
3683
3736
|
}
|
|
3684
3737
|
|
|
3685
|
-
const
|
|
3686
|
-
|
|
3687
|
-
// Detect template blocks (e.g., from every/some with complex predicates).
|
|
3688
|
-
// These cannot be placed inside {{if ...}} directly.
|
|
3689
|
-
// Split into preamble (template block) + condition variable.
|
|
3690
|
-
if (rendered.startsWith('{{')) {
|
|
3691
|
-
const lastOpen = rendered.lastIndexOf('{{')
|
|
3692
|
-
const lastClose = rendered.lastIndexOf('}}')
|
|
3693
|
-
if (lastOpen >= 0 && lastClose > lastOpen) {
|
|
3694
|
-
const preamble = rendered.substring(0, lastOpen)
|
|
3695
|
-
const condition = rendered.substring(lastOpen + 2, lastClose)
|
|
3696
|
-
return { condition, preamble }
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
return { condition: rendered, preamble: '' }
|
|
3738
|
+
const { preamble, expr: condition } = this.renderConditionExpr(parsed)
|
|
3739
|
+
return { condition, preamble }
|
|
3701
3740
|
}
|
|
3702
3741
|
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
private renderConditionExpr(expr: ParsedExpr): string {
|
|
3742
|
+
private renderConditionExpr(expr: ParsedExpr): { preamble: string; expr: string } {
|
|
3743
|
+
const plain = (e: string) => ({ preamble: '', expr: e })
|
|
3744
|
+
|
|
3707
3745
|
switch (expr.kind) {
|
|
3708
3746
|
case 'identifier':
|
|
3709
|
-
// Inside a `{{range $_, $todo := .Todos}}` loop, a bare reference
|
|
3710
|
-
// to the loop variable (`todo`) is just Go template's dot. The
|
|
3711
|
-
// `ParsedExprEmitter` path already handles this at memberAccess
|
|
3712
|
-
// (line ~2449); this condition-expression path needs the same
|
|
3713
|
-
// normalization or `todo.done` ends up as `.Todo.Done` — a
|
|
3714
|
-
// non-existent field that Go template silently expands to ""
|
|
3715
|
-
// and then aborts the surrounding `{{if}}`/template execution
|
|
3716
|
-
// (echo logs it as a 200 with truncated bytes; #1442 repro).
|
|
3717
3747
|
{
|
|
3718
3748
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
3719
3749
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
3720
|
-
return '.'
|
|
3750
|
+
return plain('.')
|
|
3751
|
+
}
|
|
3752
|
+
if (this.loopVarRefCount.has(expr.name)) {
|
|
3753
|
+
return plain(`$${expr.name}`)
|
|
3721
3754
|
}
|
|
3722
3755
|
}
|
|
3723
|
-
return `.${this.capitalizeFieldName(expr.name)}`
|
|
3756
|
+
return plain(`.${this.capitalizeFieldName(expr.name)}`)
|
|
3724
3757
|
|
|
3725
3758
|
case 'literal':
|
|
3726
|
-
if (expr.literalType === 'string') {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
if (expr.literalType === 'null') {
|
|
3730
|
-
return '""'
|
|
3731
|
-
}
|
|
3732
|
-
return String(expr.value)
|
|
3759
|
+
if (expr.literalType === 'string') return plain(`"${expr.value}"`)
|
|
3760
|
+
if (expr.literalType === 'null') return plain('nil')
|
|
3761
|
+
return plain(String(expr.value))
|
|
3733
3762
|
|
|
3734
3763
|
case 'call': {
|
|
3735
|
-
// Signal call: count() -> .Count
|
|
3736
3764
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
3737
|
-
return `.${this.capitalizeFieldName(expr.callee.name)}`
|
|
3765
|
+
return plain(`.${this.capitalizeFieldName(expr.callee.name)}`)
|
|
3738
3766
|
}
|
|
3739
|
-
return this.renderParsedExpr(expr)
|
|
3767
|
+
return plain(this.renderParsedExpr(expr))
|
|
3740
3768
|
}
|
|
3741
3769
|
|
|
3742
3770
|
case 'member': {
|
|
3743
|
-
// Handle .length with higher-order filter → len (bf_filter ...)
|
|
3744
3771
|
if (expr.property === 'length' && expr.object.kind === 'higher-order') {
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3772
|
+
// renderFilterLengthExpr uses bf_filter runtime helpers (not
|
|
3773
|
+
// template blocks), so .preamble is always empty here today.
|
|
3774
|
+
// If a future higher-order method produces preambles through
|
|
3775
|
+
// this path, the callback would need to propagate them.
|
|
3776
|
+
const result = this.renderFilterLengthExpr(expr.object, e => this.renderConditionExpr(e).expr)
|
|
3777
|
+
if (result) return plain(result)
|
|
3749
3778
|
}
|
|
3750
3779
|
|
|
3751
|
-
// Handle SolidJS-style props pattern: props.xxx -> .Xxx
|
|
3752
3780
|
if (expr.object.kind === 'identifier' && this.propsObjectName && expr.object.name === this.propsObjectName) {
|
|
3753
|
-
return `.${this.capitalizeFieldName(expr.property)}`
|
|
3781
|
+
return plain(`.${this.capitalizeFieldName(expr.property)}`)
|
|
3754
3782
|
}
|
|
3755
3783
|
|
|
3756
|
-
// Loop-param member access: `todo.done` inside
|
|
3757
|
-
// `{{range $_, $todo := .Todos}}` is `.Done` (Go template's dot
|
|
3758
|
-
// is the current item). The `ParsedExprEmitter` already does
|
|
3759
|
-
// this for renderParsedExpr; mirror it here so condition-only
|
|
3760
|
-
// positions like boolean attributes (`checked={todo.done}`)
|
|
3761
|
-
// and `{{if}}` operands don't fall through to the generic
|
|
3762
|
-
// `.Todo.Done` shape, which references a non-existent field.
|
|
3763
3784
|
{
|
|
3764
3785
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
3765
3786
|
if (expr.object.kind === 'identifier' && currentLoopParam && expr.object.name === currentLoopParam) {
|
|
3766
|
-
return `.${this.capitalizeFieldName(expr.property)}`
|
|
3787
|
+
return plain(`.${this.capitalizeFieldName(expr.property)}`)
|
|
3767
3788
|
}
|
|
3768
3789
|
}
|
|
3769
3790
|
|
|
3770
3791
|
const obj = this.renderConditionExpr(expr.object)
|
|
3771
3792
|
if (expr.property === 'length') {
|
|
3772
|
-
return `len ${obj}`
|
|
3793
|
+
return { preamble: obj.preamble, expr: `len ${obj.expr}` }
|
|
3773
3794
|
}
|
|
3774
|
-
return `${obj}.${this.capitalizeFieldName(expr.property)}`
|
|
3795
|
+
return { preamble: obj.preamble, expr: `${obj.expr}.${this.capitalizeFieldName(expr.property)}` }
|
|
3775
3796
|
}
|
|
3776
3797
|
|
|
3777
3798
|
case 'binary': {
|
|
3778
|
-
// Check if left operand needs parentheses (e.g., function calls in Go template)
|
|
3779
3799
|
const leftNeedsParens = this.needsParensInGoTemplate(expr.left)
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
left = `(${left})`
|
|
3783
|
-
}
|
|
3800
|
+
const leftResult = this.renderConditionExpr(expr.left)
|
|
3801
|
+
const left = leftNeedsParens ? `(${leftResult.expr})` : leftResult.expr
|
|
3784
3802
|
|
|
3785
3803
|
const rightNeedsParens = this.needsParensInGoTemplate(expr.right)
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3804
|
+
const rightResult = this.renderConditionExpr(expr.right)
|
|
3805
|
+
const right = rightNeedsParens ? `(${rightResult.expr})` : rightResult.expr
|
|
3806
|
+
|
|
3807
|
+
const preamble = leftResult.preamble + rightResult.preamble
|
|
3790
3808
|
|
|
3809
|
+
let result: string
|
|
3791
3810
|
switch (expr.op) {
|
|
3792
3811
|
case '===':
|
|
3793
3812
|
case '==':
|
|
3794
|
-
|
|
3813
|
+
result = `eq ${left} ${right}`; break
|
|
3795
3814
|
case '!==':
|
|
3796
3815
|
case '!=':
|
|
3797
|
-
|
|
3816
|
+
result = `ne ${left} ${right}`; break
|
|
3798
3817
|
case '>':
|
|
3799
|
-
|
|
3818
|
+
result = `gt ${left} ${right}`; break
|
|
3800
3819
|
case '<':
|
|
3801
|
-
|
|
3820
|
+
result = `lt ${left} ${right}`; break
|
|
3802
3821
|
case '>=':
|
|
3803
|
-
|
|
3822
|
+
result = `ge ${left} ${right}`; break
|
|
3804
3823
|
case '<=':
|
|
3805
|
-
|
|
3806
|
-
// Arithmetic in conditions
|
|
3824
|
+
result = `le ${left} ${right}`; break
|
|
3807
3825
|
case '+':
|
|
3808
|
-
|
|
3826
|
+
result = `bf_add ${left} ${right}`; break
|
|
3809
3827
|
case '-':
|
|
3810
|
-
|
|
3828
|
+
result = `bf_sub ${left} ${right}`; break
|
|
3811
3829
|
case '*':
|
|
3812
|
-
|
|
3830
|
+
result = `bf_mul ${left} ${right}`; break
|
|
3813
3831
|
case '/':
|
|
3814
|
-
|
|
3832
|
+
result = `bf_div ${left} ${right}`; break
|
|
3815
3833
|
default:
|
|
3816
|
-
|
|
3834
|
+
result = `${left} ${expr.op} ${right}`
|
|
3817
3835
|
}
|
|
3836
|
+
return { preamble, expr: result }
|
|
3818
3837
|
}
|
|
3819
3838
|
|
|
3820
3839
|
case 'unary': {
|
|
3821
3840
|
const arg = this.renderConditionExpr(expr.argument)
|
|
3822
|
-
if (expr.op === '!') {
|
|
3823
|
-
|
|
3824
|
-
}
|
|
3825
|
-
if (expr.op === '-') {
|
|
3826
|
-
return `bf_neg ${arg}`
|
|
3827
|
-
}
|
|
3841
|
+
if (expr.op === '!') return { preamble: arg.preamble, expr: `not ${arg.expr}` }
|
|
3842
|
+
if (expr.op === '-') return { preamble: arg.preamble, expr: `bf_neg ${arg.expr}` }
|
|
3828
3843
|
return arg
|
|
3829
3844
|
}
|
|
3830
3845
|
|
|
3831
3846
|
case 'logical': {
|
|
3832
|
-
const
|
|
3833
|
-
const
|
|
3834
|
-
|
|
3835
|
-
const wrapLeft = this.needsParens(expr.left) ? `(${
|
|
3836
|
-
const wrapRight = this.needsParens(expr.right) ? `(${
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
return
|
|
3847
|
+
const leftResult = this.renderConditionExpr(expr.left)
|
|
3848
|
+
const rightResult = this.renderConditionExpr(expr.right)
|
|
3849
|
+
const preamble = leftResult.preamble + rightResult.preamble
|
|
3850
|
+
const wrapLeft = this.needsParens(expr.left) ? `(${leftResult.expr})` : leftResult.expr
|
|
3851
|
+
const wrapRight = this.needsParens(expr.right) ? `(${rightResult.expr})` : rightResult.expr
|
|
3852
|
+
const result = expr.op === '&&'
|
|
3853
|
+
? `and ${wrapLeft} ${wrapRight}`
|
|
3854
|
+
: `or ${wrapLeft} ${wrapRight}`
|
|
3855
|
+
return { preamble, expr: result }
|
|
3841
3856
|
}
|
|
3842
3857
|
|
|
3843
3858
|
case 'conditional': {
|
|
3844
|
-
// Ternary in condition: (cond ? a : b) is unusual but handle it
|
|
3845
3859
|
const test = this.renderConditionExpr(expr.test)
|
|
3846
|
-
return test
|
|
3860
|
+
return test
|
|
3847
3861
|
}
|
|
3848
3862
|
|
|
3849
3863
|
case 'template-literal':
|
|
3850
|
-
|
|
3851
|
-
return this.renderParsedExpr(expr)
|
|
3864
|
+
return plain(this.renderParsedExpr(expr))
|
|
3852
3865
|
|
|
3853
3866
|
case 'arrow-fn':
|
|
3854
|
-
|
|
3855
|
-
return '[ARROW-FN]'
|
|
3867
|
+
return plain('[ARROW-FN]')
|
|
3856
3868
|
|
|
3857
|
-
case 'higher-order':
|
|
3858
|
-
|
|
3859
|
-
|
|
3869
|
+
case 'higher-order': {
|
|
3870
|
+
const rendered = this.renderParsedExpr(expr)
|
|
3871
|
+
const split = this.splitPreamble(rendered)
|
|
3872
|
+
if (split) return split
|
|
3873
|
+
return plain(rendered)
|
|
3874
|
+
}
|
|
3860
3875
|
|
|
3861
3876
|
case 'array-literal':
|
|
3862
|
-
|
|
3863
|
-
// delegate to renderParsedExpr so the `arrayLiteral` BF101
|
|
3864
|
-
// gate fires consistently with non-condition positions.
|
|
3865
|
-
return this.renderParsedExpr(expr)
|
|
3877
|
+
return plain(this.renderParsedExpr(expr))
|
|
3866
3878
|
|
|
3867
3879
|
case 'array-method':
|
|
3868
|
-
|
|
3869
|
-
// refusal diagnostic at one site rather than duplicating it
|
|
3870
|
-
// for condition-position emission.
|
|
3871
|
-
return this.renderParsedExpr(expr)
|
|
3880
|
+
return plain(this.renderParsedExpr(expr))
|
|
3872
3881
|
|
|
3873
3882
|
case 'unsupported':
|
|
3874
|
-
return expr.raw
|
|
3883
|
+
return plain(expr.raw)
|
|
3875
3884
|
}
|
|
3876
3885
|
}
|
|
3877
3886
|
|
|
@@ -3914,7 +3923,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3914
3923
|
|
|
3915
3924
|
let goArray = this.convertExpressionToGo(loop.array)
|
|
3916
3925
|
const param = loop.param
|
|
3917
|
-
|
|
3926
|
+
let index = loop.index || '_'
|
|
3927
|
+
|
|
3928
|
+
// `.keys().map(k => ...)` — the callback param is the *index*, not
|
|
3929
|
+
// the value. Swap into the Go range's first binding slot so
|
|
3930
|
+
// `{{range $k, $_ := .Arr}}` makes `$k` the 0-based index.
|
|
3931
|
+
let rangeIndex = index
|
|
3932
|
+
let rangeValue = param
|
|
3933
|
+
if (loop.iterationShape === 'keys') {
|
|
3934
|
+
rangeIndex = param
|
|
3935
|
+
rangeValue = '_'
|
|
3936
|
+
}
|
|
3918
3937
|
|
|
3919
3938
|
// Check if the loop contains a component child
|
|
3920
3939
|
// If so, use .{ComponentName}s which has ScopeID for each item
|
|
@@ -3925,8 +3944,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3925
3944
|
}
|
|
3926
3945
|
|
|
3927
3946
|
this.inLoop = true
|
|
3928
|
-
|
|
3947
|
+
// Track Go template loop variables. The range *value* variable
|
|
3948
|
+
// is the dot context (`.`) and goes on `loopParamStack`; the
|
|
3949
|
+
// range *index* variable needs `$name` notation and goes on
|
|
3950
|
+
// `loopVarRefCount`. For `.keys()`, the user's param IS the index
|
|
3951
|
+
// (in the `$k, $_` position), so it needs `$name` — don't push
|
|
3952
|
+
// it to loopParamStack (`.` would resolve to the value, not key).
|
|
3953
|
+
// Push `''` instead — falsy, so the `currentLoopParam &&` guard
|
|
3954
|
+
// in `identifier()` / `renderConditionExpr` short-circuits and
|
|
3955
|
+
// no name ever matches the empty string.
|
|
3956
|
+
// Uses ref-counting (not a flat Set) so nested loops with the
|
|
3957
|
+
// same index var name don't clobber the outer loop's entry on
|
|
3958
|
+
// cleanup.
|
|
3959
|
+
const addedLoopVars: string[] = []
|
|
3960
|
+
if (loop.iterationShape === 'keys') {
|
|
3961
|
+
this.loopParamStack.push('')
|
|
3962
|
+
this.loopVarRefCount.set(param, (this.loopVarRefCount.get(param) ?? 0) + 1)
|
|
3963
|
+
addedLoopVars.push(param)
|
|
3964
|
+
} else {
|
|
3965
|
+
this.loopParamStack.push(param)
|
|
3966
|
+
if (rangeIndex !== '_') {
|
|
3967
|
+
this.loopVarRefCount.set(rangeIndex, (this.loopVarRefCount.get(rangeIndex) ?? 0) + 1)
|
|
3968
|
+
addedLoopVars.push(rangeIndex)
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3929
3971
|
const children = this.renderChildren(loop.children)
|
|
3972
|
+
for (const v of addedLoopVars) {
|
|
3973
|
+
const rc = (this.loopVarRefCount.get(v) ?? 1) - 1
|
|
3974
|
+
if (rc <= 0) this.loopVarRefCount.delete(v)
|
|
3975
|
+
else this.loopVarRefCount.set(v, rc)
|
|
3976
|
+
}
|
|
3930
3977
|
this.loopParamStack.pop()
|
|
3931
3978
|
this.inLoop = false
|
|
3932
3979
|
|
|
@@ -3961,11 +4008,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3961
4008
|
|
|
3962
4009
|
// Per-item start marker for multi-root Fragment items (#1212).
|
|
3963
4010
|
const itemMarker = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : ''
|
|
3964
|
-
return `{{bfComment "loop:${loop.markerId}"}}{{range $${
|
|
4011
|
+
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}{{if ${filterCond}}}${itemMarker}${children}{{end}}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
3965
4012
|
}
|
|
3966
4013
|
|
|
3967
4014
|
const itemMarker = loop.bodyIsMultiRoot ? `{{bfComment "bf-loop-i"}}` : ''
|
|
3968
|
-
return `{{bfComment "loop:${loop.markerId}"}}{{range $${
|
|
4015
|
+
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
3969
4016
|
}
|
|
3970
4017
|
|
|
3971
4018
|
/**
|