@rhinostone/swig 2.0.0-alpha.1 → 2.0.0-alpha.3
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/.changes/unreleased/carve-dateformatter.yaml +4 -0
- package/.changes/unreleased/swig-twig-date-filter.yaml +4 -0
- package/.changes/v2.0.0-alpha.2.md +4 -0
- package/HISTORY.md +5 -0
- package/ROADMAP.md +6 -2
- package/dist/swig.js +4133 -5133
- package/dist/swig.min.js +80 -7
- package/dist/swig.min.js.map +1 -1
- package/lib/dateformatter.js +11 -198
- package/lib/filters.js +3 -4
- package/lib/lexer.js +1 -1
- package/lib/loaders/filesystem.js +3 -5
- package/lib/loaders/index.js +3 -5
- package/lib/loaders/memory.js +4 -6
- package/lib/parser.js +16 -15
- package/lib/swig.js +3 -3
- package/lib/tags/autoescape.js +24 -1
- package/lib/tags/block.js +4 -1
- package/lib/tags/elseif.js +12 -1
- package/lib/tags/extends.js +8 -0
- package/lib/tags/filter.js +68 -9
- package/lib/tags/for.js +34 -28
- package/lib/tags/if.js +43 -4
- package/lib/tags/import.js +23 -3
- package/lib/tags/include.js +69 -9
- package/lib/tags/macro.js +23 -15
- package/lib/tags/parent.js +9 -2
- package/lib/tags/raw.js +12 -1
- package/lib/tags/set.js +58 -3
- package/lib/utils.js +6 -8
- package/package.json +4 -2
package/lib/lexer.js
CHANGED
|
@@ -3,7 +3,7 @@ var utils = require('./utils');
|
|
|
3
3
|
// Token type enum lives in @rhinostone/swig-core so the shared
|
|
4
4
|
// TokenParser (expression-level codegen) and every frontend lexer
|
|
5
5
|
// agree on the numeric IDs. See .claude/architecture/multi-flavor-ir.md.
|
|
6
|
-
var TYPES = require('
|
|
6
|
+
var TYPES = require('@rhinostone/swig-core/lib/tokentypes');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A lexer token.
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase 1 carve bridge — filesystem loader moved to @rhinostone/swig-core.
|
|
3
3
|
*
|
|
4
|
-
* Kept as a
|
|
5
|
-
*
|
|
6
|
-
* predates scoped packages and cannot resolve `@rhinostone/swig-core` —
|
|
7
|
-
* keeps producing an identical browser bundle. See
|
|
4
|
+
* Kept as a shim so the in-repo consumer (`lib/loaders/index.js` →
|
|
5
|
+
* `require('./filesystem')`) keeps resolving. See
|
|
8
6
|
* .claude/architecture/multi-flavor-ir.md.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
module.exports = require('
|
|
9
|
+
module.exports = require('@rhinostone/swig-core/lib/loaders/filesystem');
|
package/lib/loaders/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase 1 carve bridge — loader aggregator moved to @rhinostone/swig-core.
|
|
3
3
|
*
|
|
4
|
-
* Kept as a
|
|
5
|
-
*
|
|
6
|
-
* cannot resolve `@rhinostone/swig-core` — keeps producing an identical
|
|
7
|
-
* browser bundle. See .claude/architecture/multi-flavor-ir.md.
|
|
4
|
+
* Kept as a shim so `lib/swig.js`'s `require('./loaders')` keeps resolving.
|
|
5
|
+
* See .claude/architecture/multi-flavor-ir.md.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
module.exports = require('
|
|
8
|
+
module.exports = require('@rhinostone/swig-core/lib/loaders');
|
package/lib/loaders/memory.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase 1 carve bridge — memory loader moved to @rhinostone/swig-core.
|
|
3
3
|
*
|
|
4
|
-
* Kept as a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `@rhinostone/swig-core` — keeps producing an identical browser bundle.
|
|
8
|
-
* See .claude/architecture/multi-flavor-ir.md.
|
|
4
|
+
* Kept as a shim so the in-repo consumer (`lib/loaders/index.js` →
|
|
5
|
+
* `require('./memory')`) keeps resolving. See
|
|
6
|
+
* .claude/architecture/multi-flavor-ir.md.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
module.exports = require('
|
|
9
|
+
module.exports = require('@rhinostone/swig-core/lib/loaders/memory');
|
package/lib/parser.js
CHANGED
|
@@ -9,7 +9,7 @@ var _t = lexer.types;
|
|
|
9
9
|
// receives filters + filename as per-call arguments so each flavor
|
|
10
10
|
// frontend (native Swig, future Twig / Jinja2 / Django) plugs in its
|
|
11
11
|
// own catalog. See .claude/architecture/multi-flavor-ir.md.
|
|
12
|
-
var TokenParser = require('
|
|
12
|
+
var TokenParser = require('@rhinostone/swig-core/lib/tokenparser').TokenParser;
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -116,25 +116,20 @@ exports.parse = function (swig, source, opts, tags, filters) {
|
|
|
116
116
|
function parseVariable(str, line) {
|
|
117
117
|
var tokens = lexer.read(utils.strip(str)),
|
|
118
118
|
parser,
|
|
119
|
-
|
|
119
|
+
node;
|
|
120
120
|
|
|
121
121
|
parser = new TokenParser(tokens, filters, escape, line, opts.filename);
|
|
122
|
-
|
|
122
|
+
node = parser.parseOutput(tokens);
|
|
123
123
|
|
|
124
124
|
if (parser.state.length) {
|
|
125
125
|
utils.throwError('Unable to parse "' + str + '"', line, opts.filename);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
compile: function () {
|
|
135
|
-
return '_output += ' + out + ';\n';
|
|
136
|
-
}
|
|
137
|
-
};
|
|
128
|
+
// Pre-built IR node. The backend walker detects the missing
|
|
129
|
+
// `.compile` and splices the node in directly (no second compile
|
|
130
|
+
// pass). See packages/swig-core/lib/backend.js § pre-built IR
|
|
131
|
+
// detection.
|
|
132
|
+
return node;
|
|
138
133
|
}
|
|
139
134
|
exports.parseVariable = parseVariable;
|
|
140
135
|
|
|
@@ -221,6 +216,11 @@ exports.parse = function (swig, source, opts, tags, filters) {
|
|
|
221
216
|
break;
|
|
222
217
|
}
|
|
223
218
|
|
|
219
|
+
var irExpr;
|
|
220
|
+
if (typeof tag.lowerExpr === 'function') {
|
|
221
|
+
irExpr = tag.lowerExpr(parser, tokens, chunks[1], line);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
224
|
/**
|
|
225
225
|
* A parsed tag token.
|
|
226
226
|
* @typedef {Object} TagToken
|
|
@@ -236,7 +236,8 @@ exports.parse = function (swig, source, opts, tags, filters) {
|
|
|
236
236
|
args: args,
|
|
237
237
|
content: [],
|
|
238
238
|
ends: tag.ends,
|
|
239
|
-
name: tagName
|
|
239
|
+
name: tagName,
|
|
240
|
+
irExpr: irExpr
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -365,4 +366,4 @@ exports.parse = function (swig, source, opts, tags, filters) {
|
|
|
365
366
|
// (packages/swig-core/lib/backend.js). Re-exported here so lib/swig.js
|
|
366
367
|
// and lib/tags/import.js keep resolving `parser.compile` through the
|
|
367
368
|
// same module path. See .claude/architecture/multi-flavor-ir.md.
|
|
368
|
-
exports.compile = require('
|
|
369
|
+
exports.compile = require('@rhinostone/swig-core/lib/backend').compile;
|
package/lib/swig.js
CHANGED
|
@@ -4,16 +4,16 @@ var utils = require('./utils'),
|
|
|
4
4
|
parser = require('./parser'),
|
|
5
5
|
dateformatter = require('./dateformatter'),
|
|
6
6
|
loaders = require('./loaders'),
|
|
7
|
-
engine = require('
|
|
7
|
+
engine = require('@rhinostone/swig-core/lib/engine');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Swig version number as a string.
|
|
11
11
|
* @example
|
|
12
|
-
* if (swig.version === "2.0.0-alpha.
|
|
12
|
+
* if (swig.version === "2.0.0-alpha.3") { ... }
|
|
13
13
|
*
|
|
14
14
|
* @type {String}
|
|
15
15
|
*/
|
|
16
|
-
exports.version = "2.0.0-alpha.
|
|
16
|
+
exports.version = "2.0.0-alpha.3";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Swig Options Object. This object can be passed to many of the API-level Swig methods to control various aspects of the engine. All keys are optional.
|
package/lib/tags/autoescape.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
var utils = require('../utils'),
|
|
2
|
+
ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
3
|
strings = ['html', 'js'];
|
|
3
4
|
|
|
5
|
+
/*!
|
|
6
|
+
* Lower the frontend's raw autoescape-strategy token match into the
|
|
7
|
+
* typed shape IRAutoescape.strategy expects. The parser captures
|
|
8
|
+
* `token.match` verbatim (BOOL: 'true'/'false'; STRING: quoted like
|
|
9
|
+
* "'js'"). The native backend is a no-op on strategy (parser already
|
|
10
|
+
* baked escape behavior into the variable tokens at parse time), but
|
|
11
|
+
* a tight IR keeps the shape honest for future flavor backends.
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
function lowerStrategy(raw) {
|
|
15
|
+
if (raw === 'true') { return true; }
|
|
16
|
+
if (raw === 'false') { return false; }
|
|
17
|
+
if (typeof raw === 'string' && raw.length >= 2) {
|
|
18
|
+
var first = raw.charAt(0), last = raw.charAt(raw.length - 1);
|
|
19
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
20
|
+
return raw.slice(1, -1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
/**
|
|
5
27
|
* Control auto-escaping of variable output from within your templates.
|
|
6
28
|
*
|
|
@@ -16,7 +38,8 @@ var utils = require('../utils'),
|
|
|
16
38
|
* @param {boolean|string} control One of `true`, `false`, `"js"` or `"html"`.
|
|
17
39
|
*/
|
|
18
40
|
exports.compile = function (compiler, args, content, parents, options, blockName) {
|
|
19
|
-
|
|
41
|
+
var bodyJS = compiler(content, parents, options, blockName);
|
|
42
|
+
return ir.autoescape(lowerStrategy(args[0]), [ir.legacyJS(bodyJS)]);
|
|
20
43
|
};
|
|
21
44
|
exports.parse = function (str, line, parser, types, stack, opts) {
|
|
22
45
|
var matched;
|
package/lib/tags/block.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
var ir = require('@rhinostone/swig-core/lib/ir');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Defines a block in a template that can be overridden by a template extending this one and/or will override the current template's parent template block of the same name.
|
|
3
5
|
*
|
|
@@ -11,7 +13,8 @@
|
|
|
11
13
|
* @param {literal} name Name of the block for use in parent and extended templates.
|
|
12
14
|
*/
|
|
13
15
|
exports.compile = function (compiler, args, content, parents, options) {
|
|
14
|
-
|
|
16
|
+
var name = args.join('');
|
|
17
|
+
return ir.block(name, [ir.legacyJS(compiler(content, parents, options, name))]);
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
exports.parse = function (str, line, parser) {
|
package/lib/tags/elseif.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
var ifparser = require('./if').parse
|
|
1
|
+
var ifparser = require('./if').parse,
|
|
2
|
+
_t = require('@rhinostone/swig-core/lib/tokentypes');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Like <code data-language="swig">{% else %}</code>, except this tag can take more conditional statements.
|
|
@@ -22,6 +23,16 @@ exports.compile = function (compiler, args) {
|
|
|
22
23
|
return '} else if (' + args.join(' ') + ') {\n';
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
exports.lowerExpr = function (parser, tokens) {
|
|
27
|
+
var i;
|
|
28
|
+
for (i = 0; i < tokens.length; i++) {
|
|
29
|
+
if (tokens[i].type === _t.FILTER || tokens[i].type === _t.FILTEREMPTY) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parser.parseExpr(tokens);
|
|
34
|
+
};
|
|
35
|
+
|
|
25
36
|
exports.parse = function (str, line, parser, types, stack) {
|
|
26
37
|
var okay = ifparser(str, line, parser, types, stack);
|
|
27
38
|
return okay && (stack.length && stack[stack.length - 1].name === 'if');
|
package/lib/tags/extends.js
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @param {string} parentFile Relative path to the file that this template extends.
|
|
12
12
|
*/
|
|
13
|
+
// Phase 2 (#T15): extends is a parse-time declaration, not an emit-time
|
|
14
|
+
// construct. The engine's getParents / remapBlocks resolves the parent
|
|
15
|
+
// chain before the backend walks the token tree, so by compile-time
|
|
16
|
+
// there is nothing to emit. No IRExtends node exists — the Template IR
|
|
17
|
+
// already carries `.parent` / `.blocks` metadata for flavors that want
|
|
18
|
+
// to reason about inheritance before lowering. The compile function
|
|
19
|
+
// returns undefined and the backend skips it via the `result === undefined`
|
|
20
|
+
// check at the top of the emit loop. This stays this way post-Phase 2.
|
|
13
21
|
exports.compile = function () {};
|
|
14
22
|
|
|
15
23
|
exports.parse = function () {
|
package/lib/tags/filter.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
var filters = require('../filters')
|
|
1
|
+
var filters = require('../filters'),
|
|
2
|
+
ir = require('@rhinostone/swig-core/lib/ir'),
|
|
3
|
+
_t = require('@rhinostone/swig-core/lib/tokentypes');
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Apply a filter to an entire block of template.
|
|
@@ -16,20 +18,77 @@ var filters = require('../filters');
|
|
|
16
18
|
* @param {function} filter The filter that should be applied to the contents of the tag.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
exports.compile = function (compiler, args, content, parents, options, blockName) {
|
|
21
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
20
22
|
var filter = args.shift().replace(/\($/, ''),
|
|
21
|
-
|
|
22
|
-
' var _output = "";\n' +
|
|
23
|
-
compiler(content, parents, options, blockName) +
|
|
24
|
-
' return _output;\n' +
|
|
25
|
-
'})()';
|
|
23
|
+
bodyJS = compiler(content, parents, options, blockName);
|
|
26
24
|
|
|
27
25
|
if (args[args.length - 1] === ')') {
|
|
28
26
|
args.pop();
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// Value prefers the lowered IRExpr[] carried on the token when
|
|
30
|
+
// lowerExpr produced one; otherwise fall back to the joined JS-source
|
|
31
|
+
// fragments the parse handler emitted (preserves userland setTag shape
|
|
32
|
+
// and the arg-less `{% filter name %}` case).
|
|
33
|
+
var irArgs = (token && token.irExpr) ? token.irExpr : (args.length ? args : undefined);
|
|
34
|
+
return ir.filter(filter, [ir.legacyJS(bodyJS)], irArgs);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.lowerExpr = function (parser, tokens) {
|
|
38
|
+
// Skip leading whitespace; find the filter-name token (VAR, FUNCTION,
|
|
39
|
+
// or FUNCTIONEMPTY).
|
|
40
|
+
var i = 0;
|
|
41
|
+
while (i < tokens.length && tokens[i].type === _t.WHITESPACE) { i += 1; }
|
|
42
|
+
if (i >= tokens.length) { return undefined; }
|
|
43
|
+
var head = tokens[i];
|
|
44
|
+
// Arg-less `{% filter name %}` — nothing to lower. Falling back keeps
|
|
45
|
+
// the userland setTag string-args path live for zero-arg filters.
|
|
46
|
+
if (head.type === _t.VAR) { return undefined; }
|
|
47
|
+
if (head.type === _t.FUNCTIONEMPTY) { return []; }
|
|
48
|
+
if (head.type !== _t.FUNCTION) { return undefined; }
|
|
49
|
+
|
|
50
|
+
// FUNCTION consumes its implicit open paren; walk forward tracking
|
|
51
|
+
// paren / bracket / curly depth. Slice at top-level COMMAs; stop at the
|
|
52
|
+
// PARENCLOSE that balances the FUNCTION's paren. Bail (return
|
|
53
|
+
// undefined) on any nested FILTER / FILTEREMPTY — filter pipes inside
|
|
54
|
+
// the argument expression are not part of the expression grammar
|
|
55
|
+
// (Output-site concern; Session 14+ work).
|
|
56
|
+
var depth = 1,
|
|
57
|
+
start = i + 1,
|
|
58
|
+
slices = [],
|
|
59
|
+
j;
|
|
60
|
+
for (j = i + 1; j < tokens.length; j += 1) {
|
|
61
|
+
var tk = tokens[j];
|
|
62
|
+
if (tk.type === _t.FILTER || tk.type === _t.FILTEREMPTY) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (tk.type === _t.PARENOPEN || tk.type === _t.FUNCTION ||
|
|
66
|
+
tk.type === _t.BRACKETOPEN || tk.type === _t.CURLYOPEN) {
|
|
67
|
+
depth += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (tk.type === _t.PARENCLOSE || tk.type === _t.BRACKETCLOSE ||
|
|
71
|
+
tk.type === _t.CURLYCLOSE) {
|
|
72
|
+
depth -= 1;
|
|
73
|
+
if (depth === 0) {
|
|
74
|
+
if (j > start) {
|
|
75
|
+
slices.push(tokens.slice(start, j));
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (tk.type === _t.COMMA && depth === 1) {
|
|
82
|
+
slices.push(tokens.slice(start, j));
|
|
83
|
+
start = j + 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (depth !== 0) { return undefined; }
|
|
87
|
+
var exprs = [];
|
|
88
|
+
for (j = 0; j < slices.length; j += 1) {
|
|
89
|
+
exprs.push(parser.parseExpr(slices[j]));
|
|
90
|
+
}
|
|
91
|
+
return exprs;
|
|
33
92
|
};
|
|
34
93
|
|
|
35
94
|
exports.parse = function (str, line, parser, types) {
|
package/lib/tags/for.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
1
|
+
var ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
|
+
_t = require('@rhinostone/swig-core/lib/tokentypes');
|
|
3
3
|
|
|
4
4
|
// CVE-2023-25345: prototype-chain properties that must not be used as loop
|
|
5
5
|
// variable names. The for tag assigns loop variables to _ctx, so dangerous
|
|
6
6
|
// names would pollute the prototype chain. Shared constant in
|
|
7
7
|
// @rhinostone/swig-core — see .claude/security.md.
|
|
8
|
-
var _dangerousProps = require('
|
|
8
|
+
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Loop over objects and arrays.
|
|
@@ -47,10 +47,9 @@ var _dangerousProps = require('../../packages/swig-core/lib/security').dangerous
|
|
|
47
47
|
* @return {loop.first} True if the current object is the first in the object or array.
|
|
48
48
|
* @return {loop.last} True if the current object is the last in the object or array.
|
|
49
49
|
*/
|
|
50
|
-
exports.compile = function (compiler, args, content, parents, options, blockName) {
|
|
50
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
51
51
|
var val = args.shift(),
|
|
52
52
|
key = '__k',
|
|
53
|
-
ctxloopcache = (ctx + '__loopcache' + Math.random()).replace(/\./g, ''),
|
|
54
53
|
last;
|
|
55
54
|
|
|
56
55
|
if (args[0] && args[0] === ',') {
|
|
@@ -59,29 +58,28 @@ exports.compile = function (compiler, args, content, parents, options, blockName
|
|
|
59
58
|
val = args.shift();
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
last = args.join('');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
].join('');
|
|
61
|
+
last = (token && token.irExpr) ? token.irExpr : args.join('');
|
|
62
|
+
|
|
63
|
+
var bodyJS = compiler(content, parents, options, blockName);
|
|
64
|
+
return ir.forStmt(val, last, [ir.legacyJS(bodyJS)], key);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
exports.lowerExpr = function (parser, tokens) {
|
|
68
|
+
var inIdx = -1, i;
|
|
69
|
+
for (i = 0; i < tokens.length; i++) {
|
|
70
|
+
if (tokens[i].type === _t.COMPARATOR && tokens[i].match === 'in') {
|
|
71
|
+
inIdx = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (inIdx === -1) { return undefined; }
|
|
76
|
+
var iterTokens = tokens.slice(inIdx + 1);
|
|
77
|
+
for (i = 0; i < iterTokens.length; i++) {
|
|
78
|
+
if (iterTokens[i].type === _t.FILTER || iterTokens[i].type === _t.FILTEREMPTY) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parser.parseExpr(iterTokens);
|
|
85
83
|
};
|
|
86
84
|
|
|
87
85
|
exports.parse = function (str, line, parser, types) {
|
|
@@ -106,6 +104,14 @@ exports.parse = function (str, line, parser, types) {
|
|
|
106
104
|
return true;
|
|
107
105
|
}
|
|
108
106
|
|
|
107
|
+
// CVE-2023-25345: the lexer folds dotted paths into a single VAR token
|
|
108
|
+
// (e.g. `foo.__proto__` matches as one VAR with .match === "foo.__proto__"),
|
|
109
|
+
// so the _dangerousProps indexOf check below misses the prototype segment.
|
|
110
|
+
// Loop variables bind to `_ctx.<name>` — bare identifiers only.
|
|
111
|
+
if (token.match.indexOf('.') !== -1) {
|
|
112
|
+
throw new Error('Loop variable "' + token.match + '" must be a bare identifier in "for" tag on line ' + line + '.');
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
// CVE-2023-25345: block prototype-chain property names as loop variables
|
|
110
116
|
if (_dangerousProps.indexOf(token.match) !== -1) {
|
|
111
117
|
throw new Error('Unsafe loop variable "' + token.match + '" is not allowed (CVE-2023-25345) on line ' + line + '.');
|
package/lib/tags/if.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
|
+
_t = require('@rhinostone/swig-core/lib/tokentypes');
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Used to create conditional statements in templates. Accepts most JavaScript valid comparisons.
|
|
3
6
|
*
|
|
@@ -39,10 +42,46 @@
|
|
|
39
42
|
*
|
|
40
43
|
* @param {...mixed} conditional Conditional statement that returns a truthy or falsy value.
|
|
41
44
|
*/
|
|
42
|
-
exports.compile = function (compiler, args, content, parents, options, blockName) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
46
|
+
// Phase 2 Session 14b Commit 11: filter-in-test fallback lifts into
|
|
47
|
+
// `IRLegacyJS` rather than a raw string. `if.lowerExpr` bails to
|
|
48
|
+
// `undefined` when the test contains FILTER/FILTEREMPTY because
|
|
49
|
+
// per-operand filter precedence (`a === b|upper` binds the filter to
|
|
50
|
+
// `b` only) can't be captured in flat IR — same widening recipe as
|
|
51
|
+
// `IROutput.expr` (Session 14b Commit 9). Backend dispatches
|
|
52
|
+
// `LegacyJS` before `emitExpr`. String fallback still valid for
|
|
53
|
+
// userland `setTag` compile functions that build branches manually.
|
|
54
|
+
var branches = [],
|
|
55
|
+
currentTest = (token && token.irExpr) ? token.irExpr : ir.legacyJS(args.join(' ')),
|
|
56
|
+
currentSlice = [],
|
|
57
|
+
i,
|
|
58
|
+
t;
|
|
59
|
+
for (i = 0; i < content.length; i++) {
|
|
60
|
+
t = content[i];
|
|
61
|
+
if (t && (t.name === 'elseif' || t.name === 'else')) {
|
|
62
|
+
branches.push(ir.ifBranch(currentTest, [ir.legacyJS(compiler(currentSlice, parents, options, blockName))]));
|
|
63
|
+
if (t.name === 'else') {
|
|
64
|
+
currentTest = null;
|
|
65
|
+
} else {
|
|
66
|
+
currentTest = (t.irExpr) ? t.irExpr : ir.legacyJS(t.args.join(' '));
|
|
67
|
+
}
|
|
68
|
+
currentSlice = [];
|
|
69
|
+
} else {
|
|
70
|
+
currentSlice.push(t);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
branches.push(ir.ifBranch(currentTest, [ir.legacyJS(compiler(currentSlice, parents, options, blockName))]));
|
|
74
|
+
return ir.ifStmt(branches);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
exports.lowerExpr = function (parser, tokens) {
|
|
78
|
+
var i;
|
|
79
|
+
for (i = 0; i < tokens.length; i++) {
|
|
80
|
+
if (tokens[i].type === _t.FILTER || tokens[i].type === _t.FILTEREMPTY) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return parser.parseExpr(tokens);
|
|
46
85
|
};
|
|
47
86
|
|
|
48
87
|
exports.parse = function (str, line, parser, types) {
|
package/lib/tags/import.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
// Phase 2 (#T15): the import tag stays on the IRLegacyJS escape hatch.
|
|
2
|
+
// The compile function performs regex surgery over the compiled body
|
|
3
|
+
// of each imported macro to rewrite `_ctx.<macroName>` into
|
|
4
|
+
// `_ctx.<namespace>.<macroName>`, including sibling-macro references
|
|
5
|
+
// (the `(?!' + allMacros + ')` negative lookahead). That rewrite is
|
|
6
|
+
// swig-specific coupling on the exact JS source shape of a compiled
|
|
7
|
+
// macro — it has no cross-flavor invariant and does not meet the
|
|
8
|
+
// flavor-invariant test for a dedicated IR node. When TokenParser
|
|
9
|
+
// migrates to IRExpr (Session 14+), the macro-name → namespace
|
|
10
|
+
// rewrite moves into the emitter itself, at which point the import
|
|
11
|
+
// tag collapses to an IRImport node. Until then, return a JS source
|
|
12
|
+
// string; the backend lifts it into IRLegacyJS at the emit-loop
|
|
13
|
+
// entry. See .claude/architecture/multi-flavor-ir.md § Flavor-
|
|
14
|
+
// invariant test.
|
|
15
|
+
var utils = require('../utils'),
|
|
16
|
+
backend = require('@rhinostone/swig-core/lib/backend');
|
|
2
17
|
|
|
3
18
|
// CVE-2023-25345: prototype-chain properties that must not be used as import
|
|
4
19
|
// aliases. The import tag assigns a namespace object to _ctx, so dangerous
|
|
5
20
|
// names would pollute the prototype chain. Shared constant in
|
|
6
21
|
// @rhinostone/swig-core — see .claude/security.md.
|
|
7
|
-
var _dangerousProps = require('
|
|
22
|
+
var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
|
|
8
23
|
|
|
9
24
|
/**
|
|
10
25
|
* Allows you to import macros from another file directly into your current context.
|
|
@@ -70,7 +85,12 @@ exports.parse = function (str, line, parser, types, stack, opts, swig) {
|
|
|
70
85
|
return;
|
|
71
86
|
}
|
|
72
87
|
macroName = token.args[0];
|
|
73
|
-
|
|
88
|
+
// Phase 2 (#T15): macro.compile now returns an IRMacro node
|
|
89
|
+
// rather than a JS source string. Render it through the shared
|
|
90
|
+
// backend so import.js still gets the JS source it performs
|
|
91
|
+
// regex-surgery on for namespace-prefixing. The +'\n' trailing
|
|
92
|
+
// newline matches the pre-Phase-2 compile output exactly.
|
|
93
|
+
out += backend.compile([token.compile(compiler, token.args, token.content, [], compileOpts)], [], compileOpts) + '\n';
|
|
74
94
|
self.out.push({compiled: out, name: macroName});
|
|
75
95
|
});
|
|
76
96
|
return;
|
package/lib/tags/include.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var ir = require('@rhinostone/swig-core/lib/ir'),
|
|
2
|
+
_t = require('@rhinostone/swig-core/lib/tokentypes');
|
|
3
|
+
|
|
1
4
|
var ignore = 'ignore',
|
|
2
5
|
missing = 'missing',
|
|
3
6
|
only = 'only';
|
|
@@ -28,21 +31,78 @@ var ignore = 'ignore',
|
|
|
28
31
|
* @param {literal} [only] Restricts to <strong>only</strong> passing the <code>with context</code> as local variables–the included template will not be aware of any other local variables in the parent template. For best performance, usage of this option is recommended if possible.
|
|
29
32
|
* @param {literal} [ignore missing] Will output empty string if not found instead of throwing an error.
|
|
30
33
|
*/
|
|
31
|
-
exports.compile = function (compiler, args) {
|
|
34
|
+
exports.compile = function (compiler, args, content, parents, options, blockName, token) {
|
|
32
35
|
var file = args.shift(),
|
|
33
36
|
onlyIdx = args.indexOf(only),
|
|
34
37
|
onlyCtx = onlyIdx !== -1 ? args.splice(onlyIdx, 1) : false,
|
|
35
38
|
parentFile = (args.pop() || '').replace(/\\/g, '\\\\'),
|
|
36
|
-
|
|
39
|
+
ignoreMissing = args[args.length - 1] === missing ? (args.pop()) : false,
|
|
37
40
|
w = args.join('');
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
if (token && token.irExpr) {
|
|
43
|
+
file = token.irExpr.file;
|
|
44
|
+
if (token.irExpr.context !== undefined) {
|
|
45
|
+
w = token.irExpr.context;
|
|
46
|
+
} else {
|
|
47
|
+
w = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return ir.include(file, w || undefined, !!onlyCtx, !!ignoreMissing, parentFile);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
exports.lowerExpr = function (parser, tokens) {
|
|
55
|
+
var i, tk, depth = 0,
|
|
56
|
+
withIdx = -1, onlyIdx = -1, ignoreIdx = -1;
|
|
57
|
+
|
|
58
|
+
for (i = 0; i < tokens.length; i++) {
|
|
59
|
+
tk = tokens[i];
|
|
60
|
+
if (tk.type === _t.FILTER || tk.type === _t.FILTEREMPTY) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (tk.type === _t.PARENOPEN || tk.type === _t.BRACKETOPEN ||
|
|
64
|
+
tk.type === _t.ARRAYOPEN || tk.type === _t.CURLYOPEN ||
|
|
65
|
+
tk.type === _t.FUNCTION || tk.type === _t.METHODOPEN) {
|
|
66
|
+
depth++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (tk.type === _t.PARENCLOSE || tk.type === _t.BRACKETCLOSE ||
|
|
70
|
+
tk.type === _t.CURLYCLOSE) {
|
|
71
|
+
depth--;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (depth !== 0) { continue; }
|
|
75
|
+
if (tk.type === _t.VAR) {
|
|
76
|
+
if (withIdx === -1 && tk.match === 'with') {
|
|
77
|
+
withIdx = i;
|
|
78
|
+
} else if (withIdx !== -1 && onlyIdx === -1 && tk.match === only) {
|
|
79
|
+
onlyIdx = i;
|
|
80
|
+
} else if (ignoreIdx === -1 && tk.match === ignore) {
|
|
81
|
+
ignoreIdx = i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var pathEnd = tokens.length;
|
|
87
|
+
if (withIdx !== -1) { pathEnd = withIdx; }
|
|
88
|
+
else if (ignoreIdx !== -1) { pathEnd = ignoreIdx; }
|
|
89
|
+
|
|
90
|
+
var pathTokens = tokens.slice(0, pathEnd);
|
|
91
|
+
if (pathTokens.length === 0) { return undefined; }
|
|
92
|
+
|
|
93
|
+
var result = { file: parser.parseExpr(pathTokens) };
|
|
94
|
+
|
|
95
|
+
if (withIdx !== -1) {
|
|
96
|
+
var ctxEnd = tokens.length;
|
|
97
|
+
if (onlyIdx !== -1) { ctxEnd = onlyIdx; }
|
|
98
|
+
else if (ignoreIdx !== -1) { ctxEnd = ignoreIdx; }
|
|
99
|
+
var ctxTokens = tokens.slice(withIdx + 1, ctxEnd);
|
|
100
|
+
if (ctxTokens.length > 0) {
|
|
101
|
+
result.context = parser.parseExpr(ctxTokens);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
46
106
|
};
|
|
47
107
|
|
|
48
108
|
exports.parse = function (str, line, parser, types, stack, opts) {
|