@rhinostone/swig-django 2.9.13 → 2.11.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
@@ -24,8 +24,8 @@ var out = swig.render('Hello, {{ name|upper }}!', {
24
24
  // => Hello, WORLD!
25
25
  ```
26
26
 
27
- Supported surface (as of 2.7.0)
28
- -------------------------------
27
+ Supported surface
28
+ -----------------
29
29
 
30
30
  Renders real Django templates — everything below was cross-checked against Django 5.2.
31
31
 
@@ -33,6 +33,8 @@ Renders real Django templates — everything below was cross-checked against Dja
33
33
  * **Variable resolution** — Django-faithful attribute / dictionary / index lookup. Callable attributes are auto-called (honoring `alters_data` / `do_not_call_in_templates`), numeric indexing works (`{{ list.0 }}`), and dicts iterate via `.keys` / `.values` / `.items`.
34
34
  * **Filter arguments** — Django colon syntax (`{{ value|date:"Y-m-d" }}`).
35
35
  * **Filters** — 42 built-ins (`add`, `addslashes`, `capfirst`, `center`, `cut`, `date`, `default`, `default_if_none`, `divisibleby`, `escape` / `e`, `escapejs`, `filesizeformat`, `first`, `floatformat`, `force_escape`, `get_digit`, `iriencode`, `join`, `last`, `length`, `linebreaks`, `linebreaksbr`, `linenumbers`, `ljust`, `lower`, `make_list`, `phone2numeric`, `pluralize`, `rjust`, `safe`, `slice`, `slugify`, `striptags`, `time`, `title`, `truncatechars`, `truncatewords`, `upper`, `urlencode`, `wordcount`, `wordwrap`, `yesno`). See `lib/filters.js` for the full list.
36
+ * **Equality** — `==`, `!=` and `in` compare as Python does: a string never equals a number, and lists and mappings compare item by item.
37
+ * **Unknown names** — a filter name the instance does not hold is a compile error wherever it stands, as in Django. Register custom filters before the compile that uses them.
36
38
  * **Async** — `renderFile(path, locals, cb)` against a loader with `async === true`, or `compileFile` with `codegenMode: 'async'`; `renderFileAsync` / `compileFileAsync` still work but are deprecated.
37
39
 
38
40
  Explicitly unsupported (parse-time throw or absent)
package/lib/index.js CHANGED
@@ -32,7 +32,7 @@ exports.name = 'django';
32
32
  *
33
33
  * @type {String}
34
34
  */
35
- exports.version = "2.9.13";
35
+ exports.version = "2.11.0";
36
36
 
37
37
  /**
38
38
  * Expression-level parser — Pratt-style recursive descent that consumes
package/lib/lexer.js CHANGED
@@ -243,9 +243,9 @@ exports.types = TYPES;
243
243
  /**
244
244
  * Match the next token at the start of `str`.
245
245
  *
246
- * Throws via utils.throwError when no rule matches. The throw is opaque (no
247
- * line / file info); the Django frontend's onCompileError callback attaches
248
- * filename + line per the swig-core / frontend seam rule.
246
+ * Throws when no rule matches. This module does not know the line or the
247
+ * file, so the error is flagged `unplaced` and the Django frontend's
248
+ * `exports.parse` names them.
249
249
  *
250
250
  * @param {string} str Input slice starting at the unconsumed offset.
251
251
  * @return {LexerToken} Matched token.
@@ -277,7 +277,9 @@ function reader(str) {
277
277
  });
278
278
 
279
279
  if (!matched) {
280
- utils.throwError('Unexpected token "' + str.charAt(0) + '" in Django expression');
280
+ var err = new Error('Unexpected token "' + str.charAt(0) + '" in Django expression.');
281
+ err.unplaced = true;
282
+ throw err;
281
283
  }
282
284
 
283
285
  return matched;
package/lib/parser.js CHANGED
@@ -5,6 +5,10 @@ var ir = require('@rhinostone/swig-core/lib/ir'),
5
5
  var lexer = require('./lexer');
6
6
  var _t = require('./tokentypes');
7
7
 
8
+ // A catalog entry is read as an own property: a filter can be registered under
9
+ // the name hasOwnProperty, and a name like toString is not a filter.
10
+ var hasOwn = Object.prototype.hasOwnProperty;
11
+
8
12
  /**
9
13
  * Make a string safe for embedding into a regular expression.
10
14
  * @param {string} str
@@ -21,6 +25,23 @@ function escapeRegExp(str) {
21
25
  */
22
26
  var _reserved = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with'];
23
27
 
28
+ /**
29
+ * Build the IR of `left == right` or `left != right`, flagged so swig-core
30
+ * emits Python's equality (`_utils.pyEq`) instead of JavaScript's loose
31
+ * operators.
32
+ *
33
+ * @param {string} op `==` or `!=`.
34
+ * @param {object} left Left operand IR.
35
+ * @param {object} right Right operand IR.
36
+ * @return {object} IRBinaryOp.
37
+ * @private
38
+ */
39
+ function equality(op, left, right) {
40
+ var node = ir.binaryOp(op, left, right);
41
+ node.pyEq = 'django';
42
+ return node;
43
+ }
44
+
24
45
  /**
25
46
  * Build the IR of `left in right`, or of `left not in right` when `negate`
26
47
  * is set: a BinaryOp `in` flagged with this flavor's name, so swig-core
@@ -154,9 +175,13 @@ exports.missingAsEmpty = function (expr) {
154
175
  * @param {object} [_posOut] Optional out-param; final cursor stored on
155
176
  * `_posOut.pos` to let callers detect partial
156
177
  * consumption.
178
+ * @param {object} [_scope] Set by `exports.parse`: the `line` and the
179
+ * `filename` an error names, and the instance's
180
+ * `extensions`, which make a filter name the
181
+ * catalog does not hold an error.
157
182
  * @return {object} IRExpr tree.
158
183
  */
159
- exports.parseExpr = function (tokens, filters, _posOut) {
184
+ exports.parseExpr = function (tokens, filters, _posOut, _scope) {
160
185
  var pos = 0;
161
186
  filters = filters || {};
162
187
 
@@ -173,7 +198,7 @@ exports.parseExpr = function (tokens, filters, _posOut) {
173
198
  return t;
174
199
  }
175
200
  function bail(msg) {
176
- utils.throwError(msg);
201
+ utils.throwError(msg, _scope && _scope.line, _scope && _scope.filename);
177
202
  }
178
203
 
179
204
  function guardSegment(segment) {
@@ -406,7 +431,7 @@ exports.parseExpr = function (tokens, filters, _posOut) {
406
431
  } else if (tok.type === _t.FILTEREMPTY || tok.type === _t.FILTER) {
407
432
  consume();
408
433
  fname = tok.match;
409
- if (filters.hasOwnProperty(fname) && typeof filters[fname] !== 'function') {
434
+ if (hasOwn.call(filters, fname) ? typeof filters[fname] !== 'function' : (_scope && _scope.extensions)) {
410
435
  bail('Invalid filter "' + fname + '"');
411
436
  }
412
437
  // Django colon-filter: an optional single positional argument
@@ -525,7 +550,11 @@ exports.parseExpr = function (tokens, filters, _posOut) {
525
550
  continue;
526
551
  }
527
552
  var right = parseExpression(info.prec + 1);
528
- left = (info.op === 'in' || info.op === 'not in') ? membership(left, right, info.op === 'not in') : ir.binaryOp(info.op, left, right);
553
+ if (info.op === 'in' || info.op === 'not in') {
554
+ left = membership(left, right, info.op === 'not in');
555
+ } else {
556
+ left = (info.op === '==' || info.op === '!=') ? equality(info.op, left, right) : ir.binaryOp(info.op, left, right);
557
+ }
529
558
  }
530
559
  return left;
531
560
  }
@@ -590,6 +619,25 @@ exports.parse = function (swig, source, opts, tags, filters) {
590
619
  tags = tags || {};
591
620
  filters = filters || {};
592
621
 
622
+ // Where parseExpr is working, so that an expression error can name its line
623
+ // and file, and whether it has a catalog to check a filter name against (only
624
+ // an engine instance has). A tag gets a handle on this module whose parseExpr
625
+ // works in the same scope and on this parse's filter catalog.
626
+ var scope = { line: 0, filename: opts.filename, extensions: swig && swig.extensions };
627
+ var exprParser = Object.create(exports);
628
+ exprParser.parseExpr = function (tokens, _filters, posOut) {
629
+ return exports.parseExpr(tokens, filters, posOut, scope);
630
+ };
631
+
632
+ // The lexer does not know where it is working, so the place of its error is
633
+ // named here. An error from a nested parse has its place already.
634
+ function throwPlaced(err) {
635
+ if (err.unplaced) {
636
+ utils.throwError(err.message.slice(0, -1), scope.line, scope.filename);
637
+ }
638
+ throw err;
639
+ }
640
+
593
641
  var varControls = opts.varControls || ['{{', '}}'];
594
642
  var tagControls = opts.tagControls || ['{%', '%}'];
595
643
  var cmtControls = opts.cmtControls || ['{#', '#}'];
@@ -642,16 +690,22 @@ exports.parse = function (swig, source, opts, tags, filters) {
642
690
  * @private
643
691
  */
644
692
  function parseVariable(str, _line) {
645
- var lexed = lexer.read(utils.strip(str));
693
+ scope.line = _line;
694
+ var lexed;
695
+ try {
696
+ lexed = lexer.read(utils.strip(str));
697
+ } catch (e) {
698
+ throwPlaced(e);
699
+ }
646
700
  var sawSafe = false;
647
701
  utils.each(lexed, function (tok) {
648
702
  if (tok.type === _t.FILTEREMPTY || tok.type === _t.FILTER) {
649
- if (filters.hasOwnProperty(tok.match) && filters[tok.match].safe === true) {
703
+ if (hasOwn.call(filters, tok.match) && filters[tok.match].safe === true) {
650
704
  sawSafe = true;
651
705
  }
652
706
  }
653
707
  });
654
- var expr = exports.missingAsEmpty(exports.parseExpr(lexed, filters));
708
+ var expr = exports.missingAsEmpty(exports.parseExpr(lexed, filters, undefined, scope));
655
709
  var tail;
656
710
  var esc = escapeStack[escapeStack.length - 1];
657
711
  if (esc && !sawSafe) {
@@ -734,7 +788,13 @@ exports.parse = function (swig, source, opts, tags, filters) {
734
788
  escape: escapeStack[escapeStack.length - 1]
735
789
  };
736
790
 
737
- var ok = tag.parse(tagArgs, _line, exports, _t, stack, opts, swig, token);
791
+ scope.line = _line;
792
+ var ok;
793
+ try {
794
+ ok = tag.parse(tagArgs, _line, exprParser, _t, stack, opts, swig, token);
795
+ } catch (e) {
796
+ throwPlaced(e);
797
+ }
738
798
  if (!ok) {
739
799
  utils.throwError('Unexpected tag "' + tagName + '"', _line, opts.filename);
740
800
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-django",
3
- "version": "2.9.13",
3
+ "version": "2.11.0",
4
4
  "description": "Django-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
5
5
  "keywords": [
6
6
  "template",
@@ -22,7 +22,7 @@
22
22
  "node": ">=12"
23
23
  },
24
24
  "peerDependencies": {
25
- "@rhinostone/swig-core": "2.9.13"
25
+ "@rhinostone/swig-core": "2.11.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"