@barefootjs/go-template 0.1.1 → 0.1.3
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 +15 -9
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +121 -86
- package/dist/build.js +121 -86
- package/dist/index.js +121 -86
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +110 -10
- package/src/adapter/go-template-adapter.ts +142 -140
|
@@ -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 {
|
|
@@ -308,6 +309,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
308
309
|
* follow-up).
|
|
309
310
|
*/
|
|
310
311
|
private restPropsName: string | null = null
|
|
312
|
+
private templateVarCounter: number = 0
|
|
311
313
|
/** Local type names resolved from typeDefinitions (populated during generateTypes) */
|
|
312
314
|
private localTypeNames: Set<string> = new Set()
|
|
313
315
|
/** Local type aliases mapping type name to base type (e.g., Filter → 'string') */
|
|
@@ -334,6 +336,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
334
336
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
|
|
335
337
|
this.componentName = ir.metadata.componentName
|
|
336
338
|
this.errors = []
|
|
339
|
+
this.templateVarCounter = 0
|
|
337
340
|
this.propsObjectName = ir.metadata.propsObjectName
|
|
338
341
|
this.restPropsName = ir.metadata.restPropsName ?? null
|
|
339
342
|
|
|
@@ -807,8 +810,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
807
810
|
lines.push('\tBfParent string // Optional: parent scope id')
|
|
808
811
|
lines.push('\tBfMount string // Optional: slot id in parent')
|
|
809
812
|
|
|
810
|
-
// Static nested components appear in Input;
|
|
811
|
-
|
|
813
|
+
// Static + prop-derived nested components appear in Input;
|
|
814
|
+
// signal-backed dynamic ones are template-only
|
|
815
|
+
const inputNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
812
816
|
|
|
813
817
|
// Collect nested component array field names to skip from propsParams
|
|
814
818
|
const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
|
|
@@ -821,8 +825,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
821
825
|
lines.push(`\t${fieldName} ${goType}`)
|
|
822
826
|
}
|
|
823
827
|
|
|
824
|
-
// Add nested component input arrays
|
|
825
|
-
for (const nested of
|
|
828
|
+
// Add nested component input arrays
|
|
829
|
+
for (const nested of inputNested) {
|
|
826
830
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
827
831
|
}
|
|
828
832
|
|
|
@@ -953,10 +957,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
953
957
|
|
|
954
958
|
// Add array fields for nested components (for template rendering)
|
|
955
959
|
for (const nested of nestedComponents) {
|
|
956
|
-
if (nested.isDynamic) {
|
|
957
|
-
// Dynamic
|
|
960
|
+
if (nested.isDynamic && !nested.isPropDerived) {
|
|
961
|
+
// Dynamic signal array loops: template-only, not in JSON
|
|
958
962
|
lines.push(`\t${nested.name}s []${nested.name}Props \`json:"-"\``)
|
|
959
963
|
} else {
|
|
964
|
+
// Static arrays and prop-derived dynamic arrays: include in JSON
|
|
965
|
+
// so the client can hydrate via mapArray or forEach
|
|
960
966
|
const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
|
|
961
967
|
lines.push(`\t${nested.name}s []${nested.name}Props \`json:"${jsonTag}"\``)
|
|
962
968
|
}
|
|
@@ -1005,9 +1011,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1005
1011
|
// field, the SSR template iterates over it, but
|
|
1006
1012
|
// `NewTodoAppProps(TodoAppInput{Initial: ...})` returns it empty
|
|
1007
1013
|
// and the page renders a blank list (#1442 echo TodoApp repro).
|
|
1008
|
-
const
|
|
1014
|
+
const signalDynamicNested = nestedComponents.filter(n => n.isDynamic && !n.isPropDerived)
|
|
1009
1015
|
lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
|
|
1010
|
-
for (const nested of
|
|
1016
|
+
for (const nested of signalDynamicNested) {
|
|
1011
1017
|
const arrayField = `${nested.name}s`
|
|
1012
1018
|
lines.push(`//`)
|
|
1013
1019
|
lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`)
|
|
@@ -1030,8 +1036,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1030
1036
|
lines.push('\t}')
|
|
1031
1037
|
lines.push('')
|
|
1032
1038
|
|
|
1033
|
-
// Static nested components
|
|
1034
|
-
|
|
1039
|
+
// Static + prop-derived nested components: auto-populate from input.
|
|
1040
|
+
// Signal-backed dynamic arrays are set manually by the handler.
|
|
1041
|
+
const staticNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
|
|
1035
1042
|
|
|
1036
1043
|
// Handle nested components
|
|
1037
1044
|
if (staticNested.length > 0) {
|
|
@@ -1266,6 +1273,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1266
1273
|
result.push({
|
|
1267
1274
|
...loop.childComponent,
|
|
1268
1275
|
isDynamic: !loop.isStaticArray,
|
|
1276
|
+
isPropDerived: !!loop.isPropDerivedArray,
|
|
1269
1277
|
})
|
|
1270
1278
|
}
|
|
1271
1279
|
}
|
|
@@ -2471,7 +2479,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2471
2479
|
|
|
2472
2480
|
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2473
2481
|
if (literalType === 'string') return `"${value}"`
|
|
2474
|
-
if (literalType === 'null') return '
|
|
2482
|
+
if (literalType === 'null') return 'nil'
|
|
2475
2483
|
return String(value)
|
|
2476
2484
|
}
|
|
2477
2485
|
|
|
@@ -2530,8 +2538,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2530
2538
|
if (result) return result
|
|
2531
2539
|
}
|
|
2532
2540
|
|
|
2533
|
-
// find().property → {{with bf_find ...}}{{.Property}}{{end}}
|
|
2534
|
-
if (object.kind === 'higher-order' && object.method === 'find') {
|
|
2541
|
+
// find().property / findLast().property → {{with bf_find ...}}{{.Property}}{{end}}
|
|
2542
|
+
if (object.kind === 'higher-order' && (object.method === 'find' || object.method === 'findLast')) {
|
|
2535
2543
|
const findResult = this.renderHigherOrderExpr(object, emit)
|
|
2536
2544
|
if (findResult) {
|
|
2537
2545
|
return `{{with ${findResult}}}{{.${this.capitalizeFieldName(property)}}}{{end}}`
|
|
@@ -2613,6 +2621,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2613
2621
|
return `or ${wrapLeft} ${wrapRight}`
|
|
2614
2622
|
}
|
|
2615
2623
|
|
|
2624
|
+
// Note: JSX-level ternaries (`{expr ? a : b}`) are handled at the
|
|
2625
|
+
// IR level as IRConditional, which goes through convertConditionToGo
|
|
2626
|
+
// → renderConditionExpr (preamble-aware). This emitter method is
|
|
2627
|
+
// only reached for ternaries nested inside other ParsedExpr trees
|
|
2628
|
+
// (e.g. template-literal interpolation), where the test is always a
|
|
2629
|
+
// simple pipeline expression (runtime helpers, not template blocks).
|
|
2616
2630
|
conditional(
|
|
2617
2631
|
test: ParsedExpr,
|
|
2618
2632
|
consequent: ParsedExpr,
|
|
@@ -2620,8 +2634,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2620
2634
|
emit: (e: ParsedExpr) => string,
|
|
2621
2635
|
): string {
|
|
2622
2636
|
const t = emit(test)
|
|
2623
|
-
// Nested conditionals already return complete {{if}}...{{end}} blocks;
|
|
2624
|
-
// literals return bare text (used within attributes).
|
|
2625
2637
|
const c = this.renderConditionalBranch(consequent)
|
|
2626
2638
|
const a = this.renderConditionalBranch(alternate)
|
|
2627
2639
|
return `{{if ${t}}}${c}{{else}}${a}{{end}}`
|
|
@@ -2672,7 +2684,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2672
2684
|
const reconstructed = { kind: 'higher-order' as const, method, object, param, predicate }
|
|
2673
2685
|
const result = this.renderHigherOrderExpr(reconstructed, emit)
|
|
2674
2686
|
if (result) return result
|
|
2675
|
-
if (method === 'find' || method === 'findIndex') {
|
|
2687
|
+
if (method === 'find' || method === 'findIndex' || method === 'findLast' || method === 'findLastIndex') {
|
|
2676
2688
|
const templateBlock = this.renderFindTemplateBlock(reconstructed, emit)
|
|
2677
2689
|
if (templateBlock) return templateBlock
|
|
2678
2690
|
}
|
|
@@ -2941,26 +2953,29 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2941
2953
|
return `bf_filter ${arrayExpr} "${field}" ${value}`
|
|
2942
2954
|
}
|
|
2943
2955
|
|
|
2944
|
-
if (expr.method === 'find' || expr.method === 'findIndex') {
|
|
2956
|
+
if (expr.method === 'find' || expr.method === 'findIndex' || expr.method === 'findLast' || expr.method === 'findLastIndex') {
|
|
2945
2957
|
const eqPred = this.extractEqualityPredicate(
|
|
2946
2958
|
expr.predicate, expr.param, e => this.renderParsedExpr(e)
|
|
2947
2959
|
)
|
|
2948
2960
|
if (!eqPred) return null
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2961
|
+
const funcMap: Record<string, string> = {
|
|
2962
|
+
find: 'bf_find', findIndex: 'bf_find_index',
|
|
2963
|
+
findLast: 'bf_find_last', findLastIndex: 'bf_find_last_index',
|
|
2964
|
+
}
|
|
2965
|
+
return `${funcMap[expr.method]} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`
|
|
2951
2966
|
}
|
|
2952
2967
|
|
|
2953
2968
|
return null
|
|
2954
2969
|
}
|
|
2955
2970
|
|
|
2956
2971
|
/**
|
|
2957
|
-
* Render find
|
|
2958
|
-
* Falls back from bf_find/
|
|
2959
|
-
*
|
|
2972
|
+
* Render find/findIndex/findLast/findLastIndex with complex predicates
|
|
2973
|
+
* using range/if blocks. Falls back from bf_find/bf_find_last helpers
|
|
2974
|
+
* when extractEqualityPredicate returns null.
|
|
2960
2975
|
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
*
|
|
2976
|
+
* find/findIndex use break on first match (forward scan).
|
|
2977
|
+
* findLast/findLastIndex iterate forward and keep overwriting a result
|
|
2978
|
+
* variable; the final value is the last match.
|
|
2964
2979
|
*/
|
|
2965
2980
|
private renderFindTemplateBlock(
|
|
2966
2981
|
expr: Extract<ParsedExpr, { kind: 'higher-order' }>,
|
|
@@ -2980,6 +2995,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2980
2995
|
return `{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{$i}}{{break}}{{end}}{{end}}`
|
|
2981
2996
|
}
|
|
2982
2997
|
|
|
2998
|
+
if (expr.method === 'findLast') {
|
|
2999
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3000
|
+
const capture = propertyAccess ? `.${propertyAccess}` : '.'
|
|
3001
|
+
return `{{${v} := ""}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = ${capture}}}{{end}}{{end}}{{${v}}}`
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
if (expr.method === 'findLastIndex') {
|
|
3005
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3006
|
+
return `{{${v} := -1}}{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{${v} = $i}}{{end}}{{end}}{{${v}}}`
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2983
3009
|
return null
|
|
2984
3010
|
}
|
|
2985
3011
|
|
|
@@ -3003,13 +3029,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3003
3029
|
if (condition.includes('[UNSUPPORTED')) return null
|
|
3004
3030
|
|
|
3005
3031
|
if (expr.method === 'every') {
|
|
3006
|
-
|
|
3032
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3007
3033
|
const negated = this.negateGoCondition(condition)
|
|
3008
|
-
return `{{$
|
|
3034
|
+
return `{{${v} := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{${v} = false}}{{break}}{{end}}{{end}}{{${v}}}`
|
|
3009
3035
|
}
|
|
3010
3036
|
|
|
3011
3037
|
if (expr.method === 'some') {
|
|
3012
|
-
|
|
3038
|
+
const v = `$bf_r${this.templateVarCounter++}`
|
|
3039
|
+
return `{{${v} := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = true}}{{break}}{{end}}{{end}}{{${v}}}`
|
|
3013
3040
|
}
|
|
3014
3041
|
|
|
3015
3042
|
return null
|
|
@@ -3068,6 +3095,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3068
3095
|
return expr.kind === 'logical' || expr.kind === 'unary' || expr.kind === 'conditional'
|
|
3069
3096
|
}
|
|
3070
3097
|
|
|
3098
|
+
/**
|
|
3099
|
+
* Split a rendered template block into preamble + final expression.
|
|
3100
|
+
* The last `{{...}}` must be a variable reference (`$bf_rN` or
|
|
3101
|
+
* `$bf_result`). Control tokens like `{{end}}` or `{{break}}` are
|
|
3102
|
+
* rejected — those template blocks (e.g. find's range/break form)
|
|
3103
|
+
* can't be composed in binary/logical expressions.
|
|
3104
|
+
*/
|
|
3105
|
+
private splitPreamble(rendered: string): { preamble: string; expr: string } | null {
|
|
3106
|
+
if (!rendered.includes('{{')) return null
|
|
3107
|
+
const lastOpen = rendered.lastIndexOf('{{')
|
|
3108
|
+
const lastClose = rendered.lastIndexOf('}}')
|
|
3109
|
+
if (lastOpen >= 0 && lastClose > lastOpen) {
|
|
3110
|
+
const candidate = rendered.substring(lastOpen + 2, lastClose)
|
|
3111
|
+
if (!candidate.startsWith('$')) return null
|
|
3112
|
+
return {
|
|
3113
|
+
preamble: rendered.substring(0, lastOpen),
|
|
3114
|
+
expr: candidate,
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return null
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3071
3120
|
// =============================================================================
|
|
3072
3121
|
// Block Body Condition Rendering
|
|
3073
3122
|
// =============================================================================
|
|
@@ -3310,7 +3359,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3310
3359
|
return `"${expr.value}"`
|
|
3311
3360
|
}
|
|
3312
3361
|
if (expr.literalType === 'null') {
|
|
3313
|
-
return '
|
|
3362
|
+
return 'nil'
|
|
3314
3363
|
}
|
|
3315
3364
|
return String(expr.value)
|
|
3316
3365
|
|
|
@@ -3682,196 +3731,149 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3682
3731
|
return { condition: `false`, preamble: '' }
|
|
3683
3732
|
}
|
|
3684
3733
|
|
|
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: '' }
|
|
3734
|
+
const { preamble, expr: condition } = this.renderConditionExpr(parsed)
|
|
3735
|
+
return { condition, preamble }
|
|
3701
3736
|
}
|
|
3702
3737
|
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
private renderConditionExpr(expr: ParsedExpr): string {
|
|
3738
|
+
private renderConditionExpr(expr: ParsedExpr): { preamble: string; expr: string } {
|
|
3739
|
+
const plain = (e: string) => ({ preamble: '', expr: e })
|
|
3740
|
+
|
|
3707
3741
|
switch (expr.kind) {
|
|
3708
3742
|
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
3743
|
{
|
|
3718
3744
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
3719
3745
|
if (currentLoopParam && expr.name === currentLoopParam) {
|
|
3720
|
-
return '.'
|
|
3746
|
+
return plain('.')
|
|
3721
3747
|
}
|
|
3722
3748
|
}
|
|
3723
|
-
return `.${this.capitalizeFieldName(expr.name)}`
|
|
3749
|
+
return plain(`.${this.capitalizeFieldName(expr.name)}`)
|
|
3724
3750
|
|
|
3725
3751
|
case 'literal':
|
|
3726
|
-
if (expr.literalType === 'string') {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
if (expr.literalType === 'null') {
|
|
3730
|
-
return '""'
|
|
3731
|
-
}
|
|
3732
|
-
return String(expr.value)
|
|
3752
|
+
if (expr.literalType === 'string') return plain(`"${expr.value}"`)
|
|
3753
|
+
if (expr.literalType === 'null') return plain('nil')
|
|
3754
|
+
return plain(String(expr.value))
|
|
3733
3755
|
|
|
3734
3756
|
case 'call': {
|
|
3735
|
-
// Signal call: count() -> .Count
|
|
3736
3757
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
3737
|
-
return `.${this.capitalizeFieldName(expr.callee.name)}`
|
|
3758
|
+
return plain(`.${this.capitalizeFieldName(expr.callee.name)}`)
|
|
3738
3759
|
}
|
|
3739
|
-
return this.renderParsedExpr(expr)
|
|
3760
|
+
return plain(this.renderParsedExpr(expr))
|
|
3740
3761
|
}
|
|
3741
3762
|
|
|
3742
3763
|
case 'member': {
|
|
3743
|
-
// Handle .length with higher-order filter → len (bf_filter ...)
|
|
3744
3764
|
if (expr.property === 'length' && expr.object.kind === 'higher-order') {
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3765
|
+
// renderFilterLengthExpr uses bf_filter runtime helpers (not
|
|
3766
|
+
// template blocks), so .preamble is always empty here today.
|
|
3767
|
+
// If a future higher-order method produces preambles through
|
|
3768
|
+
// this path, the callback would need to propagate them.
|
|
3769
|
+
const result = this.renderFilterLengthExpr(expr.object, e => this.renderConditionExpr(e).expr)
|
|
3770
|
+
if (result) return plain(result)
|
|
3749
3771
|
}
|
|
3750
3772
|
|
|
3751
|
-
// Handle SolidJS-style props pattern: props.xxx -> .Xxx
|
|
3752
3773
|
if (expr.object.kind === 'identifier' && this.propsObjectName && expr.object.name === this.propsObjectName) {
|
|
3753
|
-
return `.${this.capitalizeFieldName(expr.property)}`
|
|
3774
|
+
return plain(`.${this.capitalizeFieldName(expr.property)}`)
|
|
3754
3775
|
}
|
|
3755
3776
|
|
|
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
3777
|
{
|
|
3764
3778
|
const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
|
|
3765
3779
|
if (expr.object.kind === 'identifier' && currentLoopParam && expr.object.name === currentLoopParam) {
|
|
3766
|
-
return `.${this.capitalizeFieldName(expr.property)}`
|
|
3780
|
+
return plain(`.${this.capitalizeFieldName(expr.property)}`)
|
|
3767
3781
|
}
|
|
3768
3782
|
}
|
|
3769
3783
|
|
|
3770
3784
|
const obj = this.renderConditionExpr(expr.object)
|
|
3771
3785
|
if (expr.property === 'length') {
|
|
3772
|
-
return `len ${obj}`
|
|
3786
|
+
return { preamble: obj.preamble, expr: `len ${obj.expr}` }
|
|
3773
3787
|
}
|
|
3774
|
-
return `${obj}.${this.capitalizeFieldName(expr.property)}`
|
|
3788
|
+
return { preamble: obj.preamble, expr: `${obj.expr}.${this.capitalizeFieldName(expr.property)}` }
|
|
3775
3789
|
}
|
|
3776
3790
|
|
|
3777
3791
|
case 'binary': {
|
|
3778
|
-
// Check if left operand needs parentheses (e.g., function calls in Go template)
|
|
3779
3792
|
const leftNeedsParens = this.needsParensInGoTemplate(expr.left)
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
left = `(${left})`
|
|
3783
|
-
}
|
|
3793
|
+
const leftResult = this.renderConditionExpr(expr.left)
|
|
3794
|
+
const left = leftNeedsParens ? `(${leftResult.expr})` : leftResult.expr
|
|
3784
3795
|
|
|
3785
3796
|
const rightNeedsParens = this.needsParensInGoTemplate(expr.right)
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
right = `(${right})`
|
|
3789
|
-
}
|
|
3797
|
+
const rightResult = this.renderConditionExpr(expr.right)
|
|
3798
|
+
const right = rightNeedsParens ? `(${rightResult.expr})` : rightResult.expr
|
|
3790
3799
|
|
|
3800
|
+
const preamble = leftResult.preamble + rightResult.preamble
|
|
3801
|
+
|
|
3802
|
+
let result: string
|
|
3791
3803
|
switch (expr.op) {
|
|
3792
3804
|
case '===':
|
|
3793
3805
|
case '==':
|
|
3794
|
-
|
|
3806
|
+
result = `eq ${left} ${right}`; break
|
|
3795
3807
|
case '!==':
|
|
3796
3808
|
case '!=':
|
|
3797
|
-
|
|
3809
|
+
result = `ne ${left} ${right}`; break
|
|
3798
3810
|
case '>':
|
|
3799
|
-
|
|
3811
|
+
result = `gt ${left} ${right}`; break
|
|
3800
3812
|
case '<':
|
|
3801
|
-
|
|
3813
|
+
result = `lt ${left} ${right}`; break
|
|
3802
3814
|
case '>=':
|
|
3803
|
-
|
|
3815
|
+
result = `ge ${left} ${right}`; break
|
|
3804
3816
|
case '<=':
|
|
3805
|
-
|
|
3806
|
-
// Arithmetic in conditions
|
|
3817
|
+
result = `le ${left} ${right}`; break
|
|
3807
3818
|
case '+':
|
|
3808
|
-
|
|
3819
|
+
result = `bf_add ${left} ${right}`; break
|
|
3809
3820
|
case '-':
|
|
3810
|
-
|
|
3821
|
+
result = `bf_sub ${left} ${right}`; break
|
|
3811
3822
|
case '*':
|
|
3812
|
-
|
|
3823
|
+
result = `bf_mul ${left} ${right}`; break
|
|
3813
3824
|
case '/':
|
|
3814
|
-
|
|
3825
|
+
result = `bf_div ${left} ${right}`; break
|
|
3815
3826
|
default:
|
|
3816
|
-
|
|
3827
|
+
result = `${left} ${expr.op} ${right}`
|
|
3817
3828
|
}
|
|
3829
|
+
return { preamble, expr: result }
|
|
3818
3830
|
}
|
|
3819
3831
|
|
|
3820
3832
|
case 'unary': {
|
|
3821
3833
|
const arg = this.renderConditionExpr(expr.argument)
|
|
3822
|
-
if (expr.op === '!') {
|
|
3823
|
-
|
|
3824
|
-
}
|
|
3825
|
-
if (expr.op === '-') {
|
|
3826
|
-
return `bf_neg ${arg}`
|
|
3827
|
-
}
|
|
3834
|
+
if (expr.op === '!') return { preamble: arg.preamble, expr: `not ${arg.expr}` }
|
|
3835
|
+
if (expr.op === '-') return { preamble: arg.preamble, expr: `bf_neg ${arg.expr}` }
|
|
3828
3836
|
return arg
|
|
3829
3837
|
}
|
|
3830
3838
|
|
|
3831
3839
|
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
|
|
3840
|
+
const leftResult = this.renderConditionExpr(expr.left)
|
|
3841
|
+
const rightResult = this.renderConditionExpr(expr.right)
|
|
3842
|
+
const preamble = leftResult.preamble + rightResult.preamble
|
|
3843
|
+
const wrapLeft = this.needsParens(expr.left) ? `(${leftResult.expr})` : leftResult.expr
|
|
3844
|
+
const wrapRight = this.needsParens(expr.right) ? `(${rightResult.expr})` : rightResult.expr
|
|
3845
|
+
const result = expr.op === '&&'
|
|
3846
|
+
? `and ${wrapLeft} ${wrapRight}`
|
|
3847
|
+
: `or ${wrapLeft} ${wrapRight}`
|
|
3848
|
+
return { preamble, expr: result }
|
|
3841
3849
|
}
|
|
3842
3850
|
|
|
3843
3851
|
case 'conditional': {
|
|
3844
|
-
// Ternary in condition: (cond ? a : b) is unusual but handle it
|
|
3845
3852
|
const test = this.renderConditionExpr(expr.test)
|
|
3846
|
-
return test
|
|
3853
|
+
return test
|
|
3847
3854
|
}
|
|
3848
3855
|
|
|
3849
3856
|
case 'template-literal':
|
|
3850
|
-
|
|
3851
|
-
return this.renderParsedExpr(expr)
|
|
3857
|
+
return plain(this.renderParsedExpr(expr))
|
|
3852
3858
|
|
|
3853
3859
|
case 'arrow-fn':
|
|
3854
|
-
|
|
3855
|
-
return '[ARROW-FN]'
|
|
3860
|
+
return plain('[ARROW-FN]')
|
|
3856
3861
|
|
|
3857
|
-
case 'higher-order':
|
|
3858
|
-
|
|
3859
|
-
|
|
3862
|
+
case 'higher-order': {
|
|
3863
|
+
const rendered = this.renderParsedExpr(expr)
|
|
3864
|
+
const split = this.splitPreamble(rendered)
|
|
3865
|
+
if (split) return split
|
|
3866
|
+
return plain(rendered)
|
|
3867
|
+
}
|
|
3860
3868
|
|
|
3861
3869
|
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)
|
|
3870
|
+
return plain(this.renderParsedExpr(expr))
|
|
3866
3871
|
|
|
3867
3872
|
case 'array-method':
|
|
3868
|
-
|
|
3869
|
-
// refusal diagnostic at one site rather than duplicating it
|
|
3870
|
-
// for condition-position emission.
|
|
3871
|
-
return this.renderParsedExpr(expr)
|
|
3873
|
+
return plain(this.renderParsedExpr(expr))
|
|
3872
3874
|
|
|
3873
3875
|
case 'unsupported':
|
|
3874
|
-
return expr.raw
|
|
3876
|
+
return plain(expr.raw)
|
|
3875
3877
|
}
|
|
3876
3878
|
}
|
|
3877
3879
|
|