@rhinostone/swig-twig 2.0.0-alpha.4 → 2.0.0-alpha.5
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/lib/index.js +13 -1
- package/lib/parser.js +21 -5
- package/lib/tests/index.js +109 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -16,7 +16,8 @@ var utils = require('@rhinostone/swig-core/lib/utils'),
|
|
|
16
16
|
dateformatter = require('@rhinostone/swig-core/lib/dateformatter'),
|
|
17
17
|
parser = require('./parser'),
|
|
18
18
|
_tags = require('./tags'),
|
|
19
|
-
_filters = require('./filters')
|
|
19
|
+
_filters = require('./filters'),
|
|
20
|
+
_tests = require('./tests');
|
|
20
21
|
|
|
21
22
|
exports.name = 'twig';
|
|
22
23
|
|
|
@@ -140,6 +141,7 @@ exports.setDefaultTZOffset = function (offset) {
|
|
|
140
141
|
* @return {object} New Twig environment.
|
|
141
142
|
*/
|
|
142
143
|
exports.Twig = function (opts) {
|
|
144
|
+
var self = this;
|
|
143
145
|
validateOptions(opts);
|
|
144
146
|
this.options = utils.extend({}, defaultOptions, opts || {});
|
|
145
147
|
this.cache = {};
|
|
@@ -154,6 +156,16 @@ exports.Twig = function (opts) {
|
|
|
154
156
|
utils.throwError(err, null, options.filename);
|
|
155
157
|
}
|
|
156
158
|
});
|
|
159
|
+
|
|
160
|
+
// Register Twig `is <name>` runtime helpers as `_ext._test_<name>`. The
|
|
161
|
+
// parser lowers `foo is odd` to a two-segment VarRef + FnCall pointing
|
|
162
|
+
// at this slot, so the helpers must be installed on every Twig
|
|
163
|
+
// instance (including the default one). `setExtension` attaches to the
|
|
164
|
+
// per-instance `extensions` map — consumers can still override a test
|
|
165
|
+
// with their own `setExtension('_test_<name>', fn)` after construction.
|
|
166
|
+
utils.each(_tests, function (fn, name) {
|
|
167
|
+
self.setExtension('_test_' + name, fn);
|
|
168
|
+
});
|
|
157
169
|
};
|
|
158
170
|
|
|
159
171
|
/*!
|
package/lib/parser.js
CHANGED
|
@@ -302,11 +302,11 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
302
302
|
var m;
|
|
303
303
|
switch (tok.type) {
|
|
304
304
|
case _t.STRING:
|
|
305
|
-
return ir.literal('string', unquoteString(tok.match));
|
|
305
|
+
return parsePostfix(ir.literal('string', unquoteString(tok.match)));
|
|
306
306
|
case _t.NUMBER:
|
|
307
|
-
return ir.literal('number', parseFloat(tok.match));
|
|
307
|
+
return parsePostfix(ir.literal('number', parseFloat(tok.match)));
|
|
308
308
|
case _t.BOOL:
|
|
309
|
-
return ir.literal('bool', tok.match === 'true');
|
|
309
|
+
return parsePostfix(ir.literal('bool', tok.match === 'true'));
|
|
310
310
|
case _t.NOT:
|
|
311
311
|
return ir.unaryOp('!', parseUnary());
|
|
312
312
|
case _t.OPERATOR:
|
|
@@ -371,13 +371,29 @@ exports.parseExpr = function (tokens, filters, _posOut) {
|
|
|
371
371
|
// parses as `(foo is defined) and bar`.
|
|
372
372
|
if (info.op === 'is' || info.op === 'is not') {
|
|
373
373
|
var test = parseTest();
|
|
374
|
-
var testCall
|
|
374
|
+
var testCall;
|
|
375
|
+
// VarRef subjects coerce null/undefined to "" through checkMatchExpr
|
|
376
|
+
// in emitVarRef, which loses the defined/undefined signal that
|
|
377
|
+
// `is defined` / `is null` need. Route both through IRVarRefExists
|
|
378
|
+
// (same shape as the `??` fallback from C3) so a path that is
|
|
379
|
+
// actually undefined or null yields the right boolean instead of
|
|
380
|
+
// being compared against the coerced "" placeholder. Non-VarRef
|
|
381
|
+
// subjects (Literal, BinaryOp, FnCall, FilterCall) evaluate to a
|
|
382
|
+
// concrete value with no coercion, so they fall through to the
|
|
383
|
+
// generic `_ext._test_<name>` helper path registered by the engine.
|
|
384
|
+
if (test.args.length === 0 && left.type === 'VarRef' && test.name === 'defined') {
|
|
385
|
+
testCall = ir.varRefExists(left.path, left.loc);
|
|
386
|
+
} else if (test.args.length === 0 && left.type === 'VarRef' && test.name === 'null') {
|
|
387
|
+
testCall = ir.unaryOp('!', ir.varRefExists(left.path, left.loc));
|
|
388
|
+
} else {
|
|
389
|
+
testCall = ir.fnCall(ir.varRef(['_ext', '_test_' + test.name]), [left].concat(test.args));
|
|
390
|
+
}
|
|
375
391
|
left = info.op === 'is not' ? ir.unaryOp('!', testCall) : testCall;
|
|
376
392
|
continue;
|
|
377
393
|
}
|
|
378
394
|
var right = parseExpression(info.prec + 1);
|
|
379
395
|
if (info.op === '..') {
|
|
380
|
-
left = ir.fnCall(ir.varRef(['
|
|
396
|
+
left = ir.fnCall(ir.varRef(['_utils', 'range']), [left, right]);
|
|
381
397
|
} else {
|
|
382
398
|
left = ir.binaryOp(info.op, left, right);
|
|
383
399
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rhinostone/swig-twig — built-in test runtime helpers.
|
|
3
|
+
*
|
|
4
|
+
* Twig `is <name>` / `is not <name>` expressions lower to
|
|
5
|
+
* `_ext._test_<name>(subject, ...args)` at the IR layer. The Twig
|
|
6
|
+
* constructor registers each export here via `self.setExtension('_test_'
|
|
7
|
+
* + name, fn)`, which installs the helper onto the per-instance
|
|
8
|
+
* `_swig.extensions` map — so Path A (`new Twig().render(...)`) honors
|
|
9
|
+
* per-instance overrides without leaking cross-instance.
|
|
10
|
+
*
|
|
11
|
+
* Two tests (`defined`, `null`) are additionally special-cased in the
|
|
12
|
+
* Twig parser when the subject is a VarRef with no args: both route
|
|
13
|
+
* through IRVarRefExists to preserve the defined/undefined signal that
|
|
14
|
+
* `emitVarRef` coerces to "". The helpers below still run for non-VarRef
|
|
15
|
+
* subjects (literals, BinaryOp, FnCall) where the coercion isn't in
|
|
16
|
+
* play. See packages/swig-twig/lib/parser.js `parseExpression` IS/ISNOT
|
|
17
|
+
* branch for the special-case logic.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function isNumber(v) {
|
|
21
|
+
return typeof v === 'number' && !isNaN(v);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `foo is defined` — true when the subject is not `undefined`. The
|
|
26
|
+
* VarRef-subject path bypasses this helper and uses IRVarRefExists.
|
|
27
|
+
*
|
|
28
|
+
* @param {*} v
|
|
29
|
+
* @return {boolean}
|
|
30
|
+
*/
|
|
31
|
+
exports['defined'] = function (v) {
|
|
32
|
+
return typeof v !== 'undefined';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `foo is null` — true when the subject is `null` or `undefined`. The
|
|
37
|
+
* VarRef-subject path bypasses this helper and uses `!IRVarRefExists`.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} v
|
|
40
|
+
* @return {boolean}
|
|
41
|
+
*/
|
|
42
|
+
exports['null'] = function (v) {
|
|
43
|
+
return v === null || typeof v === 'undefined';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `foo is empty` — true for `null`, `undefined`, `""`, empty arrays,
|
|
48
|
+
* and objects with no own-enumerable keys.
|
|
49
|
+
*
|
|
50
|
+
* @param {*} v
|
|
51
|
+
* @return {boolean}
|
|
52
|
+
*/
|
|
53
|
+
exports['empty'] = function (v) {
|
|
54
|
+
if (v === null || typeof v === 'undefined') { return true; }
|
|
55
|
+
if (typeof v === 'string') { return v.length === 0; }
|
|
56
|
+
if (Object.prototype.toString.call(v) === '[object Array]') { return v.length === 0; }
|
|
57
|
+
if (typeof v === 'object') {
|
|
58
|
+
for (var k in v) {
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(v, k)) { return false; }
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `foo is iterable` — true for arrays and non-null objects (mirrors
|
|
68
|
+
* Twig's rule that dicts iterate by key).
|
|
69
|
+
*
|
|
70
|
+
* @param {*} v
|
|
71
|
+
* @return {boolean}
|
|
72
|
+
*/
|
|
73
|
+
exports['iterable'] = function (v) {
|
|
74
|
+
if (v === null || typeof v === 'undefined') { return false; }
|
|
75
|
+
if (Object.prototype.toString.call(v) === '[object Array]') { return true; }
|
|
76
|
+
return typeof v === 'object';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `n is odd` — true for numbers whose remainder mod 2 is non-zero.
|
|
81
|
+
*
|
|
82
|
+
* @param {number} v
|
|
83
|
+
* @return {boolean}
|
|
84
|
+
*/
|
|
85
|
+
exports['odd'] = function (v) {
|
|
86
|
+
return isNumber(v) && v % 2 !== 0;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `n is even` — true for numbers whose remainder mod 2 is zero.
|
|
91
|
+
*
|
|
92
|
+
* @param {number} v
|
|
93
|
+
* @return {boolean}
|
|
94
|
+
*/
|
|
95
|
+
exports['even'] = function (v) {
|
|
96
|
+
return isNumber(v) && v % 2 === 0;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `n is divisibleby(m)` — true when `m` is a non-zero number and `n % m
|
|
101
|
+
* === 0`. Twig's canonical name for the test.
|
|
102
|
+
*
|
|
103
|
+
* @param {number} v
|
|
104
|
+
* @param {number} n
|
|
105
|
+
* @return {boolean}
|
|
106
|
+
*/
|
|
107
|
+
exports['divisibleby'] = function (v, n) {
|
|
108
|
+
return isNumber(v) && isNumber(n) && n !== 0 && v % n === 0;
|
|
109
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhinostone/swig-twig",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "Twig frontend for the @rhinostone/swig-core template engine. Phase 3 of the multi-flavor architecture (see @rhinostone/swig #T16).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"template",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=12"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@rhinostone/swig-core": "2.0.0-alpha.
|
|
25
|
+
"@rhinostone/swig-core": "2.0.0-alpha.5"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|