@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.
@@ -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>` accessors, recursively.
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 key always wins: if a
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;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,SAAI,GAAG,CAAC,CAuBhE"}
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>` accessors, recursively.
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 key always wins: if a
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
- return payload.map((v) => withDerivedAccessors(v, depth + 1));
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 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.
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] = withDerivedAccessors(v, depth + 1);
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":"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"}
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;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC;AAErC,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;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAAI,OAAU,EAAE,KAAK,GAAG,CAAC;IAC3D,OAAO,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAM,CAAC;AAChD,CAAC;AAED,0FAA0F;AAC1F,SAAS,MAAM,CAAC,OAAgB,EAAE,KAAa,EAAE,IAAyB;IACxE,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,OAAO,CAAC,CAAC,gDAAgD;IAChF,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IACpE,iFAAiF;IACjF,kFAAkF;IAClF,wEAAwE;IACxE,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAEzF,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,MAAM,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;IACnF,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,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC;QACpF,GAAG,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IAC5B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+FAA+F;AAC/F,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,MAAc,EAAE,KAAa,EAAE,IAAyB;IACzE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,GAAgB,EAAW,EAAE;QAC3C,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAY,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,KAAK,GACT,OAAO,GAAG,KAAK,UAAU;YACvB,CAAC,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,CAAC;YACxF,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,GAAgB,EAAuB,EAAE;QACxD,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAC9C,IAAI,GAAG,KAAK,aAAa;YAAE,OAAO,IAAI,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,SAAS,CAAC;QAClD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1C,KAAK,MAAM,KAAK,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;YAC/C,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,eAAe,CAAC,KAAK,CAAC,KAAK,GAAG;gBAAE,SAAS;YACnE,MAAM,GAAG,GAAY,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;YACxD,IAAI,OAAO,GAAG,KAAK,UAAU;gBAAE,SAAS;YACxC,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;QACxC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IACF,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAW,EAAE;QAC9C,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS;QACjE,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;KACnE,CAAC,CAAC;AACL,CAAC"}
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;
@@ -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;;;;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"}
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
- walk(sub, push ? [...stack, field.fields] : stack, seen);
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:
@@ -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;AAErE,gFAAgF;AAChF,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAC/D,8DAA8D;AAC9D,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAC/D,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAC;AACnE,4EAA4E;AAC5E,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AA+B/D,MAAM,SAAS,GAAG,EAAE,CAAC;AAYrB;;;;;;;;;;;GAWG;AACH,SAAS,iBAAiB,CAAyB,KAAsB,EAAE,IAAY;IACrF,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,CAAE;YAAE,IAAI,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,IAAI,CAAyB,MAAW,EAAE,IAAY;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAAsB,EACtB,IAAY;IAEZ,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,OAAsB,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC;QACtC,IAAI,GAAG,EAAE,CAAC;YACR,OAAO,GAAG,GAAG,CAAC;YACd,MAAM;QACR,CAAC;IACH,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAE,CAAmB,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3F,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,+DAA+D;AAC/D,MAAM,OAAO,GAAG,uBAAuB,CAAC;AAExC,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,6EAA6E;AAC7E,oFAAoF;AACpF,2CAA2C;AAC3C,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE9D,SAAS,UAAU,CAAC,IAAY,EAAE,GAAW;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,SAAS,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mFAAmF;AACnF,kFAAkF;AAClF,SAAS,WAAW,CAAC,IAAY,EAAE,GAAW;IAC5C,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CACpB,YAAoB,EACpB,MAAsB,EACtB,IAAoB;IAEpB,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,CAAC;IAChC,MAAM,IAAI,GAAG,MAAM,CAAC;IACpB,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,kEAAkE;IAClE,yEAAyE;IACzE,+BAA+B;IAC/B,MAAM,WAAW,GAAa,CAAC,YAAY,CAAC,CAAC;IAE7C,SAAS,IAAI,CAAC,MAAe,EAAE,KAAmB,EAAE,IAAuB;QACzE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;QACvD,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;YAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;YAC/B,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,MAAM,CAAC,CAAC,QAAQ;gBACrB,KAAK,GAAG,CAAC,CAAC,SAAS;gBACnB,KAAK,GAAG,CAAC,CAAC,CAAC;oBACT,mDAAmD;oBACnD,IAAI,KAAK,KAAK,GAAG;wBAAE,MAAM,CAAC,mCAAmC;oBAC7D,IAAI,MAAM;wBAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;oBACvD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC;wBAC5D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;oBAC7D,MAAM;gBACR,CAAC;gBACD,KAAK,GAAG,CAAC,CAAC,gBAAgB;gBAC1B,KAAK,GAAG,CAAC,CAAC,CAAC;oBACT,gBAAgB;oBAChB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAa,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC7D,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;wBAClB,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;wBACvB,MAAM;oBACR,CAAC;oBACD,IAAI,MAAM;wBAAE,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;oBACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;oBACpC,IAAI,CAAC,KAAK,EAAE,CAAC;wBACX,2EAA2E;wBAC3E,yEAAyE;wBACzE,+DAA+D;wBAC/D,IAAI,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;4BACpC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;4BACvB,MAAM;wBACR,CAAC;wBACD,8DAA8D;wBAC9D,iEAAiE;wBACjE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;wBAC3D,MAAM;oBACR,CAAC;oBACD,sEAAsE;oBACtE,2DAA2D;oBAC3D,MAAM,IAAI,GAAG,IAAI,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC;oBACxD,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;oBAC1D,MAAM;gBACR,CAAC;gBACD,KAAK,GAAG,CAAC,CAAC,CAAC;oBACT,qBAAqB;oBACrB,IAAI,CAAC,QAAQ;wBAAE,MAAM,CAAC,mCAAmC;oBACzD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS;wBAAE,MAAM,CAAC,oBAAoB;oBACjF,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;oBACrC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;wBACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;wBAC3D,MAAM;oBACR,CAAC;oBACD,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;oBAC3C,MAAM;gBACR,CAAC;gBACD;oBACE,MAAM,CAAC,iCAAiC;YAC5C,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,aAAa,IAAI,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC;IAC9C,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,yEAAyE;QACzE,0EAA0E;QAC1E,2EAA2E;QAC3E,wEAAwE;QACxE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC;gBAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,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.0.13",
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",
@@ -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>` accessors, recursively.
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 key always wins: if a
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
- return payload.map((v) => withDerivedAccessors(v, depth + 1)) as unknown as T;
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 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.
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] = withDerivedAccessors(v, depth + 1);
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
- return out as unknown as T;
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
- walk(sub, push ? [...stack, field.fields!] : stack, seen);
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: