@barefootjs/go-template 0.33.4 → 0.34.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.
@@ -128,6 +128,44 @@ function lowerTernaryTest(ctx: GoEmitContext, test: ParsedExpr): string {
128
128
  return isBoolShape ? go : `(bf_truthy ${go})`
129
129
  }
130
130
 
131
+ /**
132
+ * First registered matcher that recognises `callee(args)`, or null (#2842).
133
+ * The single registry consultation every Go lowering path shares: the
134
+ * top-level `lowerRegisteredCall` early return, the ParsedExpr `call()`
135
+ * dispatcher (`go-template-adapter.ts`, so a registered call is recognised no
136
+ * matter how deep it sits in an expression tree — a ternary branch, a
137
+ * template-literal interpolation, a binary operand — not only when the call
138
+ * IS the whole expression), and the attribute-position `bf_attr` route all
139
+ * ask this one question. First-match-wins: a malformed helper id from one
140
+ * plugin must not be silently masked by falling through to another.
141
+ */
142
+ export function matchRegisteredCall(
143
+ ctx: GoEmitContext,
144
+ callee: ParsedExpr,
145
+ args: readonly ParsedExpr[],
146
+ ): LoweringNode | null {
147
+ for (const matcher of ctx.state.loweringMatchers) {
148
+ const node = matcher(callee, args)
149
+ if (node) return node
150
+ }
151
+ return null
152
+ }
153
+
154
+ /**
155
+ * Render a recognised call's neutral node to its Go pipeline (unparenthesised
156
+ * — same contract as `lowerRegisteredCall`), or null when no plugin matches
157
+ * or the matched helper has no Go mapping.
158
+ */
159
+ export function lowerRegisteredCallNode(
160
+ ctx: GoEmitContext,
161
+ callee: ParsedExpr,
162
+ args: readonly ParsedExpr[],
163
+ ): string | null {
164
+ if (ctx.state.loweringMatchers.length === 0) return null
165
+ const node = matchRegisteredCall(ctx, callee, args)
166
+ return node ? renderLoweringNode(ctx, node) : null
167
+ }
168
+
131
169
  /**
132
170
  * Lower a helper call to a Go template expression, or null when no registered
133
171
  * plugin recognises it (→ the generic lowering). Matchers — including the
@@ -144,8 +182,7 @@ export function lowerRegisteredCall(
144
182
  jsExpr: string,
145
183
  preParsed?: ParsedExpr,
146
184
  ): string | null {
147
- const matchers = ctx.state.loweringMatchers
148
- if (matchers.length === 0) return null
185
+ if (ctx.state.loweringMatchers.length === 0) return null
149
186
 
150
187
  let call: ParsedExpr | undefined = preParsed?.kind === 'call' ? preParsed : undefined
151
188
  if (!call) {
@@ -155,13 +192,82 @@ export function lowerRegisteredCall(
155
192
  call = parsed
156
193
  }
157
194
 
158
- for (const matcher of matchers) {
159
- const node = matcher(call.callee, call.args)
160
- if (!node) continue
161
- const rendered = renderLoweringNode(ctx, node)
162
- if (rendered !== null) return rendered
195
+ return lowerRegisteredCallNode(ctx, call.callee, call.args)
196
+ }
197
+
198
+ /**
199
+ * Attribute-position twin of `lowerRegisteredCall` (#2743). When an intrinsic
200
+ * element attribute's value is a `query` guard-list (the neutral node the
201
+ * `queryHref` plugin — or any plugin reusing the `query` helper — produces),
202
+ * emit the WHOLE attribute as one `bf_attr` action returning
203
+ * `template.HTMLAttr`, instead of `name="{{bf_query …}}"`. html/template
204
+ * infers a URL context from the attribute NAME (`href`, `src`, `action`,
205
+ * `data-*`, anything containing `src`/`uri`/`url`, …) and percent-encodes the
206
+ * entire value there; the JS reference (Hono) only HTML-escapes. Keyed on the
207
+ * neutral `helper` id, not the attribute name (Go's URL-context table is a
208
+ * heuristic we must not re-implement) and not the JS API name. Returns null
209
+ * for a non-call, an unmatched call, or any other node kind/helper — the
210
+ * caller then takes its ordinary path.
211
+ *
212
+ * Also accepts a `conditional` node directly (#2743 follow-up, pullfrog
213
+ * review on #2841): a ternary with a real, non-`undefined` alternate
214
+ * (`cond ? queryHref(...) : '/fallback'`) is syntactically valid and already
215
+ * lowers its branches correctly via `lowerTernary` — but the CALLER used to
216
+ * still wrap the resulting `(bf_ternary …)` pipeline in the ordinary
217
+ * `name="{{...}}"` form, which leaves it exposed to the same URL-context
218
+ * percent-encoding this function exists to route around. Whenever EITHER
219
+ * branch (recursing through a right-folded nested ternary chain, the same
220
+ * shape `lowerTernary` itself right-folds) is a `query` guard-list call, the
221
+ * whole ternary is wrapped in `bf_attr` too — matching the reference, which
222
+ * HTML-escapes the picked branch's value the same way regardless of which
223
+ * branch won.
224
+ *
225
+ * The caller passes the CONSEQUENT ALONE (not the full ternary) for the
226
+ * `undefined`-alternate omission shape (`cond ? queryHref(...) : undefined`,
227
+ * #2842) — omission needs the `{{if}}` wrapper that shape's caller already
228
+ * builds; a `bf_ternary … ""` inside `bf_attr` can't express it (`Attr`
229
+ * renders `name=""` for an empty value, not attribute absence).
230
+ */
231
+ export function lowerRegisteredAttrCall(
232
+ ctx: GoEmitContext,
233
+ attrName: string,
234
+ parsed: ParsedExpr,
235
+ ): string | null {
236
+ if (parsed.kind === 'conditional') {
237
+ if (!ternaryHasQueryBranch(ctx, parsed.consequent, parsed.alternate)) return null
238
+ // `lowerTernary` already returns a complete parenthesised `(bf_ternary …)`
239
+ // pipeline — pass it straight through as `bf_attr`'s value argument
240
+ // rather than re-wrapping it in a redundant second set of parens.
241
+ const rendered = lowerTernary(ctx, parsed.test, parsed.consequent, parsed.alternate)
242
+ return `{{bf_attr ${JSON.stringify(attrName)} ${rendered}}}`
163
243
  }
164
- return null
244
+ if (parsed.kind !== 'call') return null
245
+ const node = matchRegisteredCall(ctx, parsed.callee, parsed.args)
246
+ if (!node || node.kind !== 'guard-list' || node.helper !== 'query') return null
247
+ const rendered = renderLoweringNode(ctx, node)
248
+ return rendered === null ? null : `{{bf_attr ${JSON.stringify(attrName)} (${rendered})}}`
249
+ }
250
+
251
+ /** Whether a `call` node is a recognised `query` guard-list lowering. */
252
+ function isQueryGuardListCall(ctx: GoEmitContext, node: ParsedExpr): boolean {
253
+ if (node.kind !== 'call') return false
254
+ const lowered = matchRegisteredCall(ctx, node.callee, node.args)
255
+ return lowered?.kind === 'guard-list' && lowered.helper === 'query'
256
+ }
257
+
258
+ /**
259
+ * Whether either branch of a ternary is (or, for a right-folded chain,
260
+ * eventually resolves to) a `query` guard-list call — the trigger for
261
+ * routing the whole ternary through `bf_attr` in `lowerRegisteredAttrCall`.
262
+ */
263
+ function ternaryHasQueryBranch(
264
+ ctx: GoEmitContext,
265
+ consequent: ParsedExpr,
266
+ alternate: ParsedExpr,
267
+ ): boolean {
268
+ const branchHasQuery = (n: ParsedExpr): boolean =>
269
+ n.kind === 'conditional' ? ternaryHasQueryBranch(ctx, n.consequent, n.alternate) : isQueryGuardListCall(ctx, n)
270
+ return branchHasQuery(consequent) || branchHasQuery(alternate)
165
271
  }
166
272
 
167
273
  /**