@rhinostone/swig-twig 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.
@@ -0,0 +1,149 @@
1
+ /*!
2
+ * Phase 3 Session 10 — Twig `{% macro %}` tag.
3
+ *
4
+ * Twig macro syntax:
5
+ *
6
+ * {% macro name() %}…{% endmacro %}
7
+ * {% macro name(a, b, c) %}…{% endmacro %}
8
+ *
9
+ * Defines a reusable function bound to `_ctx.<name>`. Backend emits the
10
+ * full IIFE (`_utils.extend` snapshot, shadow-delete of param names from
11
+ * `_ctx`, body, restore) — see backend.js:236. Tag ships only semantic
12
+ * IR: name, params (`IRMacroParam[]`), body.
13
+ *
14
+ * Param names and the macro name are bare identifiers — dotted paths
15
+ * (`foo.bar`) and CVE-2023-25345 prototype-chain names (`__proto__`,
16
+ * `constructor`, `prototype`) are rejected at parse time. Lexer-folded
17
+ * dotted-path bail per `.claude/conventions.md § Lexer-folded-path bail`:
18
+ * single-name binding slots reject any `.` in the match before the
19
+ * `_dangerousProps` check.
20
+ *
21
+ * Twig kwargs (`{% macro foo(a=1, b="x") %}`) are deferred — Phase 4 with
22
+ * the rest of the Twig-specific surface.
23
+ */
24
+
25
+ var ir = require('@rhinostone/swig-core/lib/ir');
26
+ var utils = require('@rhinostone/swig-core/lib/utils');
27
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
28
+
29
+ var lexer = require('../lexer');
30
+ var _t = require('../tokentypes');
31
+
32
+ exports.ends = true;
33
+ exports.block = true;
34
+
35
+ /**
36
+ * Parse the `{% macro %}` tag body. Extracts the macro name and the
37
+ * optional comma-separated parameter list. Both name and params are
38
+ * validated against the bare-identifier rule and the CVE-2023-25345
39
+ * `_dangerousProps` blocklist.
40
+ *
41
+ * Accepts both shapes:
42
+ * `name` + FUNCTION/FUNCTIONEMPTY (Twig idiomatic)
43
+ * `name(a, b)` lexed as FUNCTION token whose `match` is the name
44
+ *
45
+ * Stashes `[name, {name: p1}, {name: p2}, ...]` on `token.args`. Compile
46
+ * lifts the name off the head and passes the remaining param objects
47
+ * straight to `ir.macro` — the backend handles the IIFE.
48
+ *
49
+ * @param {string} str Tag body.
50
+ * @param {number} line Source line of the opening `{%`.
51
+ * @param {object} parser The Twig parser module (unused — macro body is
52
+ * lexed locally).
53
+ * @param {object} types Twig lexer token-type enum.
54
+ * @param {Array} stack Open-tag stack (unused — parser.js manages push).
55
+ * @param {object} opts Per-call options (honors `opts.filename`).
56
+ * @param {object} swig Swig instance (unused).
57
+ * @param {object} token In-progress TagToken. `token.args` gets the
58
+ * macro name + IRMacroParam objects.
59
+ * @return {boolean} Always `true` on success. Throws otherwise.
60
+ */
61
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
62
+ var tokens = lexer.read(utils.strip(str));
63
+ var pos = 0;
64
+
65
+ function peek() {
66
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
67
+ return pos < tokens.length ? tokens[pos] : null;
68
+ }
69
+ function consume() {
70
+ var t = peek();
71
+ if (t) { pos += 1; }
72
+ return t;
73
+ }
74
+
75
+ function checkName(name, role) {
76
+ if (name.indexOf('.') !== -1) {
77
+ utils.throwError(role + ' "' + name + '" must be a bare identifier in "macro" tag', line, opts.filename);
78
+ }
79
+ if (_dangerousProps.indexOf(name) !== -1) {
80
+ utils.throwError('Unsafe ' + role.toLowerCase() + ' "' + name + '" is not allowed (CVE-2023-25345)', line, opts.filename);
81
+ }
82
+ }
83
+
84
+ var head = consume();
85
+ if (!head) {
86
+ utils.throwError('Expected macro name in "macro" tag', line, opts.filename);
87
+ }
88
+
89
+ var name;
90
+ var params = [];
91
+
92
+ if (head.type === types.FUNCTIONEMPTY) {
93
+ name = head.match;
94
+ checkName(name, 'Macro name');
95
+ } else if (head.type === types.FUNCTION) {
96
+ name = head.match;
97
+ checkName(name, 'Macro name');
98
+ var first = true;
99
+ while (true) {
100
+ var tk = peek();
101
+ if (!tk) {
102
+ utils.throwError('Unclosed parameter list in "macro" tag', line, opts.filename);
103
+ }
104
+ if (tk.type === types.PARENCLOSE) {
105
+ consume();
106
+ break;
107
+ }
108
+ if (!first) {
109
+ if (tk.type !== types.COMMA) {
110
+ utils.throwError('Expected "," between parameters in "macro" tag', line, opts.filename);
111
+ }
112
+ consume();
113
+ }
114
+ first = false;
115
+ var pTok = consume();
116
+ if (!pTok || pTok.type !== types.VAR) {
117
+ utils.throwError('Expected parameter name in "macro" tag', line, opts.filename);
118
+ }
119
+ checkName(pTok.match, 'Parameter');
120
+ params.push(ir.macroParam(pTok.match));
121
+ }
122
+ } else if (head.type === types.VAR) {
123
+ name = head.match;
124
+ checkName(name, 'Macro name');
125
+ } else {
126
+ utils.throwError('Expected macro name in "macro" tag', line, opts.filename);
127
+ }
128
+
129
+ if (peek()) {
130
+ utils.throwError('Unexpected token "' + peek().match + '" after macro signature in "macro" tag', line, opts.filename);
131
+ }
132
+
133
+ token.args = [name].concat(params);
134
+ return true;
135
+ };
136
+
137
+ /**
138
+ * Emit an IRMacro node. Backend's `Macro` branch owns the `_ctx.<name>
139
+ * = function(...) { … }` IIFE + `_utils.extend` snapshot + shadow-delete
140
+ * of param names from `_ctx`.
141
+ *
142
+ * @return {object} IRMacro node.
143
+ */
144
+ exports.compile = function (compiler, args, content, parents, options, blockName) {
145
+ var name = args[0];
146
+ var params = args.slice(1);
147
+ var bodyJS = compiler(content, parents, options, blockName);
148
+ return ir.macro(name, params, [ir.legacyJS(bodyJS)]);
149
+ };
@@ -0,0 +1,174 @@
1
+ /*!
2
+ * Phase 3 Session 7 — Twig `{% set %}` tag.
3
+ * Phase 3 Session 11 — extended with body-capture form.
4
+ *
5
+ * Twig `set` has two forms:
6
+ *
7
+ * Inline: {% set <lhs> <op> <rhs> %}
8
+ * Body: {% set <lhs> %}…{% endset %}
9
+ *
10
+ * lhs — a bare identifier or a pure-dot path (`foo`, `foo.bar.baz`).
11
+ * Bracket LHS (`foo[bar]`) is rejected at parse time — the
12
+ * bracket-lvalue contract is a cross-flavor design call and
13
+ * is deferred.
14
+ * op — any valid JS assignment operator (`=`, `+=`, `-=`, `*=`, `/=`).
15
+ * Inline form only.
16
+ * rhs — any Twig expression; parsed via `parser.parseExpr`.
17
+ * Inline form only.
18
+ *
19
+ * Inline form emits an IRSet node on `token.irExpr`:
20
+ *
21
+ * ir.set(ir.varRef(['foo', 'bar']), '=', <IRExpr value>)
22
+ *
23
+ * Body form captures the rendered content as a string via an IIFE and
24
+ * assigns it to the target. No dedicated IR factory — emits an
25
+ * IRLegacyJS fragment because the capture is a JS plumbing shape
26
+ * (IIFE over `_output`) that doesn't need its own IR surface.
27
+ *
28
+ * Static `exports.ends = true` is the default so the token's `ends`
29
+ * slot starts truthy — the body form keeps it, the inline form flips
30
+ * `token.ends = false` at parse time so the splitter does NOT push
31
+ * the inline-form token onto the open-tag stack.
32
+ *
33
+ * CVE-2023-25345 checkpoints apply twice — here on the LHS path
34
+ * segments, and again in the backend `checkDangerousSegment` walk —
35
+ * per the duplication invariant in .claude/security.md § _dangerousProps
36
+ * is duplicated across layers.
37
+ */
38
+
39
+ var ir = require('@rhinostone/swig-core/lib/ir');
40
+ var utils = require('@rhinostone/swig-core/lib/utils');
41
+ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
42
+
43
+ var lexer = require('../lexer');
44
+ var _t = require('../tokentypes');
45
+
46
+ exports.ends = true;
47
+ exports.block = true;
48
+
49
+ /**
50
+ * Parse the `{% set %}` tag body and attach the appropriate IR to the
51
+ * token. Inline form sets `token.irExpr` and flips `token.ends = false`;
52
+ * body form leaves `token.ends = true` and stashes the target path on
53
+ * `token.args` for the compile step to pick up.
54
+ *
55
+ * @param {string} str Tag body (everything between `{%` and `%}`, tag name stripped).
56
+ * @param {number} line Source line of the opening `{%`.
57
+ * @param {object} parser The Twig parser module (exposes `parseExpr`).
58
+ * @param {object} types Twig lexer token-type enum.
59
+ * @param {Array} stack Open-tag stack (unused — parser.js manages the push).
60
+ * @param {object} opts Per-call options (honors `opts.filename` for filename-aware throws).
61
+ * @param {object} swig Swig instance (unused).
62
+ * @param {object} token In-progress TagToken. Body form: `token.args` gets the path;
63
+ * `token.ends` stays true. Inline form: `token.irExpr` gets
64
+ * the IRSet; `token.ends` flips to false.
65
+ * @return {boolean} Always `true` on success. Throws otherwise.
66
+ */
67
+ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
68
+ var tokens = lexer.read(utils.strip(str));
69
+ var pos = 0;
70
+
71
+ function peek() {
72
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
73
+ return pos < tokens.length ? tokens[pos] : null;
74
+ }
75
+ function consume() {
76
+ var t = peek();
77
+ if (t) { pos += 1; }
78
+ return t;
79
+ }
80
+
81
+ var lhsTok = consume();
82
+ if (!lhsTok || lhsTok.type !== types.VAR) {
83
+ utils.throwError('Expected variable name in "set" tag', line, opts.filename);
84
+ }
85
+
86
+ var path = lhsTok.match.split('.');
87
+ utils.each(path, function (segment) {
88
+ if (_dangerousProps.indexOf(segment) !== -1) {
89
+ utils.throwError('Unsafe assignment to "' + segment + '" is not allowed (CVE-2023-25345)', line, opts.filename);
90
+ }
91
+ });
92
+
93
+ // DOTKEY tail — the Twig lexer already folds dotted paths into the
94
+ // VAR match, but a defensive DOTKEY consumer here keeps this tag
95
+ // robust if a future lexer tightening splits `foo.bar` into VAR + DOTKEY.
96
+ while (peek() && peek().type === types.DOTKEY) {
97
+ var dk = consume();
98
+ if (_dangerousProps.indexOf(dk.match) !== -1) {
99
+ utils.throwError('Unsafe assignment to "' + dk.match + '" is not allowed (CVE-2023-25345)', line, opts.filename);
100
+ }
101
+ path.push(dk.match);
102
+ }
103
+
104
+ var next = peek();
105
+ if (next && next.type === types.BRACKETOPEN) {
106
+ utils.throwError('Bracket-notation assignment is not supported in "set" (use dot-path notation)', line, opts.filename);
107
+ }
108
+
109
+ if (!next) {
110
+ // Body-capture form — no more tokens after the LHS. Keep
111
+ // `token.ends = true` (the default from `exports.ends`) so the
112
+ // splitter pushes the token onto the open-tag stack and waits
113
+ // for a matching `{% endset %}`. Stash the path on `token.args`
114
+ // for the compile handler to consume.
115
+ token.args = path;
116
+ return true;
117
+ }
118
+
119
+ // Inline form — flip `token.ends = false` so the splitter does NOT
120
+ // push the token onto the open-tag stack.
121
+ token.ends = false;
122
+
123
+ var opTok = consume();
124
+ if (opTok.type !== types.ASSIGNMENT) {
125
+ utils.throwError('Expected assignment operator in "set" tag', line, opts.filename);
126
+ }
127
+
128
+ // Skip leading whitespace between `=` and the RHS expression so the
129
+ // RHS slice starts at the first meaningful token. parseExpr tolerates
130
+ // leading whitespace internally, but trimming here keeps the slice
131
+ // shape predictable for future callers.
132
+ while (pos < tokens.length && tokens[pos].type === types.WHITESPACE) { pos += 1; }
133
+
134
+ var rhsTokens = tokens.slice(pos);
135
+ if (!rhsTokens.length) {
136
+ utils.throwError('Expected expression after assignment in "set" tag', line, opts.filename);
137
+ }
138
+
139
+ var value = parser.parseExpr(rhsTokens);
140
+ token.irExpr = ir.set(ir.varRef(path), opTok.match, value);
141
+ return true;
142
+ };
143
+
144
+ /**
145
+ * Emit the IR for either the inline-assignment or body-capture form.
146
+ * Inline form returns the pre-built IRSet via `token.irExpr`. Body form
147
+ * compiles the captured content and wraps it in an IIFE assigned to
148
+ * the target.
149
+ *
150
+ * @param {Function} compiler Backend walker (recurses into `content`).
151
+ * @param {Array} args Target path segments (body form only).
152
+ * @param {Array} content Child tokens captured between `{% set %}`
153
+ * and `{% endset %}` (body form only).
154
+ * @param {Array} parents Parent template chain (passed through).
155
+ * @param {object} options Compile options (passed through).
156
+ * @param {?string} blockName Enclosing block name (passed through).
157
+ * @param {object} token The tag token. `token.irExpr` is set
158
+ * for inline form only.
159
+ * @return {object} IRSet (inline) or IRLegacyJS (body).
160
+ */
161
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
162
+ if (token.irExpr) {
163
+ return token.irExpr;
164
+ }
165
+ var path = args;
166
+ var bodyJS = compiler(content, parents, options, blockName);
167
+ return ir.legacyJS(
168
+ '_ctx.' + path.join('.') + ' = (function () {\n' +
169
+ ' var _output = "";\n' +
170
+ bodyJS +
171
+ ' return _output;\n' +
172
+ '})();\n'
173
+ );
174
+ };
@@ -0,0 +1,52 @@
1
+ /*!
2
+ * Phase 3 Session 11 — Twig `{% verbatim %}…{% endverbatim %}` tag.
3
+ *
4
+ * Preserves arbitrary template-like content as literal output. Inside
5
+ * a verbatim block, `{{ … }}`, `{% … %}` (other than `{% endverbatim %}`),
6
+ * and `{# … #}` are NOT parsed — the splitter in `parser.js` flips an
7
+ * `inVerbatim` flag that bypasses the variable/tag/comment branches and
8
+ * wraps each chunk as `ir.text`, so the content array handed to this
9
+ * tag's compile is already a list of IRText nodes.
10
+ *
11
+ * Takes no arguments. Extra tokens after `verbatim` are rejected at
12
+ * parse time with a filename-aware throw.
13
+ */
14
+
15
+ var utils = require('@rhinostone/swig-core/lib/utils');
16
+
17
+ /**
18
+ * Reject any tokens after the `verbatim` keyword. The splitter owns
19
+ * all content-capture behaviour via its `inVerbatim` flag, so this
20
+ * handler only has to validate the tag's own argument list (which
21
+ * must be empty).
22
+ *
23
+ * @param {string} str Tag body (everything after `verbatim`).
24
+ * @param {number} line Source line of the opening `{%`.
25
+ * @param {object} parser The Twig parser module (unused).
26
+ * @param {object} types Twig lexer token-type enum (unused).
27
+ * @param {Array} stack Open-tag stack (parser.js manages the push).
28
+ * @param {object} opts Per-call options (honors `opts.filename`).
29
+ * @return {boolean} Always `true` on success. Throws otherwise.
30
+ */
31
+ exports.parse = function (str, line, parser, types, stack, opts) {
32
+ var stripped = utils.strip(str || '');
33
+ if (stripped.length > 0) {
34
+ utils.throwError('Unexpected token "' + stripped + '" after "verbatim"', line, opts.filename);
35
+ }
36
+ return true;
37
+ };
38
+
39
+ /**
40
+ * Return the captured content array unchanged. Each item is already
41
+ * an IRText node (or another pre-built IR node that the backend will
42
+ * splice through), so the backend's emit loop can iterate and emit
43
+ * without any further wrapping.
44
+ *
45
+ * @return {Array} Content node list.
46
+ */
47
+ exports.compile = function (compiler, args, content) {
48
+ return content;
49
+ };
50
+
51
+ exports.ends = true;
52
+ exports.block = false;
@@ -0,0 +1,133 @@
1
+ /*!
2
+ * Phase 3 Session 12 — Twig `{% with %}` tag.
3
+ *
4
+ * Twig scoped-context region:
5
+ *
6
+ * {% with %}…{% endwith %} (shallow copy of _ctx)
7
+ * {% with <ctx> %}…{% endwith %} (merge ctx into _ctx)
8
+ * {% with <ctx> only %}…{% endwith %} (isolated, ctx is context)
9
+ * {% with only %}…{% endwith %} (isolated, empty context)
10
+ *
11
+ * The context expression (when present) is lowered through
12
+ * `parser.parseExpr`, so object literals, variable references,
13
+ * conditionals, function calls — any Twig expression — all route
14
+ * through the same path.
15
+ *
16
+ * The `only` keyword is recognised as a bare VAR token at top-level
17
+ * paren/bracket/curly/function depth. Depth tracking prevents a nested
18
+ * `only` inside the context expression (unlikely but possible) from
19
+ * being mistaken for the keyword.
20
+ *
21
+ * The tag emits an `IRWith` node. The backend's `With` branch
22
+ * (packages/swig-core/lib/backend.js) owns the IIFE scaffolding that
23
+ * shadows `_ctx` for the body's lexical scope while letting `_output`
24
+ * stay in the outer scope via closure capture.
25
+ */
26
+
27
+ var ir = require('@rhinostone/swig-core/lib/ir');
28
+ var utils = require('@rhinostone/swig-core/lib/utils');
29
+
30
+ var lexer = require('../lexer');
31
+ var _t = require('../tokentypes');
32
+
33
+ exports.ends = true;
34
+ exports.block = false;
35
+
36
+ /**
37
+ * Parse the `{% with %}` tag body. Extracts the optional context
38
+ * expression and the optional `only` keyword marker, lowers the
39
+ * context slice through `parser.parseExpr`, and stashes the result on
40
+ * `token.irExpr` along with the `isolated` flag on `token.args`.
41
+ *
42
+ * @param {string} str Tag body.
43
+ * @param {number} line Source line of the opening `{%`.
44
+ * @param {object} parser The Twig parser module (exposes `parseExpr`).
45
+ * @param {object} types Twig 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
+
55
+ var depth = 0;
56
+ var onlyIdx = -1;
57
+ var i, tk;
58
+
59
+ for (i = 0; i < tokens.length; i += 1) {
60
+ tk = tokens[i];
61
+ if (tk.type === types.PARENOPEN || tk.type === types.BRACKETOPEN ||
62
+ tk.type === types.CURLYOPEN || tk.type === types.FUNCTION) {
63
+ depth += 1;
64
+ continue;
65
+ }
66
+ if (tk.type === types.PARENCLOSE || tk.type === types.BRACKETCLOSE ||
67
+ tk.type === types.CURLYCLOSE) {
68
+ depth -= 1;
69
+ continue;
70
+ }
71
+ if (depth !== 0) { continue; }
72
+ if (tk.type !== types.VAR) { continue; }
73
+
74
+ if (tk.match === 'only' && onlyIdx === -1) {
75
+ onlyIdx = i;
76
+ }
77
+ }
78
+
79
+ var ctxEnd = (onlyIdx !== -1) ? onlyIdx : tokens.length;
80
+ var ctxTokens = sliceTrim(tokens, 0, ctxEnd, types);
81
+
82
+ var ctxExpr;
83
+ if (ctxTokens.length) {
84
+ ctxExpr = parser.parseExpr(ctxTokens);
85
+ }
86
+
87
+ // Trailing tokens after `only` are not allowed — `{% with ctx only extra %}`
88
+ // is ambiguous (is `extra` a second context slot? a stray keyword?).
89
+ if (onlyIdx !== -1) {
90
+ var tail = sliceTrim(tokens, onlyIdx + 1, tokens.length, types);
91
+ if (tail.length) {
92
+ utils.throwError('Unexpected tokens after "only" in "with" tag', line, opts.filename);
93
+ }
94
+ }
95
+
96
+ token.args = [!!(onlyIdx !== -1)];
97
+ token.irExpr = ctxExpr;
98
+ return true;
99
+ };
100
+
101
+ /**
102
+ * Strip WHITESPACE tokens from both ends of a slice range, returning a
103
+ * plain array. Parser.parseExpr skips whitespace in the interior, but
104
+ * leading/trailing whitespace produces a zero-length effective slice
105
+ * that parseExpr cannot classify; the explicit trim keeps the empty-
106
+ * context detection (`ctxTokens.length === 0`) honest.
107
+ *
108
+ * @param {object[]} tokens Token stream.
109
+ * @param {number} start Inclusive start index.
110
+ * @param {number} end Exclusive end index.
111
+ * @param {object} types Twig lexer token-type enum.
112
+ * @return {object[]} Trimmed slice.
113
+ * @private
114
+ */
115
+ function sliceTrim(tokens, start, end, types) {
116
+ while (start < end && tokens[start].type === types.WHITESPACE) { start += 1; }
117
+ while (end > start && tokens[end - 1].type === types.WHITESPACE) { end -= 1; }
118
+ return tokens.slice(start, end);
119
+ }
120
+
121
+ /**
122
+ * Emit an IRWith node carrying the optional context IRExpr, the
123
+ * `isolated` flag, and the recursively-compiled body wrapped in
124
+ * IRLegacyJS. The backend's `With` branch owns the IIFE-shadow of
125
+ * `_ctx` for the body's lexical scope.
126
+ *
127
+ * @return {object} IRWith node.
128
+ */
129
+ exports.compile = function (compiler, args, content, parents, options, blockName, token) {
130
+ var isolated = !!args[0];
131
+ var bodyJS = compiler(content, parents, options, blockName);
132
+ return ir.withStmt(token.irExpr, isolated, [ir.legacyJS(bodyJS)]);
133
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Twig lexer token type enum — the contract between the Twig lexer and
3
+ * the Twig parser in @rhinostone/swig-twig.
4
+ *
5
+ * Numeric IDs in the shared range (0–25, 100) mirror
6
+ * @rhinostone/swig-core/lib/tokentypes by design: Twig and native Swig
7
+ * lower to the same swig-core IR, and aligning the IDs keeps shared
8
+ * consumers (e.g. backend.compile splice-through paths, CVE-2023-25345
9
+ * `_dangerousProps` enforcement) flavor-agnostic. The Twig parser is
10
+ * its own module — it does not inherit from swig-core's TokenParser —
11
+ * but the cognitive overhead of re-mapping IDs across flavors is not
12
+ * worth the freedom.
13
+ *
14
+ * Twig-only IDs (30–37) are reserved here so Session 3 can add lexer
15
+ * rules without renumbering. Keeping the layout stable up front avoids
16
+ * silent ID collisions across in-flight flavor work.
17
+ *
18
+ * See .claude/architecture/multi-flavor-ir.md § Phase 3 for the
19
+ * per-flavor split decision.
20
+ *
21
+ * @readonly
22
+ * @enum {number}
23
+ */
24
+ module.exports = {
25
+ /** Whitespace */
26
+ WHITESPACE: 0,
27
+ /** Plain string literal */
28
+ STRING: 1,
29
+ /** Variable filter call with arguments — `|name(...)` */
30
+ FILTER: 2,
31
+ /** Variable filter call with no arguments — `|name` */
32
+ FILTEREMPTY: 3,
33
+ /** Function call with arguments — `name(...)` */
34
+ FUNCTION: 4,
35
+ /** Function call with no arguments — `name()` */
36
+ FUNCTIONEMPTY: 5,
37
+ /** Open parenthesis */
38
+ PARENOPEN: 6,
39
+ /** Close parenthesis */
40
+ PARENCLOSE: 7,
41
+ /** Comma */
42
+ COMMA: 8,
43
+ /** Variable identifier */
44
+ VAR: 9,
45
+ /** Numeric literal */
46
+ NUMBER: 10,
47
+ /** Math operator (+, -, *, /, %) */
48
+ OPERATOR: 11,
49
+ /** Open square bracket */
50
+ BRACKETOPEN: 12,
51
+ /** Close square bracket */
52
+ BRACKETCLOSE: 13,
53
+ /** Dot-key accessor — `.key` */
54
+ DOTKEY: 14,
55
+ /** Open square bracket at the start of an array literal */
56
+ ARRAYOPEN: 15,
57
+ /** Open curly brace */
58
+ CURLYOPEN: 17,
59
+ /** Close curly brace */
60
+ CURLYCLOSE: 18,
61
+ /** Colon — object literal key/value separator */
62
+ COLON: 19,
63
+ /** JavaScript-valid comparator (==, !=, <=, etc.) */
64
+ COMPARATOR: 20,
65
+ /** Boolean logic (`and`, `or`, `&&`, `||`) */
66
+ LOGIC: 21,
67
+ /** Boolean negation (`not`, `!`) */
68
+ NOT: 22,
69
+ /** Boolean literal (`true`, `false`) */
70
+ BOOL: 23,
71
+ /** Variable assignment (`=`, `+=`, `-=`, `*=`, `/=`) */
72
+ ASSIGNMENT: 24,
73
+ /** Method call open — internal */
74
+ METHODOPEN: 25,
75
+
76
+ /* ---- Twig-only token IDs (reserved; rules land in Session 3+) ---- */
77
+
78
+ /** Twig string-concatenation operator — `~` */
79
+ TILDE: 30,
80
+ /** Twig range operator — `..` */
81
+ RANGE: 31,
82
+ /** Twig test operator — `is` */
83
+ IS: 32,
84
+ /** Twig negated test operator — `is not` */
85
+ ISNOT: 33,
86
+ /** Twig shorthand ternary — `?:` */
87
+ QMARK: 34,
88
+ /** Twig null-coalescing operator — `??` */
89
+ NULLCOALESCE: 35,
90
+ /** Twig string-interpolation open — `#{` inside double-quoted strings */
91
+ INTERP_OPEN: 36,
92
+ /** Twig string-interpolation close — `}` matching `#{` */
93
+ INTERP_CLOSE: 37,
94
+
95
+ /** Unknown token */
96
+ UNKNOWN: 100
97
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@rhinostone/swig-twig",
3
+ "version": "2.0.0-alpha.3",
4
+ "description": "Twig frontend for the @rhinostone/swig-core template engine. Phase 3 of the multi-flavor architecture (see @rhinostone/swig #T16).",
5
+ "keywords": [
6
+ "template",
7
+ "templating",
8
+ "twig",
9
+ "swig",
10
+ "swig-twig",
11
+ "frontend"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/gina-io/swig.git",
16
+ "directory": "packages/swig-twig"
17
+ },
18
+ "author": "Rhinostone <contact@gina.io>",
19
+ "license": "MIT",
20
+ "main": "lib/index.js",
21
+ "engines": {
22
+ "node": ">=12"
23
+ },
24
+ "peerDependencies": {
25
+ "@rhinostone/swig-core": "2.0.0-alpha.3"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }