@barefootjs/rust 0.18.3 → 0.18.5

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.
@@ -168,7 +168,7 @@ import {
168
168
  } from '@barefootjs/jsx'
169
169
  import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
170
170
  import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
171
- import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
171
+ import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
172
172
 
173
173
  import type { JinjaRenderCtx } from './lib/types.ts'
174
174
  import { JINJA_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
@@ -254,6 +254,14 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
254
254
  private options: Required<MinijinjaAdapterOptions>
255
255
  private errors: CompilerError[] = []
256
256
  private inLoop: boolean = false
257
+ /**
258
+ * `IRLoop.depth` of the loop currently being rendered (save/restore
259
+ * around `renderChildren(loop.children)`, mirroring `inLoop` above).
260
+ * `renderAttributes` reads this to derive the `key` → `data-key`/
261
+ * `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
262
+ * re-derived here (#2168 nested-loop-outer-binding).
263
+ */
264
+ private currentLoopKeyDepth = 0
257
265
  /**
258
266
  * SolidJS-style props identifier (`function(props: P)`) and the
259
267
  * analyzer-extracted prop names. Stashed at `generate()` entry so the
@@ -449,7 +457,9 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
449
457
  }
450
458
 
451
459
  emitText(node: IRText): string {
452
- return node.value
460
+ // IRText carries the entity-DECODED value (Phase 1 decodes JSX
461
+ // character references); re-escape for direct HTML emission.
462
+ return escapeHtml(node.value)
453
463
  }
454
464
 
455
465
  emitExpression(node: IRExpression): string {
@@ -810,7 +820,11 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
810
820
  // adds one `{% set %}` local per binding (`rest` aliases the item so
811
821
  // `rest.flag` resolves).
812
822
  const indexLocalLines: string[] = []
813
- if (loop.iterationShape === 'keys') {
823
+ if (loop.objectIteration) {
824
+ // `key`/`value` bind directly in the for-header (see below) via the
825
+ // `|items` filter — no derived `loop.index0` local needed, unlike
826
+ // the array `iterationShape` cases.
827
+ } else if (loop.iterationShape === 'keys') {
814
828
  indexLocalLines.push(`{% set ${minijinjaIdent(param)} = loop.index0 %}`)
815
829
  } else if (loop.index) {
816
830
  indexLocalLines.push(`{% set ${minijinjaIdent(loop.index)} = loop.index0 %}`)
@@ -843,10 +857,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
843
857
 
844
858
  const prevInLoop = this.inLoop
845
859
  this.inLoop = true
860
+ const prevLoopKeyDepth = this.currentLoopKeyDepth
861
+ this.currentLoopKeyDepth = loop.depth
846
862
  // Re-render children now that inLoop is set (so nested components use the
847
863
  // loop-child naming convention). renderedChildren above was computed with
848
864
  // the previous flag; recompute under the loop flag.
849
865
  const childrenUnderLoop = this.renderChildren(loop.children)
866
+ this.currentLoopKeyDepth = prevLoopKeyDepth
850
867
  this.inLoop = prevInLoop
851
868
  void renderedChildren
852
869
 
@@ -863,7 +880,25 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
863
880
  // Scoped per-call-site marker so sibling `.map()`s under the same parent
864
881
  // each get their own reconciliation range.
865
882
  lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`)
866
- lines.push(`{% for ${minijinjaIdent(loopVar)} in ${array} %}`)
883
+ // `objectIteration` (#2168 object-entries-map): minijinja has no
884
+ // built-in `.items()` OBJECT METHOD (unlike Python's dict) — `|items`
885
+ // is a FILTER, yielding `[key, value]` pairs, which the `for` tag's
886
+ // own tuple-unpack target (`for a, b in ...`, mirroring Jinja2) binds
887
+ // directly. There's no `|keys`/`|values` filter, so `'keys'`/`'values'`
888
+ // reuse the SAME `|items` pairs and bind the unused half to a
889
+ // throwaway name. Order is whatever the underlying `BTreeMap` gives —
890
+ // sorted-by-key, not JS insertion order (a deliberate design choice
891
+ // for a DIFFERENT feature, canonical JSON encoding — see `num.rs`);
892
+ // this happens to satisfy the current fixture, but is a documented
893
+ // known limitation for out-of-alphabetical-order data, same as Go.
894
+ const forHeader = loop.objectIteration === 'entries'
895
+ ? `{% for ${minijinjaIdent(loop.index ?? param)}, ${minijinjaIdent(param)} in ${array}|items %}`
896
+ : loop.objectIteration === 'keys'
897
+ ? `{% for ${minijinjaIdent(param)}, __bf_v in ${array}|items %}`
898
+ : loop.objectIteration === 'values'
899
+ ? `{% for __bf_k, ${minijinjaIdent(param)} in ${array}|items %}`
900
+ : `{% for ${minijinjaIdent(loopVar)} in ${array} %}`
901
+ lines.push(forHeader)
867
902
  for (const il of indexLocalLines) lines.push(il)
868
903
 
869
904
  // Handle filter().map() pattern by wrapping children in if-condition
@@ -1017,11 +1052,33 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1017
1052
  type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
1018
1053
  const segments: Segment[] = [{ kind: 'entries', parts: [] }]
1019
1054
  const currentEntries = () => this.componentPropSegmentEntries(segments)
1055
+ // Named JSX-valued props OTHER than the reserved `children`
1056
+ // (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) each get
1057
+ // their own `{% set %}` capture, prepended to the final returned
1058
+ // string below — same mechanism as the reserved children capture,
1059
+ // just keyed by the prop's own name instead of `children`.
1060
+ const namedSlotSetBlocks: string[] = []
1020
1061
 
1021
1062
  for (const p of comp.props) {
1022
1063
  // Skip callback props (onXxx) and `ref` — both are client-only for
1023
1064
  // SSR (Hono renders neither; the client JS wires them at hydration).
1024
1065
  if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
1066
+ if (p.value.kind === 'jsx-children' && p.name !== 'children') {
1067
+ const prevInLoop = this.inLoop
1068
+ this.inLoop = false
1069
+ const slotBody = this.renderChildren(p.value.children)
1070
+ this.inLoop = prevInLoop
1071
+ // Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
1072
+ // A JSX prop name can contain characters (`data-slot`) that aren't a
1073
+ // valid minijinja `{% set %}` target, and `comp.slotId` alone would
1074
+ // collide across two named-slot props on the same component
1075
+ // invocation (unlike the reserved children slot, there's only ever
1076
+ // one of those per invocation).
1077
+ const captureName = `bf_prop_${this.childrenCaptureCounter++}`
1078
+ namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`)
1079
+ currentEntries().push(`${minijinjaHashKey(p.name)}: ${captureName}`)
1080
+ continue
1081
+ }
1025
1082
  if (p.value.kind === 'spread') {
1026
1083
  const trimmed = p.value.expr.trim()
1027
1084
  // SolidJS-style props identifier (`function(props: P)`) has no
@@ -1085,12 +1142,12 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1085
1142
  const captureName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
1086
1143
  currentEntries().push(`${minijinjaHashKey('children')}: ${captureName}`)
1087
1144
  const dict = this.combineComponentPropSegments(segments)
1088
- return `{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`
1145
+ return `${namedSlotSetBlocks.join('')}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`
1089
1146
  }
1090
1147
 
1091
1148
  const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
1092
1149
  const dictEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
1093
- return `{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`
1150
+ return `${namedSlotSetBlocks.join('')}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`
1094
1151
  }
1095
1152
 
1096
1153
  private childrenCaptureCounter = 0
@@ -1175,7 +1232,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1175
1232
  * AttrValue lowering for intrinsic-element attributes (Jinja).
1176
1233
  */
1177
1234
  private readonly elementAttrEmitter: AttrValueEmitter = {
1178
- emitLiteral: (value, name) => `${name}="${value.value}"`,
1235
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
1179
1236
  emitExpression: (value, name) => {
1180
1237
  // `style={{ … }}` object literal → a CSS string with dynamic values
1181
1238
  // interpolated, instead of refusing the bare object with BF101 (#1322).
@@ -1380,7 +1437,10 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1380
1437
  // Rewrite JSX special-prop names to their HTML-attribute counterparts.
1381
1438
  let attrName: string
1382
1439
  if (attr.name === 'className') attrName = 'class'
1383
- else if (attr.name === 'key') attrName = 'data-key'
1440
+ else if (attr.name === 'key') {
1441
+ const depth = this.currentLoopKeyDepth
1442
+ attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
1443
+ }
1384
1444
  else attrName = attr.name
1385
1445
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
1386
1446
  if (lowered) parts.push(lowered)
@@ -104,9 +104,4 @@ export const conformancePins: ConformancePins = {
104
104
  // the shape loudly instead of emitting entity-escaped markup that
105
105
  // silently renders tags as text.
106
106
  'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
107
- // Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
108
- // only first-occurrence `.replace` is wired to the runtime helpers.
109
- // Refused with BF101 rather than reusing the first-only lowering,
110
- // which would silently change semantics.
111
- 'string-replaceall': [{ code: 'BF101', severity: 'error' }],
112
107
  }
@@ -16,29 +16,4 @@
16
16
 
17
17
  import type { RenderDivergences } from '@barefootjs/jsx'
18
18
 
19
- export const renderDivergences: RenderDivergences = {
20
- 'arithmetic-text':
21
- '`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)',
22
- 'html-entity-text':
23
- '`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
24
- 'math-methods':
25
- 'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
26
- 'boolean-attr-literals':
27
- 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
28
- 'camelcase-attributes':
29
- '`htmlFor` is not lowered to `for` (Hono maps it)',
30
- 'static-attr-escape':
31
- 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
32
- 'svg-icon':
33
- 'SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case',
34
- 'object-entries-map':
35
- '`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations',
36
- 'nested-loop-outer-binding':
37
- 'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
38
- 'jsx-element-prop':
39
- 'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
40
- 'string-slice':
41
- '`.slice()` on a STRING renders empty (array-slice helper misfires on strings)',
42
- 'string-trim-sided':
43
- '`.trimStart()` / `.trimEnd()` render empty (no lowering)',
44
- }
19
+ export const renderDivergences: RenderDivergences = {}