@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.
- package/dist/payload-accessors.d.ts +40 -0
- package/dist/payload-accessors.d.ts.map +1 -0
- package/dist/payload-accessors.js +103 -0
- package/dist/payload-accessors.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +5 -1
- package/dist/render.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +33 -1
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/payload-accessors.ts +97 -0
- package/src/render.ts +5 -1
- package/src/verify.ts +31 -1
|
@@ -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":"AAAA,wEAAwE;AACxE,EAAE;AACF,oFAAoF;AACpF,iFAAiF;AACjF,8EAA8E;AAC9E,0EAA0E;AAC1E,EAAE;AACF,sEAAsE;AACtE,kFAAkF;AAClF,gFAAgF;AAChF,oFAAoF;AACpF,qFAAqF;AACrF,8EAA8E;AAC9E,kFAAkF;AAClF,6EAA6E;AAE7E,+DAA+D;AAC/D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,OAAO,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAC7B,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,KAAK,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,WAAW,EAAE;QAAE,OAAO,CAAC,CAAC;IACjE,OAAO,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAI,OAAU,EAAE,KAAK,GAAG,CAAC;IAC3D,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,OAAO,CAAC,CAAC,gDAAgD;IAChF,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAiB,CAAC;IAChF,CAAC;IACD,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IACpE,mFAAmF;IACnF,mFAAmF;IACnF,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IAEjE,MAAM,GAAG,GAAG,OAAkC,CAAC;IAC/C,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IACtF,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC;YAAE,SAAS,CAAC,gBAAgB;QAC/E,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,OAAO,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;IACjD,CAAC;IACD,OAAO,GAAmB,CAAC;AAC7B,CAAC"}
|
package/dist/render.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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;
|
package/dist/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAqB,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,wBAAwB,EAAqB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -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;
|
|
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 });
|
package/dist/verify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,8EAA8E;AAC9E,sDAAsD;AACtD,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,yEAAyE;AAEzE,OAAO,QAAQ,MAAM,UAAU,CAAC;
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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 });
|