@metaobjectsdev/render 1.0.13 → 1.1.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 +16 -2
- package/dist/payload-accessors.d.ts.map +1 -1
- package/dist/payload-accessors.js +80 -9
- package/dist/payload-accessors.js.map +1 -1
- package/dist/verify.d.ts +4 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +28 -7
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/payload-accessors.ts +79 -10
- package/src/verify.ts +33 -8
|
@@ -7,6 +7,15 @@ export declare const HAS_PREFIX = "has";
|
|
|
7
7
|
* already-uppercase first character untouched.
|
|
8
8
|
*/
|
|
9
9
|
export declare function hasAccessorName(fieldName: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* The positional accessor every OBJECT element of a collection carries, so a list separator
|
|
12
|
+
* needs no hand-computed `last` flag in the payload:
|
|
13
|
+
* `{{#items}}{{name}}{{^-last}}, {{/-last}}{{/items}}`. A boolean on the element; the leading
|
|
14
|
+
* `-` cannot collide with a field name, and it is still an ordinary Mustache tag name, so the
|
|
15
|
+
* template stays valid Mustache. A scalar element has nowhere to hold a key and carries none.
|
|
16
|
+
* An authored member of the same name wins, as it does for `has<Field>`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const LAST_ACCESSOR = "-last";
|
|
10
19
|
/** Capitalize the first character, leaving an already-uppercase one untouched. */
|
|
11
20
|
export declare function capitalize(s: string): string;
|
|
12
21
|
/**
|
|
@@ -25,16 +34,21 @@ export declare function capitalize(s: string): string;
|
|
|
25
34
|
*/
|
|
26
35
|
export declare function accessorValue(value: unknown): boolean | undefined;
|
|
27
36
|
/**
|
|
28
|
-
* A view over `payload` carrying its derived `has<Field>`
|
|
37
|
+
* A view over `payload` carrying its derived accessors, recursively: `has<Field>` on every
|
|
38
|
+
* object, and `-last` on every object that is an element of a collection.
|
|
29
39
|
*
|
|
30
40
|
* 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
|
|
41
|
+
* be able to change the object it was handed. An AUTHORED member always wins: if a
|
|
32
42
|
* payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
|
|
33
43
|
* derived one.
|
|
34
44
|
*
|
|
35
45
|
* Recursion follows Mustache's own scoping: every nested object and every array
|
|
36
46
|
* ELEMENT becomes a context in its own right, so a section over `abilities` sees
|
|
37
47
|
* the accessors of the ability it is currently iterating.
|
|
48
|
+
*
|
|
49
|
+
* The payload's shape does not change the result. A plain object is copied with its
|
|
50
|
+
* accessors added; anything with its own prototype (a class instance, a Date) is read
|
|
51
|
+
* THROUGH a view instead, so its getters, methods and string form stay exactly its own.
|
|
38
52
|
*/
|
|
39
53
|
export declare function withDerivedAccessors<T>(payload: T, depth?: number): T;
|
|
40
54
|
//# sourceMappingURL=payload-accessors.d.ts.map
|
|
@@ -1 +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
|
|
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;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,SAAI,GAAG,CAAC,CAEhE"}
|
|
@@ -24,6 +24,15 @@ export const HAS_PREFIX = "has";
|
|
|
24
24
|
export function hasAccessorName(fieldName) {
|
|
25
25
|
return HAS_PREFIX + capitalize(fieldName);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The positional accessor every OBJECT element of a collection carries, so a list separator
|
|
29
|
+
* needs no hand-computed `last` flag in the payload:
|
|
30
|
+
* `{{#items}}{{name}}{{^-last}}, {{/-last}}{{/items}}`. A boolean on the element; the leading
|
|
31
|
+
* `-` cannot collide with a field name, and it is still an ordinary Mustache tag name, so the
|
|
32
|
+
* template stays valid Mustache. A scalar element has nowhere to hold a key and carries none.
|
|
33
|
+
* An authored member of the same name wins, as it does for `has<Field>`.
|
|
34
|
+
*/
|
|
35
|
+
export const LAST_ACCESSOR = "-last";
|
|
27
36
|
/** Capitalize the first character, leaving an already-uppercase one untouched. */
|
|
28
37
|
export function capitalize(s) {
|
|
29
38
|
if (s.length === 0)
|
|
@@ -60,36 +69,45 @@ export function accessorValue(value) {
|
|
|
60
69
|
return typeof value === "object";
|
|
61
70
|
}
|
|
62
71
|
/**
|
|
63
|
-
* A view over `payload` carrying its derived `has<Field>`
|
|
72
|
+
* A view over `payload` carrying its derived accessors, recursively: `has<Field>` on every
|
|
73
|
+
* object, and `-last` on every object that is an element of a collection.
|
|
64
74
|
*
|
|
65
75
|
* 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
|
|
76
|
+
* be able to change the object it was handed. An AUTHORED member always wins: if a
|
|
67
77
|
* payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
|
|
68
78
|
* derived one.
|
|
69
79
|
*
|
|
70
80
|
* Recursion follows Mustache's own scoping: every nested object and every array
|
|
71
81
|
* ELEMENT becomes a context in its own right, so a section over `abilities` sees
|
|
72
82
|
* the accessors of the ability it is currently iterating.
|
|
83
|
+
*
|
|
84
|
+
* The payload's shape does not change the result. A plain object is copied with its
|
|
85
|
+
* accessors added; anything with its own prototype (a class instance, a Date) is read
|
|
86
|
+
* THROUGH a view instead, so its getters, methods and string form stay exactly its own.
|
|
73
87
|
*/
|
|
74
88
|
export function withDerivedAccessors(payload, depth = 0) {
|
|
89
|
+
return derive(payload, depth, undefined);
|
|
90
|
+
}
|
|
91
|
+
/** `last` is set only for an element of a collection: its position in that collection. */
|
|
92
|
+
function derive(payload, depth, last) {
|
|
75
93
|
if (depth > 32)
|
|
76
94
|
return payload; // pathological graph; render is not a validator
|
|
77
95
|
if (Array.isArray(payload)) {
|
|
78
|
-
|
|
96
|
+
const end = payload.length - 1;
|
|
97
|
+
return payload.map((v, i) => derive(v, depth + 1, i === end));
|
|
79
98
|
}
|
|
80
99
|
if (payload === null || typeof payload !== "object")
|
|
81
100
|
return payload;
|
|
82
|
-
// PLAIN
|
|
83
|
-
// its own prototype — a Date stringifies to "[object Object]" and a
|
|
84
|
-
// loses its getters —
|
|
85
|
-
// rebuilding more here would be a divergence as well as a regression.
|
|
101
|
+
// Only a PLAIN object is rebuilt. Rebuilding from Object.entries() would flatten
|
|
102
|
+
// anything with its own prototype — a Date stringifies to "[object Object]" and a
|
|
103
|
+
// class instance loses its getters — so those are viewed, never copied.
|
|
86
104
|
const proto = Object.getPrototypeOf(payload);
|
|
87
105
|
if (proto !== Object.prototype && proto !== null)
|
|
88
|
-
return payload;
|
|
106
|
+
return typedView(payload, depth, last);
|
|
89
107
|
const src = payload;
|
|
90
108
|
const out = {};
|
|
91
109
|
for (const [k, v] of Object.entries(src))
|
|
92
|
-
out[k] =
|
|
110
|
+
out[k] = derive(v, depth + 1, undefined);
|
|
93
111
|
for (const [k, v] of Object.entries(src)) {
|
|
94
112
|
const name = hasAccessorName(k);
|
|
95
113
|
if (Object.prototype.hasOwnProperty.call(src, name))
|
|
@@ -98,6 +116,59 @@ export function withDerivedAccessors(payload, depth = 0) {
|
|
|
98
116
|
if (derived !== undefined)
|
|
99
117
|
out[name] = derived;
|
|
100
118
|
}
|
|
119
|
+
if (last !== undefined && !Object.prototype.hasOwnProperty.call(src, LAST_ACCESSOR)) {
|
|
120
|
+
out[LAST_ACCESSOR] = last;
|
|
121
|
+
}
|
|
101
122
|
return out;
|
|
102
123
|
}
|
|
124
|
+
/** The first character lowercased — the inverse of `capitalize` for a camelCase field name. */
|
|
125
|
+
function decapitalize(s) {
|
|
126
|
+
return s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The derived-accessor view of an object that has its own prototype. Every member the
|
|
130
|
+
* object has resolves to the object's own — a getter runs against the object itself, a
|
|
131
|
+
* method stays bound to it — so the Mustache engine sees the names and values it always
|
|
132
|
+
* did. Only a name the object does NOT have can resolve to a derived accessor.
|
|
133
|
+
*
|
|
134
|
+
* The proxy's target is an empty stand-in, not the object: a frozen payload would
|
|
135
|
+
* otherwise forbid returning the derived view of one of its nested values.
|
|
136
|
+
*/
|
|
137
|
+
function typedView(target, depth, last) {
|
|
138
|
+
const members = new Map();
|
|
139
|
+
const member = (key) => {
|
|
140
|
+
if (members.has(key))
|
|
141
|
+
return members.get(key);
|
|
142
|
+
const raw = Reflect.get(target, key, target);
|
|
143
|
+
const value = typeof raw === "function"
|
|
144
|
+
? (...args) => derive(Reflect.apply(raw, target, args), depth + 1, undefined)
|
|
145
|
+
: derive(raw, depth + 1, undefined);
|
|
146
|
+
members.set(key, value);
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
const derived = (key) => {
|
|
150
|
+
if (typeof key !== "string")
|
|
151
|
+
return undefined;
|
|
152
|
+
if (key === LAST_ACCESSOR)
|
|
153
|
+
return last;
|
|
154
|
+
if (!key.startsWith(HAS_PREFIX))
|
|
155
|
+
return undefined;
|
|
156
|
+
const rest = key.slice(HAS_PREFIX.length);
|
|
157
|
+
for (const field of [decapitalize(rest), rest]) {
|
|
158
|
+
if (!(field in target) || hasAccessorName(field) !== key)
|
|
159
|
+
continue;
|
|
160
|
+
const raw = Reflect.get(target, field, target);
|
|
161
|
+
if (typeof raw === "function")
|
|
162
|
+
continue;
|
|
163
|
+
const value = accessorValue(raw);
|
|
164
|
+
if (value !== undefined)
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
};
|
|
169
|
+
return new Proxy(Object.create(null), {
|
|
170
|
+
has: (_stand, key) => key in target || derived(key) !== undefined,
|
|
171
|
+
get: (_stand, key) => (key in target ? member(key) : derived(key)),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
103
174
|
//# sourceMappingURL=payload-accessors.js.map
|
|
@@ -1 +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,uFAAuF;AACvF,iFAAiF;AACjF,oFAAoF;AACpF,2IAA2I;AAC3I,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
|
|
1
|
+
{"version":3,"file":"payload-accessors.js","sourceRoot":"","sources":["../src/payload-accessors.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts
CHANGED
|
@@ -10,11 +10,14 @@ export declare const ERR_OUTPUT_TAG_MISSING = "ERR_OUTPUT_TAG_MISSING";
|
|
|
10
10
|
/**
|
|
11
11
|
* A plain field-tree node mirroring an `object.value` view-object's field walk.
|
|
12
12
|
* `fields` present → a context-pushing field (object / array-of-object); absent
|
|
13
|
-
* → a scalar (string/number/boolean/scalar-array).
|
|
13
|
+
* → a scalar (string/number/boolean/scalar-array). `isArray` is the field's
|
|
14
|
+
* effective array-ness: with `fields` it is a list of objects, without it a list
|
|
15
|
+
* of scalars.
|
|
14
16
|
*/
|
|
15
17
|
export interface PayloadField {
|
|
16
18
|
name: string;
|
|
17
19
|
fields?: PayloadField[];
|
|
20
|
+
isArray?: boolean;
|
|
18
21
|
}
|
|
19
22
|
export interface VerifyError {
|
|
20
23
|
code: string;
|
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;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
|
|
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;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;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;AAsCjF;;;;;;;;;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,CA0Gf"}
|
package/dist/verify.js
CHANGED
|
@@ -9,7 +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
|
+
import { HAS_PREFIX, LAST_ACCESSOR, hasAccessorName } from "./payload-accessors.js";
|
|
13
13
|
/** A `{{var}}` references a field the (contextual) payload does not declare. */
|
|
14
14
|
export const ERR_VAR_NOT_ON_PAYLOAD = "ERR_VAR_NOT_ON_PAYLOAD";
|
|
15
15
|
/** A `{{> ref}}` partial does not resolve in the provider. */
|
|
@@ -43,6 +43,16 @@ function isBooleanAccessor(stack, name) {
|
|
|
43
43
|
}
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* True when `name` is the positional accessor `-last` and it is available in the current scope.
|
|
48
|
+
* `positional` is true exactly when the innermost enclosing ARRAY section iterates object
|
|
49
|
+
* elements — the engine puts `-last` on each of those (payload-accessors.ts). An array of
|
|
50
|
+
* scalars makes it unavailable even inside an outer object list: a scalar element carries no
|
|
51
|
+
* key, so the lookup would bind to the OUTER element's position and render wrong.
|
|
52
|
+
*/
|
|
53
|
+
function isPositionalAccessor(positional, name) {
|
|
54
|
+
return positional && name === LAST_ACCESSOR;
|
|
55
|
+
}
|
|
46
56
|
function find(fields, name) {
|
|
47
57
|
return fields.find((f) => f.name === name);
|
|
48
58
|
}
|
|
@@ -116,7 +126,7 @@ export function verify(templateText, fields, opts) {
|
|
|
116
126
|
// provider-resolved partial body, collected during the single walk below
|
|
117
127
|
// (no second resolution pass).
|
|
118
128
|
const staticTexts = [templateText];
|
|
119
|
-
function walk(tokens, stack, seen) {
|
|
129
|
+
function walk(tokens, stack, seen, positional = false) {
|
|
120
130
|
const atRoot = stack.length === 1 && stack[0] === root;
|
|
121
131
|
for (const tok of tokens) {
|
|
122
132
|
const type = tok[0];
|
|
@@ -130,7 +140,7 @@ export function verify(templateText, fields, opts) {
|
|
|
130
140
|
break; // implicit iterator — always valid
|
|
131
141
|
if (atRoot)
|
|
132
142
|
referencedAtRoot.add(value.split(".")[0]);
|
|
133
|
-
if (!resolve(stack, value) && !isBooleanAccessor(stack, value))
|
|
143
|
+
if (!resolve(stack, value) && !isBooleanAccessor(stack, value) && !isPositionalAccessor(positional, value))
|
|
134
144
|
errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
|
|
135
145
|
break;
|
|
136
146
|
}
|
|
@@ -139,18 +149,24 @@ export function verify(templateText, fields, opts) {
|
|
|
139
149
|
// {{^x}}…{{/x}}
|
|
140
150
|
const sub = Array.isArray(tok[4]) ? tok[4] : [];
|
|
141
151
|
if (value === ".") {
|
|
142
|
-
walk(sub, stack, seen);
|
|
152
|
+
walk(sub, stack, seen, positional);
|
|
143
153
|
break;
|
|
144
154
|
}
|
|
145
155
|
if (atRoot)
|
|
146
156
|
referencedAtRoot.add(value.split(".")[0]);
|
|
147
157
|
const field = resolve(stack, value);
|
|
148
158
|
if (!field) {
|
|
159
|
+
// A positional `-last` gate is a BOOLEAN on the current element: it pushes
|
|
160
|
+
// nothing, so the body walks in the SAME scope.
|
|
161
|
+
if (isPositionalAccessor(positional, value)) {
|
|
162
|
+
walk(sub, stack, seen, positional);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
149
165
|
// A derived `has<Field>` gate is a BOOLEAN over the current context, so it
|
|
150
166
|
// resolves nothing and pushes nothing — walk the body in the SAME scope,
|
|
151
167
|
// which is what `{{#hasAbilities}}{{#abilities}}…` depends on.
|
|
152
168
|
if (isBooleanAccessor(stack, value)) {
|
|
153
|
-
walk(sub, stack, seen);
|
|
169
|
+
walk(sub, stack, seen, positional);
|
|
154
170
|
break;
|
|
155
171
|
}
|
|
156
172
|
// Unresolved section head is itself drift; skip the body (its
|
|
@@ -161,7 +177,12 @@ export function verify(templateText, fields, opts) {
|
|
|
161
177
|
// `#` over a container pushes its element fields; `^` (and `#` over a
|
|
162
178
|
// scalar, used as a conditional) keep the current context.
|
|
163
179
|
const push = type === "#" && field.fields !== undefined;
|
|
164
|
-
|
|
180
|
+
// Only a `#` over an ARRAY iterates, so only it moves the positional scope: object
|
|
181
|
+
// elements carry `-last`, scalar elements do not. A section over a non-array field
|
|
182
|
+
// (a scalar conditional, a single nested object) and every `^` section are
|
|
183
|
+
// transparent — the enclosing element's position is still the one in reach.
|
|
184
|
+
const iterates = type === "#" && field.isArray === true;
|
|
185
|
+
walk(sub, push ? [...stack, field.fields] : stack, seen, iterates ? field.fields !== undefined : positional);
|
|
165
186
|
break;
|
|
166
187
|
}
|
|
167
188
|
case ">": {
|
|
@@ -176,7 +197,7 @@ export function verify(templateText, fields, opts) {
|
|
|
176
197
|
break;
|
|
177
198
|
}
|
|
178
199
|
staticTexts.push(text);
|
|
179
|
-
walk(parse(text), stack, [...seen, value]);
|
|
200
|
+
walk(parse(text), stack, [...seen, value], positional);
|
|
180
201
|
break;
|
|
181
202
|
}
|
|
182
203
|
default:
|
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;AAEhC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,wBAAwB,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": "1.
|
|
3
|
+
"version": "1.1.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",
|
package/src/payload-accessors.ts
CHANGED
|
@@ -27,6 +27,16 @@ export function hasAccessorName(fieldName: string): string {
|
|
|
27
27
|
return HAS_PREFIX + capitalize(fieldName);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The positional accessor every OBJECT element of a collection carries, so a list separator
|
|
32
|
+
* needs no hand-computed `last` flag in the payload:
|
|
33
|
+
* `{{#items}}{{name}}{{^-last}}, {{/-last}}{{/items}}`. A boolean on the element; the leading
|
|
34
|
+
* `-` cannot collide with a field name, and it is still an ordinary Mustache tag name, so the
|
|
35
|
+
* template stays valid Mustache. A scalar element has nowhere to hold a key and carries none.
|
|
36
|
+
* An authored member of the same name wins, as it does for `has<Field>`.
|
|
37
|
+
*/
|
|
38
|
+
export const LAST_ACCESSOR = "-last";
|
|
39
|
+
|
|
30
40
|
/** Capitalize the first character, leaving an already-uppercase one untouched. */
|
|
31
41
|
export function capitalize(s: string): string {
|
|
32
42
|
if (s.length === 0) return s;
|
|
@@ -60,38 +70,97 @@ export function accessorValue(value: unknown): boolean | undefined {
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/**
|
|
63
|
-
* A view over `payload` carrying its derived `has<Field>`
|
|
73
|
+
* A view over `payload` carrying its derived accessors, recursively: `has<Field>` on every
|
|
74
|
+
* object, and `-last` on every object that is an element of a collection.
|
|
64
75
|
*
|
|
65
76
|
* 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
|
|
77
|
+
* be able to change the object it was handed. An AUTHORED member always wins: if a
|
|
67
78
|
* payload genuinely carries `hasFoo`, that value is kept rather than shadowed by a
|
|
68
79
|
* derived one.
|
|
69
80
|
*
|
|
70
81
|
* Recursion follows Mustache's own scoping: every nested object and every array
|
|
71
82
|
* ELEMENT becomes a context in its own right, so a section over `abilities` sees
|
|
72
83
|
* the accessors of the ability it is currently iterating.
|
|
84
|
+
*
|
|
85
|
+
* The payload's shape does not change the result. A plain object is copied with its
|
|
86
|
+
* accessors added; anything with its own prototype (a class instance, a Date) is read
|
|
87
|
+
* THROUGH a view instead, so its getters, methods and string form stay exactly its own.
|
|
73
88
|
*/
|
|
74
89
|
export function withDerivedAccessors<T>(payload: T, depth = 0): T {
|
|
90
|
+
return derive(payload, depth, undefined) as T;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `last` is set only for an element of a collection: its position in that collection. */
|
|
94
|
+
function derive(payload: unknown, depth: number, last: boolean | undefined): unknown {
|
|
75
95
|
if (depth > 32) return payload; // pathological graph; render is not a validator
|
|
76
96
|
if (Array.isArray(payload)) {
|
|
77
|
-
|
|
97
|
+
const end = payload.length - 1;
|
|
98
|
+
return payload.map((v, i) => derive(v, depth + 1, i === end));
|
|
78
99
|
}
|
|
79
100
|
if (payload === null || typeof payload !== "object") return payload;
|
|
80
|
-
// PLAIN
|
|
81
|
-
// its own prototype — a Date stringifies to "[object Object]" and a
|
|
82
|
-
// loses its getters —
|
|
83
|
-
// rebuilding more here would be a divergence as well as a regression.
|
|
101
|
+
// Only a PLAIN object is rebuilt. Rebuilding from Object.entries() would flatten
|
|
102
|
+
// anything with its own prototype — a Date stringifies to "[object Object]" and a
|
|
103
|
+
// class instance loses its getters — so those are viewed, never copied.
|
|
84
104
|
const proto = Object.getPrototypeOf(payload);
|
|
85
|
-
if (proto !== Object.prototype && proto !== null) return payload;
|
|
105
|
+
if (proto !== Object.prototype && proto !== null) return typedView(payload, depth, last);
|
|
86
106
|
|
|
87
107
|
const src = payload as Record<string, unknown>;
|
|
88
108
|
const out: Record<string, unknown> = {};
|
|
89
|
-
for (const [k, v] of Object.entries(src)) out[k] =
|
|
109
|
+
for (const [k, v] of Object.entries(src)) out[k] = derive(v, depth + 1, undefined);
|
|
90
110
|
for (const [k, v] of Object.entries(src)) {
|
|
91
111
|
const name = hasAccessorName(k);
|
|
92
112
|
if (Object.prototype.hasOwnProperty.call(src, name)) continue; // authored wins
|
|
93
113
|
const derived = accessorValue(v);
|
|
94
114
|
if (derived !== undefined) out[name] = derived;
|
|
95
115
|
}
|
|
96
|
-
|
|
116
|
+
if (last !== undefined && !Object.prototype.hasOwnProperty.call(src, LAST_ACCESSOR)) {
|
|
117
|
+
out[LAST_ACCESSOR] = last;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The first character lowercased — the inverse of `capitalize` for a camelCase field name. */
|
|
123
|
+
function decapitalize(s: string): string {
|
|
124
|
+
return s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The derived-accessor view of an object that has its own prototype. Every member the
|
|
129
|
+
* object has resolves to the object's own — a getter runs against the object itself, a
|
|
130
|
+
* method stays bound to it — so the Mustache engine sees the names and values it always
|
|
131
|
+
* did. Only a name the object does NOT have can resolve to a derived accessor.
|
|
132
|
+
*
|
|
133
|
+
* The proxy's target is an empty stand-in, not the object: a frozen payload would
|
|
134
|
+
* otherwise forbid returning the derived view of one of its nested values.
|
|
135
|
+
*/
|
|
136
|
+
function typedView(target: object, depth: number, last: boolean | undefined): object {
|
|
137
|
+
const members = new Map<PropertyKey, unknown>();
|
|
138
|
+
const member = (key: PropertyKey): unknown => {
|
|
139
|
+
if (members.has(key)) return members.get(key);
|
|
140
|
+
const raw: unknown = Reflect.get(target, key, target);
|
|
141
|
+
const value =
|
|
142
|
+
typeof raw === "function"
|
|
143
|
+
? (...args: unknown[]) => derive(Reflect.apply(raw, target, args), depth + 1, undefined)
|
|
144
|
+
: derive(raw, depth + 1, undefined);
|
|
145
|
+
members.set(key, value);
|
|
146
|
+
return value;
|
|
147
|
+
};
|
|
148
|
+
const derived = (key: PropertyKey): boolean | undefined => {
|
|
149
|
+
if (typeof key !== "string") return undefined;
|
|
150
|
+
if (key === LAST_ACCESSOR) return last;
|
|
151
|
+
if (!key.startsWith(HAS_PREFIX)) return undefined;
|
|
152
|
+
const rest = key.slice(HAS_PREFIX.length);
|
|
153
|
+
for (const field of [decapitalize(rest), rest]) {
|
|
154
|
+
if (!(field in target) || hasAccessorName(field) !== key) continue;
|
|
155
|
+
const raw: unknown = Reflect.get(target, field, target);
|
|
156
|
+
if (typeof raw === "function") continue;
|
|
157
|
+
const value = accessorValue(raw);
|
|
158
|
+
if (value !== undefined) return value;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
return new Proxy(Object.create(null) as object, {
|
|
163
|
+
has: (_stand, key) => key in target || derived(key) !== undefined,
|
|
164
|
+
get: (_stand, key) => (key in target ? member(key) : derived(key)),
|
|
165
|
+
});
|
|
97
166
|
}
|
package/src/verify.ts
CHANGED
|
@@ -11,7 +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
|
+
import { HAS_PREFIX, LAST_ACCESSOR, hasAccessorName } from "./payload-accessors.js";
|
|
15
15
|
|
|
16
16
|
/** A `{{var}}` references a field the (contextual) payload does not declare. */
|
|
17
17
|
export const ERR_VAR_NOT_ON_PAYLOAD = "ERR_VAR_NOT_ON_PAYLOAD";
|
|
@@ -25,11 +25,14 @@ export const ERR_OUTPUT_TAG_MISSING = "ERR_OUTPUT_TAG_MISSING";
|
|
|
25
25
|
/**
|
|
26
26
|
* A plain field-tree node mirroring an `object.value` view-object's field walk.
|
|
27
27
|
* `fields` present → a context-pushing field (object / array-of-object); absent
|
|
28
|
-
* → a scalar (string/number/boolean/scalar-array).
|
|
28
|
+
* → a scalar (string/number/boolean/scalar-array). `isArray` is the field's
|
|
29
|
+
* effective array-ness: with `fields` it is a list of objects, without it a list
|
|
30
|
+
* of scalars.
|
|
29
31
|
*/
|
|
30
32
|
export interface PayloadField {
|
|
31
33
|
name: string;
|
|
32
34
|
fields?: PayloadField[];
|
|
35
|
+
isArray?: boolean;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface VerifyError {
|
|
@@ -84,6 +87,17 @@ function isBooleanAccessor<F extends PayloadField>(stack: ResolveStack<F>, name:
|
|
|
84
87
|
return false;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* True when `name` is the positional accessor `-last` and it is available in the current scope.
|
|
92
|
+
* `positional` is true exactly when the innermost enclosing ARRAY section iterates object
|
|
93
|
+
* elements — the engine puts `-last` on each of those (payload-accessors.ts). An array of
|
|
94
|
+
* scalars makes it unavailable even inside an outer object list: a scalar element carries no
|
|
95
|
+
* key, so the lookup would bind to the OUTER element's position and render wrong.
|
|
96
|
+
*/
|
|
97
|
+
function isPositionalAccessor(positional: boolean, name: string): boolean {
|
|
98
|
+
return positional && name === LAST_ACCESSOR;
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
function find<F extends PayloadField>(fields: F[], name: string): F | undefined {
|
|
88
102
|
return fields.find((f) => f.name === name);
|
|
89
103
|
}
|
|
@@ -172,7 +186,7 @@ export function verify(
|
|
|
172
186
|
// (no second resolution pass).
|
|
173
187
|
const staticTexts: string[] = [templateText];
|
|
174
188
|
|
|
175
|
-
function walk(tokens: Token[], stack: ResolveStack, seen: readonly string[]): void {
|
|
189
|
+
function walk(tokens: Token[], stack: ResolveStack, seen: readonly string[], positional = false): void {
|
|
176
190
|
const atRoot = stack.length === 1 && stack[0] === root;
|
|
177
191
|
for (const tok of tokens) {
|
|
178
192
|
const type = tok[0] as string;
|
|
@@ -184,7 +198,7 @@ export function verify(
|
|
|
184
198
|
// {{{x}}} (spec); mustache.js emits "&" for it too
|
|
185
199
|
if (value === ".") break; // implicit iterator — always valid
|
|
186
200
|
if (atRoot) referencedAtRoot.add(value.split(".")[0]!);
|
|
187
|
-
if (!resolve(stack, value) && !isBooleanAccessor(stack, value))
|
|
201
|
+
if (!resolve(stack, value) && !isBooleanAccessor(stack, value) && !isPositionalAccessor(positional, value))
|
|
188
202
|
errors.push({ code: ERR_VAR_NOT_ON_PAYLOAD, path: value });
|
|
189
203
|
break;
|
|
190
204
|
}
|
|
@@ -193,17 +207,23 @@ export function verify(
|
|
|
193
207
|
// {{^x}}…{{/x}}
|
|
194
208
|
const sub = Array.isArray(tok[4]) ? (tok[4] as Token[]) : [];
|
|
195
209
|
if (value === ".") {
|
|
196
|
-
walk(sub, stack, seen);
|
|
210
|
+
walk(sub, stack, seen, positional);
|
|
197
211
|
break;
|
|
198
212
|
}
|
|
199
213
|
if (atRoot) referencedAtRoot.add(value.split(".")[0]!);
|
|
200
214
|
const field = resolve(stack, value);
|
|
201
215
|
if (!field) {
|
|
216
|
+
// A positional `-last` gate is a BOOLEAN on the current element: it pushes
|
|
217
|
+
// nothing, so the body walks in the SAME scope.
|
|
218
|
+
if (isPositionalAccessor(positional, value)) {
|
|
219
|
+
walk(sub, stack, seen, positional);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
202
222
|
// A derived `has<Field>` gate is a BOOLEAN over the current context, so it
|
|
203
223
|
// resolves nothing and pushes nothing — walk the body in the SAME scope,
|
|
204
224
|
// which is what `{{#hasAbilities}}{{#abilities}}…` depends on.
|
|
205
225
|
if (isBooleanAccessor(stack, value)) {
|
|
206
|
-
walk(sub, stack, seen);
|
|
226
|
+
walk(sub, stack, seen, positional);
|
|
207
227
|
break;
|
|
208
228
|
}
|
|
209
229
|
// Unresolved section head is itself drift; skip the body (its
|
|
@@ -214,7 +234,12 @@ export function verify(
|
|
|
214
234
|
// `#` over a container pushes its element fields; `^` (and `#` over a
|
|
215
235
|
// scalar, used as a conditional) keep the current context.
|
|
216
236
|
const push = type === "#" && field.fields !== undefined;
|
|
217
|
-
|
|
237
|
+
// Only a `#` over an ARRAY iterates, so only it moves the positional scope: object
|
|
238
|
+
// elements carry `-last`, scalar elements do not. A section over a non-array field
|
|
239
|
+
// (a scalar conditional, a single nested object) and every `^` section are
|
|
240
|
+
// transparent — the enclosing element's position is still the one in reach.
|
|
241
|
+
const iterates = type === "#" && field.isArray === true;
|
|
242
|
+
walk(sub, push ? [...stack, field.fields!] : stack, seen, iterates ? field.fields !== undefined : positional);
|
|
218
243
|
break;
|
|
219
244
|
}
|
|
220
245
|
case ">": {
|
|
@@ -227,7 +252,7 @@ export function verify(
|
|
|
227
252
|
break;
|
|
228
253
|
}
|
|
229
254
|
staticTexts.push(text);
|
|
230
|
-
walk(parse(text), stack, [...seen, value]);
|
|
255
|
+
walk(parse(text), stack, [...seen, value], positional);
|
|
231
256
|
break;
|
|
232
257
|
}
|
|
233
258
|
default:
|