@rhinostone/swig-jinja2 2.5.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.
@@ -0,0 +1,121 @@
1
+ /*!
2
+ * Jinja2 `{% with %}` tag.
3
+ *
4
+ * Jinja2 scoped-context region with optional named assignments:
5
+ *
6
+ * {% with %}…{% endwith %} (new scope, shallow copy of _ctx)
7
+ * {% with x = 1 %}…{% endwith %} (x bound in the inner scope)
8
+ * {% with x = 1, y = total + 1 %}…{% endwith %} (multiple assignments)
9
+ *
10
+ * Each assignment is `<name> = <expr>`; the value expression is lowered
11
+ * through `parser.parseExpr`, so object literals, variable references,
12
+ * conditionals, and function calls all route through the same path. The
13
+ * assignments are collected into an object literal that the backend
14
+ * merges over a shallow copy of the outer context — so an assignment's
15
+ * value sees the enclosing scope (not its sibling assignments), and the
16
+ * inner bindings do not leak past `{% endwith %}`.
17
+ *
18
+ * Unlike Twig's `{% with {ctx} only %}`, Jinja2 has no `only` keyword and
19
+ * the region is never isolated — the outer context stays visible inside.
20
+ *
21
+ * Each assignment target is a bare identifier — dotted paths are rejected
22
+ * at parse time, and CVE-2023-25345 prototype-chain names are rejected
23
+ * before the binding lands (a quoted `"__proto__"` object key still sets
24
+ * the prototype in JS).
25
+ */
26
+
27
+ var ir = require('@rhinostone/swig-core/lib/ir');
28
+ var utils = require('@rhinostone/swig-core/lib/utils');
29
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
30
+
31
+ var lexer = require('../lexer');
32
+
33
+ exports.ends = true;
34
+ exports.block = false;
35
+
36
+ /**
37
+ * Parse the `{% with %}` tag body. Collects zero or more
38
+ * comma-separated `<name> = <expr>` assignments into an object literal on
39
+ * `token.irExpr`. A bare `{% with %}` leaves `token.irExpr` undefined so
40
+ * the backend emits a shallow copy of `_ctx`.
41
+ *
42
+ * @param {string} str Tag body.
43
+ * @param {number} line Source line of the opening `{%`.
44
+ * @param {object} parser The Jinja2 parser module (exposes `parseExpr`).
45
+ * @param {object} types Jinja2 lexer token-type enum.
46
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
47
+ * @param {object} opts Per-call options (honors `opts.filename`).
48
+ * @param {object} swig Swig instance (unused).
49
+ * @param {object} token In-progress TagToken.
50
+ * @return {boolean} Always `true` on success. Throws otherwise.
51
+ */
52
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
53
+ var tokens = lexer.read(utils.strip(str));
54
+ var pos = 0;
55
+
56
+ function peek() {
57
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
58
+ return pos < tokens.length ? tokens[pos] : null;
59
+ }
60
+ function consume() {
61
+ var t = peek();
62
+ if (t) { pos += 1; }
63
+ return t;
64
+ }
65
+
66
+ var props = [];
67
+
68
+ // Bare `{% with %}` has no assignments — the leading peek() is null and
69
+ // the loop is skipped, leaving token.irExpr undefined.
70
+ if (peek()) {
71
+ while (true) {
72
+ var nameTok = consume();
73
+ if (!nameTok || nameTok.type !== types.VAR) {
74
+ utils.throwError('Expected variable name in "with" tag', line, opts.filename);
75
+ }
76
+ if (nameTok.match.indexOf('.') !== -1) {
77
+ utils.throwError('"with" assignment target "' + nameTok.match + '" must be a bare identifier', line, opts.filename);
78
+ }
79
+ if (_dangerousProps.indexOf(nameTok.match) !== -1) {
80
+ utils.throwError('Unsafe "with" assignment to "' + nameTok.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
81
+ }
82
+
83
+ var eqTok = consume();
84
+ if (!eqTok || eqTok.type !== types.ASSIGNMENT || eqTok.match !== '=') {
85
+ utils.throwError('Expected "=" after "' + nameTok.match + '" in "with" tag', line, opts.filename);
86
+ }
87
+
88
+ // Parse a single value expression from the cursor. parseExpr stops
89
+ // at the next COMMA (which is not part of an expression); the
90
+ // out-param reports how many slice tokens it consumed.
91
+ var posOut = {};
92
+ var valExpr = parser.parseExpr(tokens.slice(pos), {}, posOut);
93
+ pos += posOut.pos;
94
+ props.push(ir.objectProperty(ir.literal('string', nameTok.match), valExpr));
95
+
96
+ var next = peek();
97
+ if (!next) { break; }
98
+ if (next.type !== types.COMMA) {
99
+ utils.throwError('Expected "," between assignments in "with" tag', line, opts.filename);
100
+ }
101
+ consume();
102
+ }
103
+ }
104
+
105
+ token.irExpr = props.length ? ir.objectLiteral(props) : undefined;
106
+ return true;
107
+ };
108
+
109
+ /**
110
+ * Emit an IRWith node carrying the optional context object literal (the
111
+ * collected assignments) and the recursively-compiled body wrapped in
112
+ * IRLegacyJS. The region is never isolated; the backend's `With` branch
113
+ * merges the context over a shallow copy of `_ctx` for the body's lexical
114
+ * scope.
115
+ *
116
+ * @return {object} IRWith node.
117
+ */
118
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
119
+ var bodyJS = compiler(content, parents, options, blockName);
120
+ return ir.withStmt(token.irExpr, false, [ir.legacyJS(bodyJS)]);
121
+ };
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @rhinostone/swig-jinja2 — built-in test runtime helpers.
3
+ *
4
+ * Jinja2 `is <name>` / `is not <name>` expressions lower to
5
+ * `_ext._test_<name>(subject, ...args)` at the IR layer. The Jinja2
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 Jinja2().render(...)`) honors
9
+ * per-instance overrides without leaking cross-instance.
10
+ *
11
+ * Three tests (`defined`, `none`, `undefined`) are additionally
12
+ * special-cased in the parser when the subject is a VarRef with no args:
13
+ * they route through IRVarRefExists to preserve the defined/undefined
14
+ * signal that `emitVarRef` coerces to "". The helpers below still run for
15
+ * non-VarRef subjects (literals, BinaryOp, FnCall) where the coercion is
16
+ * not in play. See parser.js `parseExpression` IS/ISNOT branch.
17
+ *
18
+ * JS / Python impedance notes (documented divergences):
19
+ * - No int/float distinction in JS, so `is integer` / `is float` are not
20
+ * provided; use `is number`.
21
+ * - `is sequence` is array-or-string (ordered, integer-indexed); a dict
22
+ * is `is mapping` (not `is sequence`). `is iterable` covers arrays,
23
+ * strings, and objects, matching the `{% for %}` iterate-by-key rule.
24
+ */
25
+
26
+ /*!
27
+ * Array detection without depending on the runtime's Array.isArray (kept
28
+ * browser-safe and consistent with the other per-flavor test helpers).
29
+ * @private
30
+ */
31
+ function isArr(v) {
32
+ return Object.prototype.toString.call(v) === '[object Array]';
33
+ }
34
+
35
+ /*!
36
+ * Finite-number guard shared by the numeric tests. @private
37
+ */
38
+ function isNumber(v) {
39
+ return typeof v === 'number' && !isNaN(v);
40
+ }
41
+
42
+ /**
43
+ * `foo is defined` — true when the subject is not `undefined`. The
44
+ * VarRef-subject path bypasses this helper and uses IRVarRefExists.
45
+ *
46
+ * @param {*} v
47
+ * @return {boolean}
48
+ */
49
+ exports['defined'] = function (v) {
50
+ return typeof v !== 'undefined';
51
+ };
52
+
53
+ /**
54
+ * `foo is undefined` — true when the subject is `undefined`. The
55
+ * VarRef-subject path bypasses this helper and uses `!IRVarRefExists`.
56
+ *
57
+ * @param {*} v
58
+ * @return {boolean}
59
+ */
60
+ exports['undefined'] = function (v) {
61
+ return typeof v === 'undefined';
62
+ };
63
+
64
+ /**
65
+ * `foo is none` — true when the subject is `null` or `undefined` (Python
66
+ * `None`). The VarRef-subject path bypasses this helper.
67
+ *
68
+ * @param {*} v
69
+ * @return {boolean}
70
+ */
71
+ exports['none'] = function (v) {
72
+ return v === null || typeof v === 'undefined';
73
+ };
74
+
75
+ /**
76
+ * `foo is boolean` — true for a JS boolean.
77
+ *
78
+ * @param {*} v
79
+ * @return {boolean}
80
+ */
81
+ exports['boolean'] = function (v) {
82
+ return typeof v === 'boolean';
83
+ };
84
+
85
+ /**
86
+ * `foo is number` — true for a finite number.
87
+ *
88
+ * @param {*} v
89
+ * @return {boolean}
90
+ */
91
+ exports['number'] = function (v) {
92
+ return isNumber(v);
93
+ };
94
+
95
+ /**
96
+ * `foo is string` — true for a string.
97
+ *
98
+ * @param {*} v
99
+ * @return {boolean}
100
+ */
101
+ exports['string'] = function (v) {
102
+ return typeof v === 'string';
103
+ };
104
+
105
+ /**
106
+ * `foo is mapping` — true for a plain object (a dict), excluding arrays
107
+ * and null.
108
+ *
109
+ * @param {*} v
110
+ * @return {boolean}
111
+ */
112
+ exports['mapping'] = function (v) {
113
+ return typeof v === 'object' && v !== null && !isArr(v);
114
+ };
115
+
116
+ /**
117
+ * `foo is sequence` — true for an array or string (ordered,
118
+ * integer-indexed). A dict is `mapping`, not `sequence`.
119
+ *
120
+ * @param {*} v
121
+ * @return {boolean}
122
+ */
123
+ exports['sequence'] = function (v) {
124
+ return isArr(v) || typeof v === 'string';
125
+ };
126
+
127
+ /**
128
+ * `foo is iterable` — true for arrays, strings, and non-null objects
129
+ * (mirrors the `{% for %}` rule that dicts iterate by key).
130
+ *
131
+ * @param {*} v
132
+ * @return {boolean}
133
+ */
134
+ exports['iterable'] = function (v) {
135
+ if (v === null || typeof v === 'undefined') { return false; }
136
+ if (isArr(v) || typeof v === 'string') { return true; }
137
+ return typeof v === 'object';
138
+ };
139
+
140
+ /**
141
+ * `foo is callable` — true for a function.
142
+ *
143
+ * @param {*} v
144
+ * @return {boolean}
145
+ */
146
+ exports['callable'] = function (v) {
147
+ return typeof v === 'function';
148
+ };
149
+
150
+ /**
151
+ * `foo is sameas(bar)` — strict identity check (`foo === bar`).
152
+ *
153
+ * @param {*} v
154
+ * @param {*} other
155
+ * @return {boolean}
156
+ */
157
+ exports['sameas'] = function (v, other) {
158
+ return v === other;
159
+ };
160
+
161
+ /**
162
+ * `foo is lower` — true when the subject is a string equal to its own
163
+ * lowercase form.
164
+ *
165
+ * @param {*} v
166
+ * @return {boolean}
167
+ */
168
+ exports['lower'] = function (v) {
169
+ return typeof v === 'string' && v === v.toLowerCase();
170
+ };
171
+
172
+ /**
173
+ * `foo is upper` — true when the subject is a string equal to its own
174
+ * uppercase form.
175
+ *
176
+ * @param {*} v
177
+ * @return {boolean}
178
+ */
179
+ exports['upper'] = function (v) {
180
+ return typeof v === 'string' && v === v.toUpperCase();
181
+ };
182
+
183
+ /**
184
+ * `n is even` — true for numbers whose remainder mod 2 is zero.
185
+ *
186
+ * @param {number} v
187
+ * @return {boolean}
188
+ */
189
+ exports['even'] = function (v) {
190
+ return isNumber(v) && v % 2 === 0;
191
+ };
192
+
193
+ /**
194
+ * `n is odd` — true for numbers whose remainder mod 2 is non-zero.
195
+ *
196
+ * @param {number} v
197
+ * @return {boolean}
198
+ */
199
+ exports['odd'] = function (v) {
200
+ return isNumber(v) && v % 2 !== 0;
201
+ };
202
+
203
+ /**
204
+ * `n is divisibleby(m)` — true when `m` is a non-zero number and `n % m
205
+ * === 0`.
206
+ *
207
+ * @param {number} v
208
+ * @param {number} n
209
+ * @return {boolean}
210
+ */
211
+ exports['divisibleby'] = function (v, n) {
212
+ return isNumber(v) && isNumber(n) && n !== 0 && v % n === 0;
213
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Jinja2 lexer token type enum — the contract between the Jinja2 lexer and
3
+ * the Jinja2 parser in @rhinostone/swig-jinja2.
4
+ *
5
+ * Numeric IDs in the shared range (0–25, 100) mirror
6
+ * @rhinostone/swig-core/lib/tokentypes by design: Jinja2 and native Swig
7
+ * lower to the same swig-core IR, and aligning the IDs keeps shared
8
+ * consumers (e.g. CVE-2023-25345 `_dangerousProps` enforcement) flavor-
9
+ * agnostic. The Jinja2 parser is its own module — it does not inherit
10
+ * from swig-core's TokenParser — but the cognitive overhead of re-mapping
11
+ * IDs across flavors is not worth the freedom.
12
+ *
13
+ * Jinja2-only IDs (30+) are reserved here so later commits can add lexer
14
+ * rules without renumbering. Jinja2 has no range (`..`), null-coalescing
15
+ * (`??`), ternary (`?:`), or `#{}` string interpolation operators, so the
16
+ * Jinja2-only block is smaller than Twig's: `~` concat, `**` power, `//`
17
+ * floor-division, and the `is` / `is not` test keywords.
18
+ *
19
+ * @readonly
20
+ * @enum {number}
21
+ */
22
+ module.exports = {
23
+ /** Whitespace */
24
+ WHITESPACE: 0,
25
+ /** Plain string literal */
26
+ STRING: 1,
27
+ /** Variable filter call with arguments — `|name(...)` */
28
+ FILTER: 2,
29
+ /** Variable filter call with no arguments — `|name` */
30
+ FILTEREMPTY: 3,
31
+ /** Function call with arguments — `name(...)` */
32
+ FUNCTION: 4,
33
+ /** Function call with no arguments — `name()` */
34
+ FUNCTIONEMPTY: 5,
35
+ /** Open parenthesis */
36
+ PARENOPEN: 6,
37
+ /** Close parenthesis */
38
+ PARENCLOSE: 7,
39
+ /** Comma */
40
+ COMMA: 8,
41
+ /** Variable identifier */
42
+ VAR: 9,
43
+ /** Numeric literal */
44
+ NUMBER: 10,
45
+ /** Math operator (+, -, *, /, %) */
46
+ OPERATOR: 11,
47
+ /** Open square bracket */
48
+ BRACKETOPEN: 12,
49
+ /** Close square bracket */
50
+ BRACKETCLOSE: 13,
51
+ /** Dot-key accessor — `.key` */
52
+ DOTKEY: 14,
53
+ /** Open square bracket at the start of an array literal */
54
+ ARRAYOPEN: 15,
55
+ /** Open curly brace */
56
+ CURLYOPEN: 17,
57
+ /** Close curly brace */
58
+ CURLYCLOSE: 18,
59
+ /** Colon — object-literal key/value separator, and slice subscript */
60
+ COLON: 19,
61
+ /** JavaScript-valid comparator (==, !=, <=, etc.) */
62
+ COMPARATOR: 20,
63
+ /** Boolean logic (`and`, `or`, `&&`, `||`) */
64
+ LOGIC: 21,
65
+ /** Boolean negation (`not`, `!`) */
66
+ NOT: 22,
67
+ /** Boolean literal (`true`, `false`) */
68
+ BOOL: 23,
69
+ /** Variable assignment (`=`, `+=`, `-=`, `*=`, `/=`) */
70
+ ASSIGNMENT: 24,
71
+ /** Method call open — internal */
72
+ METHODOPEN: 25,
73
+
74
+ /* ---- Jinja2-only token IDs (reserved; rules land in later commits) ---- */
75
+
76
+ /** Jinja2 string-concatenation operator — `~` */
77
+ TILDE: 30,
78
+ /** Jinja2 exponentiation operator — `**` */
79
+ POWER: 31,
80
+ /** Jinja2 floor-division operator — `//` */
81
+ FLOORDIV: 32,
82
+ /** Jinja2 test operator — `is` */
83
+ IS: 33,
84
+ /** Jinja2 negated test operator — `is not` */
85
+ ISNOT: 34,
86
+
87
+ /** Unknown token */
88
+ UNKNOWN: 100
89
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@rhinostone/swig-jinja2",
3
+ "version": "2.5.0",
4
+ "description": "Jinja2-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
5
+ "keywords": [
6
+ "template",
7
+ "templating",
8
+ "jinja2",
9
+ "jinja",
10
+ "swig",
11
+ "swig-jinja2",
12
+ "frontend"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/gina-io/swig.git",
17
+ "directory": "packages/swig-jinja2"
18
+ },
19
+ "author": "Rhinostone <contact@gina.io>",
20
+ "license": "MIT",
21
+ "main": "lib/index.js",
22
+ "engines": {
23
+ "node": ">=12"
24
+ },
25
+ "peerDependencies": {
26
+ "@rhinostone/swig-core": "2.5.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }