@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/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('../packages/swig-core/lib/tokentypes');
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 relative-path shim so the in-repo consumer (`lib/loaders/index.js`
5
- * `require('./filesystem')`) keeps resolving and `browserify@2` — which
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('../../packages/swig-core/lib/loaders/filesystem');
9
+ module.exports = require('@rhinostone/swig-core/lib/loaders/filesystem');
@@ -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 relative-path shim so `lib/swig.js`'s `require('./loaders')`
5
- * keeps resolving and `browserify@2` — which predates scoped packages and
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('../../packages/swig-core/lib/loaders');
8
+ module.exports = require('@rhinostone/swig-core/lib/loaders');
@@ -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 relative-path shim so the in-repo consumer
5
- * (`lib/loaders/index.js` → `require('./memory')`) keeps resolving and
6
- * `browserify@2` — which predates scoped packages and cannot resolve
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('../../packages/swig-core/lib/loaders/memory');
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('../packages/swig-core/lib/tokenparser').TokenParser;
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
- out;
119
+ node;
120
120
 
121
121
  parser = new TokenParser(tokens, filters, escape, line, opts.filename);
122
- out = parser.parse().join('');
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
- * A parsed variable token.
130
- * @typedef {object} VarToken
131
- * @property {function} compile Method for compiling this token.
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('../packages/swig-core/lib/backend').compile;
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('../packages/swig-core/lib/engine');
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.1") { ... }
12
+ * if (swig.version === "2.0.0-alpha.3") { ... }
13
13
  *
14
14
  * @type {String}
15
15
  */
16
- exports.version = "2.0.0-alpha.1";
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.
@@ -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
- return compiler(content, parents, options, blockName);
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
- return compiler(content, parents, options, args.join(''));
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) {
@@ -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');
@@ -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 () {
@@ -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
- val = '(function () {\n' +
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
- args = (args.length) ? ', ' + args.join('') : '';
32
- return '_output += _filters["' + filter + '"](' + val + args + ');\n';
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 ctx = '_ctx.',
2
- ctxloop = ctx + 'loop';
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('../../packages/swig-core/lib/security').dangerousProps;
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
- return [
65
- '(function () {\n',
66
- ' var __l = ' + last + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n',
67
- ' if (!__l) { return; }\n',
68
- ' var ' + ctxloopcache + ' = { loop: ' + ctxloop + ', ' + val + ': ' + ctx + val + ', ' + key + ': ' + ctx + key + ' };\n',
69
- ' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n',
70
- ' _utils.each(__l, function (' + val + ', ' + key + ') {\n',
71
- ' ' + ctx + val + ' = ' + val + ';\n',
72
- ' ' + ctx + key + ' = ' + key + ';\n',
73
- ' ' + ctxloop + '.key = ' + key + ';\n',
74
- ' ' + ctxloop + '.first = (' + ctxloop + '.index0 === 0);\n',
75
- ' ' + ctxloop + '.last = (' + ctxloop + '.revindex0 === 0);\n',
76
- ' ' + compiler(content, parents, options, blockName),
77
- ' ' + ctxloop + '.index += 1; ' + ctxloop + '.index0 += 1; ' + ctxloop + '.revindex -= 1; ' + ctxloop + '.revindex0 -= 1;\n',
78
- ' });\n',
79
- ' ' + ctxloop + ' = ' + ctxloopcache + '.loop;\n',
80
- ' ' + ctx + val + ' = ' + ctxloopcache + '.' + val + ';\n',
81
- ' ' + ctx + key + ' = ' + ctxloopcache + '.' + key + ';\n',
82
- ' ' + ctxloopcache + ' = undefined;\n',
83
- '})();\n'
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
- return 'if (' + args.join(' ') + ') { \n' +
44
- compiler(content, parents, options, blockName) + '\n' +
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) {
@@ -1,10 +1,25 @@
1
- var utils = require('../utils');
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('../../packages/swig-core/lib/security').dangerousProps;
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
- out += token.compile(compiler, token.args, token.content, [], compileOpts) + '\n';
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;
@@ -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
- ignore = args[args.length - 1] === missing ? (args.pop()) : false,
39
+ ignoreMissing = args[args.length - 1] === missing ? (args.pop()) : false,
37
40
  w = args.join('');
38
41
 
39
- return (ignore ? ' try {\n' : '') +
40
- '_output += _swig.compileFile(' + file + ', {' +
41
- 'resolveFrom: "' + parentFile + '"' +
42
- '})(' +
43
- ((onlyCtx && w) ? w : (!w ? '_ctx' : '_utils.extend({}, _ctx, ' + w + ')')) +
44
- ');\n' +
45
- (ignore ? '} catch (e) {}\n' : '');
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) {