@metaobjectsdev/render 0.22.1 → 0.23.1-rc.1

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.
@@ -0,0 +1,40 @@
1
+ /** The `has` prefix every derived boolean accessor carries. */
2
+ export declare const HAS_PREFIX = "has";
3
+ /**
4
+ * The boolean-accessor section name for a payload field: `"has" + capitalize(name)`
5
+ * (`abilities` → `hasAbilities`). Byte-identical to the JVM's
6
+ * `PayloadAccessors.hasAccessorName`, including its capitalize, which leaves an
7
+ * already-uppercase first character untouched.
8
+ */
9
+ export declare function hasAccessorName(fieldName: string): string;
10
+ /** Capitalize the first character, leaving an already-uppercase one untouched. */
11
+ export declare function capitalize(s: string): string;
12
+ /**
13
+ * Is `value` "present" for the purposes of `has<Field>`?
14
+ *
15
+ * Mirrors the JVM emitter's per-type bodies exactly:
16
+ * string → non-null AND non-blank (`!foo.isBlank()`, so whitespace is absent)
17
+ * array → non-null AND non-empty (`!foo.isEmpty()`)
18
+ * reference → non-null (any other object)
19
+ *
20
+ * Returns `undefined` for numbers and booleans, which the JVM deliberately emits NO
21
+ * accessor for — they are always-present scalars, and a `{{#hasCount}}` over an int
22
+ * is drift rather than a conditional. Returning undefined (rather than false) keeps
23
+ * that distinction: nothing is injected, so the name stays unresolved exactly as it
24
+ * is on a generated Java record that has no such method.
25
+ */
26
+ export declare function accessorValue(value: unknown): boolean | undefined;
27
+ /**
28
+ * A view over `payload` carrying its derived `has<Field>` accessors, recursively.
29
+ *
30
+ * NON-MUTATING — the caller's payload is never touched, because a render must not
31
+ * be able to change the object it was handed. An AUTHORED key always wins: if a
32
+ * payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
33
+ * derived one.
34
+ *
35
+ * Recursion follows Mustache's own scoping: every nested object and every array
36
+ * ELEMENT becomes a context in its own right, so a section over `abilities` sees
37
+ * the accessors of the ability it is currently iterating.
38
+ */
39
+ export declare function withDerivedAccessors<T>(payload: T, depth?: number): T;
40
+ //# sourceMappingURL=payload-accessors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-accessors.d.ts","sourceRoot":"","sources":["../src/payload-accessors.ts"],"names":[],"mappings":"AAgBA,+DAA+D;AAC/D,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,SAAI,GAAG,CAAC,CAuBhE"}
@@ -0,0 +1,103 @@
1
+ // Derived boolean accessors — `{{#hasFoo}}` over a payload field `foo`.
2
+ //
3
+ // A prompt needs conditional sections ("include the abilities block only when there
4
+ // ARE abilities"), and the payload contract answers that with a DERIVED accessor
5
+ // rather than an authored boolean field: the author declares `abilities`, and
6
+ // `hasAbilities` follows from it. Declaring both would let them disagree.
7
+ //
8
+ // THE RULE IS SHARED ON PURPOSE. The JVM has carried this since 7.7.7
9
+ // (`com.metaobjects.render.PayloadAccessors`, emitted by `SpringPayloadGenerator`
10
+ // onto every generated payload record and accepted by `render.Verify`), and its
11
+ // comment says the emitter and the verifier share one rule so they "can never drift
12
+ // apart". TypeScript had neither half, which is why the same template verified clean
13
+ // on the JVM and reported drift here — and, worse, RENDERED WRONG rather than
14
+ // failing: `{{#hasAbilities}}` resolved to nothing on a populated payload, so the
15
+ // section silently vanished. This module is the TS half of that shared rule.
16
+ /** The `has` prefix every derived boolean accessor carries. */
17
+ export const HAS_PREFIX = "has";
18
+ /**
19
+ * The boolean-accessor section name for a payload field: `"has" + capitalize(name)`
20
+ * (`abilities` → `hasAbilities`). Byte-identical to the JVM's
21
+ * `PayloadAccessors.hasAccessorName`, including its capitalize, which leaves an
22
+ * already-uppercase first character untouched.
23
+ */
24
+ export function hasAccessorName(fieldName) {
25
+ return HAS_PREFIX + capitalize(fieldName);
26
+ }
27
+ /** Capitalize the first character, leaving an already-uppercase one untouched. */
28
+ export function capitalize(s) {
29
+ if (s.length === 0)
30
+ return s;
31
+ const c0 = s.charAt(0);
32
+ if (c0 === c0.toUpperCase() && c0 !== c0.toLowerCase())
33
+ return s;
34
+ return c0.toUpperCase() + s.slice(1);
35
+ }
36
+ /**
37
+ * Is `value` "present" for the purposes of `has<Field>`?
38
+ *
39
+ * Mirrors the JVM emitter's per-type bodies exactly:
40
+ * string → non-null AND non-blank (`!foo.isBlank()`, so whitespace is absent)
41
+ * array → non-null AND non-empty (`!foo.isEmpty()`)
42
+ * reference → non-null (any other object)
43
+ *
44
+ * Returns `undefined` for numbers and booleans, which the JVM deliberately emits NO
45
+ * accessor for — they are always-present scalars, and a `{{#hasCount}}` over an int
46
+ * is drift rather than a conditional. Returning undefined (rather than false) keeps
47
+ * that distinction: nothing is injected, so the name stays unresolved exactly as it
48
+ * is on a generated Java record that has no such method.
49
+ */
50
+ export function accessorValue(value) {
51
+ if (value === null || value === undefined)
52
+ return false;
53
+ if (typeof value === "string")
54
+ return value.trim().length > 0;
55
+ if (Array.isArray(value))
56
+ return value.length > 0;
57
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
58
+ return undefined;
59
+ }
60
+ return typeof value === "object";
61
+ }
62
+ /**
63
+ * A view over `payload` carrying its derived `has<Field>` accessors, recursively.
64
+ *
65
+ * NON-MUTATING — the caller's payload is never touched, because a render must not
66
+ * be able to change the object it was handed. An AUTHORED key always wins: if a
67
+ * payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
68
+ * derived one.
69
+ *
70
+ * Recursion follows Mustache's own scoping: every nested object and every array
71
+ * ELEMENT becomes a context in its own right, so a section over `abilities` sees
72
+ * the accessors of the ability it is currently iterating.
73
+ */
74
+ export function withDerivedAccessors(payload, depth = 0) {
75
+ if (depth > 32)
76
+ return payload; // pathological graph; render is not a validator
77
+ if (Array.isArray(payload)) {
78
+ return payload.map((v) => withDerivedAccessors(v, depth + 1));
79
+ }
80
+ if (payload === null || typeof payload !== "object")
81
+ return payload;
82
+ // PLAIN objects only. Rebuilding from Object.entries() would flatten anything with
83
+ // its own prototype — a Date stringifies to "[object Object]" and a class instance
84
+ // loses its getters — and the other ports only rebuild map-shaped values, so
85
+ // rebuilding more here would be a divergence as well as a regression.
86
+ const proto = Object.getPrototypeOf(payload);
87
+ if (proto !== Object.prototype && proto !== null)
88
+ return payload;
89
+ const src = payload;
90
+ const out = {};
91
+ for (const [k, v] of Object.entries(src))
92
+ out[k] = withDerivedAccessors(v, depth + 1);
93
+ for (const [k, v] of Object.entries(src)) {
94
+ const name = hasAccessorName(k);
95
+ if (Object.prototype.hasOwnProperty.call(src, name))
96
+ continue; // authored wins
97
+ const derived = accessorValue(v);
98
+ if (derived !== undefined)
99
+ out[name] = derived;
100
+ }
101
+ return out;
102
+ }
103
+ //# sourceMappingURL=payload-accessors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-accessors.js","sourceRoot":"","sources":["../src/payload-accessors.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAoBlF,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CA+B/C"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAqBlF,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAkC/C"}
package/dist/render.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Mustache from "mustache";
2
2
  import { ESCAPERS } from "./escapers.js";
3
3
  import { verify, ERR_REQUIRED_SLOT_UNUSED } from "./verify.js";
4
+ import { withDerivedAccessors } from "./payload-accessors.js";
4
5
  const MAX_DEPTH = 32;
5
6
  const PARTIAL = /\{\{>\s*([^}\s]+)\s*\}\}/g;
6
7
  // Resolve `{{> group/source }}` partials by inlining their text, recursively,
@@ -37,7 +38,10 @@ export function render(o) {
37
38
  Mustache.escape = (v) => escaper(typeof v === "string" ? v : String(v));
38
39
  let result;
39
40
  try {
40
- result = Mustache.render(expanded, o.payload, {});
41
+ // Derived `has<Field>` accessors are part of the payload contract, not of the
42
+ // caller's object — see payload-accessors.ts. Injected here so a `{{#hasFoo}}`
43
+ // section resolves the same way it does against a generated JVM payload record.
44
+ result = Mustache.render(expanded, withDerivedAccessors(o.payload), {});
41
45
  }
42
46
  finally {
43
47
  Mustache.escape = prev;
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAMD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,IAAI,SAAS,CAAC,EAAE,EAAE,CAAC;AAMjF;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,YAAY,EAC5D,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EACtB,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,SAAS,CAcf;AASD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAE3E;AAsBD;;;;GAIG;AACH,wBAAgB,MAAM,CACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,YAAY,EAAE,EACtB,IAAI,CAAC,EAAE,aAAa,GACnB,WAAW,EAAE,CAuFf"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAMD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,IAAI,SAAS,CAAC,EAAE,EAAE,CAAC;AA2BjF;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,YAAY,EAC5D,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EACtB,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,SAAS,CAcf;AASD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAE3E;AAsBD;;;;GAIG;AACH,wBAAgB,MAAM,CACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,YAAY,EAAE,EACtB,IAAI,CAAC,EAAE,aAAa,GACnB,WAAW,EAAE,CA+Ff"}
package/dist/verify.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // metadata import). The CLI derives that tree from the loaded object.value and
10
10
  // passes it in — keeping this engine a standalone, byte-portable module.
11
11
  import Mustache from "mustache";
12
+ import { HAS_PREFIX, hasAccessorName } from "./payload-accessors.js";
12
13
  /** A `{{var}}` references a field the (contextual) payload does not declare. */
13
14
  export const ERR_VAR_NOT_ON_PAYLOAD = "ERR_VAR_NOT_ON_PAYLOAD";
14
15
  /** A `{{> ref}}` partial does not resolve in the provider. */
@@ -18,6 +19,30 @@ export const ERR_REQUIRED_SLOT_UNUSED = "ERR_REQUIRED_SLOT_UNUSED";
18
19
  /** A declared @requiredTags output tag is absent from the template text. */
19
20
  export const ERR_OUTPUT_TAG_MISSING = "ERR_OUTPUT_TAG_MISSING";
20
21
  const MAX_DEPTH = 32;
22
+ /**
23
+ * True when `name` is a derived boolean accessor (`has<Field>`) over a field
24
+ * reachable on the current context stack — the same rule the payload emitter uses
25
+ * (payload-accessors.ts), so an accepted section and an emitted accessor can never
26
+ * drift apart. A `{{#hasX}}` with no field `x` on any scope is NOT an accessor and
27
+ * stays ERR_VAR_NOT_ON_PAYLOAD drift.
28
+ *
29
+ * Accessors are simple (undotted) names; a dotted path is never an accessor and is
30
+ * left to normal field resolution. Byte-identical to the JVM's
31
+ * `Verify.isBooleanAccessor`, including its deliberate permissiveness: acceptance
32
+ * keys off the FIELD EXISTING, not off its type.
33
+ */
34
+ function isBooleanAccessor(stack, name) {
35
+ if (name.includes("."))
36
+ return false;
37
+ if (!name.startsWith(HAS_PREFIX))
38
+ return false;
39
+ for (let i = stack.length - 1; i >= 0; i--) {
40
+ for (const f of stack[i])
41
+ if (name === hasAccessorName(f.name))
42
+ return true;
43
+ }
44
+ return false;
45
+ }
21
46
  function find(fields, name) {
22
47
  return fields.find((f) => f.name === name);
23
48
  }
@@ -105,7 +130,7 @@ export function verify(templateText, fields, opts) {
105
130
  break; // implicit iterator — always valid
106
131
  if (atRoot)
107
132
  referencedAtRoot.add(value.split(".")[0]);
108
- if (!resolve(stack, value))
133
+ if (!resolve(stack, value) && !isBooleanAccessor(stack, value))
109
134
  errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
110
135
  break;
111
136
  }
@@ -121,6 +146,13 @@ export function verify(templateText, fields, opts) {
121
146
  referencedAtRoot.add(value.split(".")[0]);
122
147
  const field = resolve(stack, value);
123
148
  if (!field) {
149
+ // A derived `has<Field>` gate is a BOOLEAN over the current context, so it
150
+ // resolves nothing and pushes nothing — walk the body in the SAME scope,
151
+ // which is what `{{#hasAbilities}}{{#abilities}}…` depends on.
152
+ if (isBooleanAccessor(stack, value)) {
153
+ walk(sub, stack, seen);
154
+ break;
155
+ }
124
156
  // Unresolved section head is itself drift; skip the body (its
125
157
  // context is unknowable, walking it would cascade false errors).
126
158
  errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/render",
3
- "version": "0.22.1",
3
+ "version": "0.23.1-rc.1",
4
4
  "description": "Logic-less, deterministic text render engine (Mustache) for MetaObjects templates — provider-resolved partials, format-driven escaping, zero core dependency.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,97 @@
1
+ // Derived boolean accessors — `{{#hasFoo}}` over a payload field `foo`.
2
+ //
3
+ // A prompt needs conditional sections ("include the abilities block only when there
4
+ // ARE abilities"), and the payload contract answers that with a DERIVED accessor
5
+ // rather than an authored boolean field: the author declares `abilities`, and
6
+ // `hasAbilities` follows from it. Declaring both would let them disagree.
7
+ //
8
+ // THE RULE IS SHARED ON PURPOSE. The JVM has carried this since 7.7.7
9
+ // (`com.metaobjects.render.PayloadAccessors`, emitted by `SpringPayloadGenerator`
10
+ // onto every generated payload record and accepted by `render.Verify`), and its
11
+ // comment says the emitter and the verifier share one rule so they "can never drift
12
+ // apart". TypeScript had neither half, which is why the same template verified clean
13
+ // on the JVM and reported drift here — and, worse, RENDERED WRONG rather than
14
+ // failing: `{{#hasAbilities}}` resolved to nothing on a populated payload, so the
15
+ // section silently vanished. This module is the TS half of that shared rule.
16
+
17
+ /** The `has` prefix every derived boolean accessor carries. */
18
+ export const HAS_PREFIX = "has";
19
+
20
+ /**
21
+ * The boolean-accessor section name for a payload field: `"has" + capitalize(name)`
22
+ * (`abilities` → `hasAbilities`). Byte-identical to the JVM's
23
+ * `PayloadAccessors.hasAccessorName`, including its capitalize, which leaves an
24
+ * already-uppercase first character untouched.
25
+ */
26
+ export function hasAccessorName(fieldName: string): string {
27
+ return HAS_PREFIX + capitalize(fieldName);
28
+ }
29
+
30
+ /** Capitalize the first character, leaving an already-uppercase one untouched. */
31
+ export function capitalize(s: string): string {
32
+ if (s.length === 0) return s;
33
+ const c0 = s.charAt(0);
34
+ if (c0 === c0.toUpperCase() && c0 !== c0.toLowerCase()) return s;
35
+ return c0.toUpperCase() + s.slice(1);
36
+ }
37
+
38
+ /**
39
+ * Is `value` "present" for the purposes of `has<Field>`?
40
+ *
41
+ * Mirrors the JVM emitter's per-type bodies exactly:
42
+ * string → non-null AND non-blank (`!foo.isBlank()`, so whitespace is absent)
43
+ * array → non-null AND non-empty (`!foo.isEmpty()`)
44
+ * reference → non-null (any other object)
45
+ *
46
+ * Returns `undefined` for numbers and booleans, which the JVM deliberately emits NO
47
+ * accessor for — they are always-present scalars, and a `{{#hasCount}}` over an int
48
+ * is drift rather than a conditional. Returning undefined (rather than false) keeps
49
+ * that distinction: nothing is injected, so the name stays unresolved exactly as it
50
+ * is on a generated Java record that has no such method.
51
+ */
52
+ export function accessorValue(value: unknown): boolean | undefined {
53
+ if (value === null || value === undefined) return false;
54
+ if (typeof value === "string") return value.trim().length > 0;
55
+ if (Array.isArray(value)) return value.length > 0;
56
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
57
+ return undefined;
58
+ }
59
+ return typeof value === "object";
60
+ }
61
+
62
+ /**
63
+ * A view over `payload` carrying its derived `has<Field>` accessors, recursively.
64
+ *
65
+ * NON-MUTATING — the caller's payload is never touched, because a render must not
66
+ * be able to change the object it was handed. An AUTHORED key always wins: if a
67
+ * payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
68
+ * derived one.
69
+ *
70
+ * Recursion follows Mustache's own scoping: every nested object and every array
71
+ * ELEMENT becomes a context in its own right, so a section over `abilities` sees
72
+ * the accessors of the ability it is currently iterating.
73
+ */
74
+ export function withDerivedAccessors<T>(payload: T, depth = 0): T {
75
+ if (depth > 32) return payload; // pathological graph; render is not a validator
76
+ if (Array.isArray(payload)) {
77
+ return payload.map((v) => withDerivedAccessors(v, depth + 1)) as unknown as T;
78
+ }
79
+ if (payload === null || typeof payload !== "object") return payload;
80
+ // PLAIN objects only. Rebuilding from Object.entries() would flatten anything with
81
+ // its own prototype — a Date stringifies to "[object Object]" and a class instance
82
+ // loses its getters — and the other ports only rebuild map-shaped values, so
83
+ // rebuilding more here would be a divergence as well as a regression.
84
+ const proto = Object.getPrototypeOf(payload);
85
+ if (proto !== Object.prototype && proto !== null) return payload;
86
+
87
+ const src = payload as Record<string, unknown>;
88
+ const out: Record<string, unknown> = {};
89
+ for (const [k, v] of Object.entries(src)) out[k] = withDerivedAccessors(v, depth + 1);
90
+ for (const [k, v] of Object.entries(src)) {
91
+ const name = hasAccessorName(k);
92
+ if (Object.prototype.hasOwnProperty.call(src, name)) continue; // authored wins
93
+ const derived = accessorValue(v);
94
+ if (derived !== undefined) out[name] = derived;
95
+ }
96
+ return out as unknown as T;
97
+ }
package/src/render.ts CHANGED
@@ -2,6 +2,7 @@ import Mustache from "mustache";
2
2
  import type { Provider } from "./provider.js";
3
3
  import { ESCAPERS, type RenderFormat } from "./escapers.js";
4
4
  import { verify, ERR_REQUIRED_SLOT_UNUSED, type PayloadField } from "./verify.js";
5
+ import { withDerivedAccessors } from "./payload-accessors.js";
5
6
 
6
7
  const MAX_DEPTH = 32;
7
8
  const PARTIAL = /\{\{>\s*([^}\s]+)\s*\}\}/g;
@@ -68,7 +69,10 @@ export function render(o: RenderOptions): string {
68
69
  Mustache.escape = (v: unknown) => escaper(typeof v === "string" ? v : String(v));
69
70
  let result: string;
70
71
  try {
71
- result = Mustache.render(expanded, o.payload, {});
72
+ // Derived `has<Field>` accessors are part of the payload contract, not of the
73
+ // caller's object — see payload-accessors.ts. Injected here so a `{{#hasFoo}}`
74
+ // section resolves the same way it does against a generated JVM payload record.
75
+ result = Mustache.render(expanded, withDerivedAccessors(o.payload), {});
72
76
  } finally {
73
77
  Mustache.escape = prev;
74
78
  }
package/src/verify.ts CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  import Mustache from "mustache";
13
13
  import type { Provider } from "./provider.js";
14
+ import { HAS_PREFIX, hasAccessorName } from "./payload-accessors.js";
14
15
 
15
16
  /** A `{{var}}` references a field the (contextual) payload does not declare. */
16
17
  export const ERR_VAR_NOT_ON_PAYLOAD = "ERR_VAR_NOT_ON_PAYLOAD";
@@ -62,6 +63,27 @@ type Token = readonly unknown[];
62
63
  */
63
64
  export type ResolveStack<F extends PayloadField = PayloadField> = readonly F[][];
64
65
 
66
+ /**
67
+ * True when `name` is a derived boolean accessor (`has<Field>`) over a field
68
+ * reachable on the current context stack — the same rule the payload emitter uses
69
+ * (payload-accessors.ts), so an accepted section and an emitted accessor can never
70
+ * drift apart. A `{{#hasX}}` with no field `x` on any scope is NOT an accessor and
71
+ * stays ERR_VAR_NOT_ON_PAYLOAD drift.
72
+ *
73
+ * Accessors are simple (undotted) names; a dotted path is never an accessor and is
74
+ * left to normal field resolution. Byte-identical to the JVM's
75
+ * `Verify.isBooleanAccessor`, including its deliberate permissiveness: acceptance
76
+ * keys off the FIELD EXISTING, not off its type.
77
+ */
78
+ function isBooleanAccessor<F extends PayloadField>(stack: ResolveStack<F>, name: string): boolean {
79
+ if (name.includes(".")) return false;
80
+ if (!name.startsWith(HAS_PREFIX)) return false;
81
+ for (let i = stack.length - 1; i >= 0; i--) {
82
+ for (const f of stack[i]!) if (name === hasAccessorName(f.name)) return true;
83
+ }
84
+ return false;
85
+ }
86
+
65
87
  function find<F extends PayloadField>(fields: F[], name: string): F | undefined {
66
88
  return fields.find((f) => f.name === name);
67
89
  }
@@ -162,7 +184,8 @@ export function verify(
162
184
  // {{{x}}} (spec); mustache.js emits "&" for it too
163
185
  if (value === ".") break; // implicit iterator — always valid
164
186
  if (atRoot) referencedAtRoot.add(value.split(".")[0]!);
165
- if (!resolve(stack, value)) errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
187
+ if (!resolve(stack, value) && !isBooleanAccessor(stack, value))
188
+ errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
166
189
  break;
167
190
  }
168
191
  case "#": // {{#x}}…{{/x}}
@@ -176,6 +199,13 @@ export function verify(
176
199
  if (atRoot) referencedAtRoot.add(value.split(".")[0]!);
177
200
  const field = resolve(stack, value);
178
201
  if (!field) {
202
+ // A derived `has<Field>` gate is a BOOLEAN over the current context, so it
203
+ // resolves nothing and pushes nothing — walk the body in the SAME scope,
204
+ // which is what `{{#hasAbilities}}{{#abilities}}…` depends on.
205
+ if (isBooleanAccessor(stack, value)) {
206
+ walk(sub, stack, seen);
207
+ break;
208
+ }
179
209
  // Unresolved section head is itself drift; skip the body (its
180
210
  // context is unknowable, walking it would cascade false errors).
181
211
  errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });