@rhinostone/swig-core 2.6.0 → 2.7.0
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/README.md +3 -2
- package/lib/backend.js +29 -7
- package/lib/ir.js +11 -0
- package/lib/utils.js +70 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
@rhinostone/swig-core
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
-
[](https://www.npmjs.com/package/@rhinostone/swig-core) [](https://socket.dev/npm/package/@rhinostone/swig-core)
|
|
5
5
|
|
|
6
|
-
> **Shared runtime** for the `@rhinostone/swig` family of template engines. Not intended for direct consumption unless you are building a custom frontend. Install [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) for the default Swig (Jinja2/Django-inspired) flavor, [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) for the Twig flavor,
|
|
6
|
+
> **Shared runtime** for the `@rhinostone/swig` family of template engines. Not intended for direct consumption unless you are building a custom frontend. Install [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) for the default Swig (Jinja2/Django-inspired) flavor, [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) for the Twig flavor, [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) for the Python Jinja2 flavor, or [@rhinostone/swig-django](https://www.npmjs.com/package/@rhinostone/swig-django) for the Django flavor — they all pull this package in pinned to the matching version.
|
|
7
7
|
|
|
8
8
|
Extracted from `@rhinostone/swig@1.6.0` during the `2.0.0-alpha.1` multi-flavor carve. See [ROADMAP.md](https://github.com/gina-io/swig/blob/develop/ROADMAP.md) for the release narrative.
|
|
9
9
|
|
|
@@ -22,6 +22,7 @@ Consumers
|
|
|
22
22
|
* [@rhinostone/swig](https://www.npmjs.com/package/@rhinostone/swig) — default Swig flavor.
|
|
23
23
|
* [@rhinostone/swig-twig](https://www.npmjs.com/package/@rhinostone/swig-twig) — Twig parity frontend.
|
|
24
24
|
* [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) — Python Jinja2 frontend.
|
|
25
|
+
* [@rhinostone/swig-django](https://www.npmjs.com/package/@rhinostone/swig-django) — Django Template Language frontend.
|
|
25
26
|
|
|
26
27
|
Versioning
|
|
27
28
|
----------
|
package/lib/backend.js
CHANGED
|
@@ -195,7 +195,20 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
195
195
|
forBodyJS = '',
|
|
196
196
|
ctxloopcache = ('_ctx.__loopcache' + Math.random()).replace(/\./g, ''),
|
|
197
197
|
ctx = '_ctx.',
|
|
198
|
-
|
|
198
|
+
// Opt-in loop-context naming. A frontend may rename the loop
|
|
199
|
+
// variable (Django uses `forloop`), rename the counter fields, and
|
|
200
|
+
// expose the enclosing loop as `parentloop`. All three default to
|
|
201
|
+
// swig's own names, so an absent flag emits byte-identical JS — the
|
|
202
|
+
// native / Twig / Jinja2 frontends never set them and are untouched.
|
|
203
|
+
// The Django `for` tag sets all three on the IRFor node.
|
|
204
|
+
loopName = node.loopName || 'loop',
|
|
205
|
+
ctxloop = '_ctx.' + loopName,
|
|
206
|
+
loopFields = node.loopFields || {},
|
|
207
|
+
fIndex = loopFields.index || 'index',
|
|
208
|
+
fIndex0 = loopFields.index0 || 'index0',
|
|
209
|
+
fRevindex = loopFields.revindex || 'revindex',
|
|
210
|
+
fRevindex0 = loopFields.revindex0 || 'revindex0',
|
|
211
|
+
parentloopJS = node.loopParent ? ', parentloop: ' + ctxloopcache + '.' + loopName : '';
|
|
199
212
|
if (node.iterable && typeof node.iterable === 'object' && typeof node.iterable.type === 'string') {
|
|
200
213
|
forIterable = exports.emitExpr(node.iterable);
|
|
201
214
|
} else {
|
|
@@ -226,18 +239,18 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
226
239
|
out += '(function () {\n' +
|
|
227
240
|
' var __l = ' + forIterable + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n' +
|
|
228
241
|
forEmptyCheck +
|
|
229
|
-
' var ' + ctxloopcache + ' = {
|
|
230
|
-
' ' + ctxloop + ' = { first: false,
|
|
242
|
+
' var ' + ctxloopcache + ' = { ' + loopName + ': ' + ctxloop + ', ' + forVal + ': ' + ctx + forVal + ', ' + forKey + ': ' + ctx + forKey + ' };\n' +
|
|
243
|
+
' ' + ctxloop + ' = { first: false, ' + fIndex + ': 1, ' + fIndex0 + ': 0, ' + fRevindex + ': __len, ' + fRevindex0 + ': __len - 1, length: __len, last: false' + parentloopJS + ' };\n' +
|
|
231
244
|
' _utils.each(__l, function (' + forVal + ', ' + forKey + ') {\n' +
|
|
232
245
|
' ' + ctx + forVal + ' = ' + forVal + ';\n' +
|
|
233
246
|
' ' + ctx + forKey + ' = ' + forKey + ';\n' +
|
|
234
247
|
' ' + ctxloop + '.key = ' + forKey + ';\n' +
|
|
235
|
-
' ' + ctxloop + '.first = (' + ctxloop + '.
|
|
236
|
-
' ' + ctxloop + '.last = (' + ctxloop + '.
|
|
248
|
+
' ' + ctxloop + '.first = (' + ctxloop + '.' + fIndex0 + ' === 0);\n' +
|
|
249
|
+
' ' + ctxloop + '.last = (' + ctxloop + '.' + fRevindex0 + ' === 0);\n' +
|
|
237
250
|
' ' + forBodyJS +
|
|
238
|
-
' ' + ctxloop + '.
|
|
251
|
+
' ' + ctxloop + '.' + fIndex + ' += 1; ' + ctxloop + '.' + fIndex0 + ' += 1; ' + ctxloop + '.' + fRevindex + ' -= 1; ' + ctxloop + '.' + fRevindex0 + ' -= 1;\n' +
|
|
239
252
|
' });\n' +
|
|
240
|
-
' ' + ctxloop + ' = ' + ctxloopcache + '.
|
|
253
|
+
' ' + ctxloop + ' = ' + ctxloopcache + '.' + loopName + ';\n' +
|
|
241
254
|
' ' + ctx + forVal + ' = ' + ctxloopcache + '.' + forVal + ';\n' +
|
|
242
255
|
' ' + ctx + forKey + ' = ' + ctxloopcache + '.' + forKey + ';\n' +
|
|
243
256
|
' ' + ctxloopcache + ' = undefined;\n' +
|
|
@@ -781,6 +794,15 @@ function emitVarRef(node, d) {
|
|
|
781
794
|
utils.each(node.path, function (segment) {
|
|
782
795
|
checkDangerousSegment(segment, d, node);
|
|
783
796
|
});
|
|
797
|
+
// Opt-in Django-style runtime resolution: dict / attribute / method
|
|
798
|
+
// lookup, numeric index access ({{ list.0 }}), and auto-call of callable
|
|
799
|
+
// leaves, via _utils.resolve. A frontend sets node.resolve; absent it,
|
|
800
|
+
// emit the unchanged static dot-path so the other flavors stay
|
|
801
|
+
// byte-identical. The parse-time _dangerousProps guard above still runs
|
|
802
|
+
// either way; _utils.resolve adds a matching runtime guard.
|
|
803
|
+
if (node.resolve) {
|
|
804
|
+
return '_utils.resolve(_ctx, ' + JSON.stringify(node.path) + ')';
|
|
805
|
+
}
|
|
784
806
|
return checkMatchExpr(node.path);
|
|
785
807
|
}
|
|
786
808
|
|
package/lib/ir.js
CHANGED
|
@@ -109,6 +109,14 @@
|
|
|
109
109
|
* to a real {@link IRExpr}. Backends MUST tolerate both shapes — same
|
|
110
110
|
* transitional widening as {@link IRSet}'s `target`.
|
|
111
111
|
*
|
|
112
|
+
* `loopName` / `loopFields` / `loopParent` are opt-in flags a frontend
|
|
113
|
+
* sets to rename the loop-context object and its counter fields and to
|
|
114
|
+
* expose the enclosing loop. They default to swig's own names, so when a
|
|
115
|
+
* frontend leaves them unset the backend emits byte-identical JS (native /
|
|
116
|
+
* Twig / Jinja2 never set them). The Django frontend uses all three to
|
|
117
|
+
* surface `forloop` with `counter` / `counter0` / `revcounter` /
|
|
118
|
+
* `revcounter0` / `parentloop`.
|
|
119
|
+
*
|
|
112
120
|
* @typedef {Object} IRFor
|
|
113
121
|
* @property {'For'} type
|
|
114
122
|
* @property {string} [key] Loop key var (second binding).
|
|
@@ -116,6 +124,9 @@
|
|
|
116
124
|
* @property {IRExpr|string} iterable Transitional — see note above.
|
|
117
125
|
* @property {IRStatement[]} body
|
|
118
126
|
* @property {IRStatement[]} [emptyBody]
|
|
127
|
+
* @property {string} [loopName] Opt-in loop-context var name (default 'loop'; Django sets 'forloop').
|
|
128
|
+
* @property {Object} [loopFields] Opt-in counter-field rename map ({index,index0,revindex,revindex0} → emit names); absent fields keep swig names.
|
|
129
|
+
* @property {boolean} [loopParent] Opt-in: expose the enclosing loop as `<loopName>.parentloop` (Django forloop.parentloop).
|
|
119
130
|
* @property {IRLoc} [loc]
|
|
120
131
|
*/
|
|
121
132
|
|
package/lib/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
var security = require('./security');
|
|
2
|
+
|
|
1
3
|
var isArray;
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -311,3 +313,71 @@ exports.slice = function (obj, start, stop, step) {
|
|
|
311
313
|
exports.coerceOutput = function (v) {
|
|
312
314
|
return (v === null || v === undefined) ? '' : v;
|
|
313
315
|
};
|
|
316
|
+
|
|
317
|
+
/*!
|
|
318
|
+
* Resolve a single path segment against a value, Django-style. A plain
|
|
319
|
+
* property access (`obj[seg]`) covers dictionary, attribute, and array /
|
|
320
|
+
* string index lookup in one step (JS string-keys an array / string by a
|
|
321
|
+
* numeric segment, so `["a"][0]` and `["a"]["0"]` both yield the element).
|
|
322
|
+
* A callable leaf is auto-called with no arguments, bound to its receiver,
|
|
323
|
+
* honoring Django's opt-outs: `fn.alters_data === true` is not called and
|
|
324
|
+
* yields `undefined` (Django renders nothing for data-altering callables);
|
|
325
|
+
* `fn.do_not_call_in_templates === true` is returned uncalled. A call that
|
|
326
|
+
* throws (e.g. a method that needs arguments) yields `undefined`, mirroring
|
|
327
|
+
* Django's `string_if_invalid` fallback on a failed auto-call. The
|
|
328
|
+
* `_dangerousProps` segments are rejected at runtime as defense-in-depth.
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
function resolveSeg(obj, seg) {
|
|
332
|
+
var val;
|
|
333
|
+
if (obj === null || obj === undefined) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (security.dangerousProps.indexOf(seg) !== -1) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
val = obj[seg];
|
|
340
|
+
if (typeof val === 'function') {
|
|
341
|
+
if (val.alters_data === true) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
if (val.do_not_call_in_templates === true) {
|
|
345
|
+
return val;
|
|
346
|
+
}
|
|
347
|
+
// Django auto-calls a callable leaf with no args and falls back to
|
|
348
|
+
// string_if_invalid ("") when the call raises; mirror that here.
|
|
349
|
+
try {
|
|
350
|
+
return val.call(obj);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return val;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve a dotted path against a context object, Django-style — walk each
|
|
360
|
+
* segment through the per-segment lookup, short-circuiting to the missing
|
|
361
|
+
* value as soon as a segment resolves null / undefined. Powers the Django
|
|
362
|
+
* flavor's variable resolution (dict / attribute / method lookup, auto-call
|
|
363
|
+
* of callable leaves, `{{ list.0 }}` index access) when a frontend opts in
|
|
364
|
+
* via the `IRVarRef.resolve` flag. The raw resolved value (including null) is
|
|
365
|
+
* returned so downstream filters such as `default_if_none` see a real null;
|
|
366
|
+
* coercion to "" is deferred to the output drain (via `coerceOutput`).
|
|
367
|
+
*
|
|
368
|
+
* @param {*} obj The root context (usually `_ctx`).
|
|
369
|
+
* @param {string[]} path Path segments.
|
|
370
|
+
* @return {*} The resolved value, or null / undefined when a
|
|
371
|
+
* segment is missing.
|
|
372
|
+
*/
|
|
373
|
+
exports.resolve = function (obj, path) {
|
|
374
|
+
var cur = obj,
|
|
375
|
+
i;
|
|
376
|
+
for (i = 0; i < path.length; i += 1) {
|
|
377
|
+
cur = resolveSeg(cur, path[i]);
|
|
378
|
+
if (cur === null || cur === undefined) {
|
|
379
|
+
return cur;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return cur;
|
|
383
|
+
};
|