@rhinostone/swig-core 2.5.3 → 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 CHANGED
@@ -1,9 +1,9 @@
1
1
  @rhinostone/swig-core
2
2
  =====================
3
3
 
4
- [![NPM version](http://img.shields.io/npm/v/@rhinostone/swig-core.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig-core) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig-core)](https://socket.dev/npm/package/@rhinostone/swig-core)
4
+ [![NPM version](https://img.shields.io/npm/v/@rhinostone/swig-core.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig-core) [![Socket Badge](https://socket.dev/api/badge/npm/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, or [@rhinostone/swig-jinja2](https://www.npmjs.com/package/@rhinostone/swig-jinja2) for the Python Jinja2 flavor — they all pull this package in pinned to the matching version.
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
- ctxloop = '_ctx.loop';
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 + ' = { loop: ' + ctxloop + ', ' + forVal + ': ' + ctx + forVal + ', ' + forKey + ': ' + ctx + forKey + ' };\n' +
230
- ' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n' +
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 + '.index0 === 0);\n' +
236
- ' ' + ctxloop + '.last = (' + ctxloop + '.revindex0 === 0);\n' +
248
+ ' ' + ctxloop + '.first = (' + ctxloop + '.' + fIndex0 + ' === 0);\n' +
249
+ ' ' + ctxloop + '.last = (' + ctxloop + '.' + fRevindex0 + ' === 0);\n' +
237
250
  ' ' + forBodyJS +
238
- ' ' + ctxloop + '.index += 1; ' + ctxloop + '.index0 += 1; ' + ctxloop + '.revindex -= 1; ' + ctxloop + '.revindex0 -= 1;\n' +
251
+ ' ' + ctxloop + '.' + fIndex + ' += 1; ' + ctxloop + '.' + fIndex0 + ' += 1; ' + ctxloop + '.' + fRevindex + ' -= 1; ' + ctxloop + '.' + fRevindex0 + ' -= 1;\n' +
239
252
  ' });\n' +
240
- ' ' + ctxloop + ' = ' + ctxloopcache + '.loop;\n' +
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.5.3",
3
+ "version": "2.7.0",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",