driftjs-compiler 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/dist/index-cjs.js +14 -0
- package/package.json +5 -1
- package/src/generator.ts +0 -1029
- package/src/index.ts +0 -36
- package/src/lexer.ts +0 -954
- package/src/parser.ts +0 -533
- package/src/transformer.ts +0 -288
- package/tests/generator.test.ts +0 -149
- package/tests/if_conditionals.test.ts +0 -371
- package/tests/lexer.test.ts +0 -221
- package/tests/parser.test.ts +0 -302
- package/tests/transformer.test.ts +0 -110
- package/tsconfig.json +0 -8
- package/types/ast.ts +0 -88
- package/types/error.ts +0 -29
- package/types/index.ts +0 -6
- package/types/lexer-state.ts +0 -118
- package/types/opcodes.ts +0 -66
- package/types/token.ts +0 -61
- package/vite.config.ts +0 -20
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comprehensive tests for @if conditional variants in DriftJS:
|
|
3
|
-
* 1. Only-@if (no alternate branch)
|
|
4
|
-
* 2. @if / @else
|
|
5
|
-
* 3. @else if ladder (multiple @else if chains)
|
|
6
|
-
* 4. Nested @if / @else (if inside if)
|
|
7
|
-
*
|
|
8
|
-
* Tests cover Lexer tokens, Parser AST shape, and Generator bytecode output.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from 'vitest';
|
|
12
|
-
import { DriftLexer } from '../src/lexer.js';
|
|
13
|
-
import { DriftParser } from '../src/parser.js';
|
|
14
|
-
import { DriftTransformer } from '../src/transformer.js';
|
|
15
|
-
import { DriftGenerator } from '../src/generator.js';
|
|
16
|
-
import { ASTNodeType, TokenType, Opcode } from '../types/index.js';
|
|
17
|
-
|
|
18
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
function collectTokenTypes(src: string): string[] {
|
|
21
|
-
const lexer = new DriftLexer(src);
|
|
22
|
-
const types: string[] = [];
|
|
23
|
-
while (true) {
|
|
24
|
-
const tok = lexer.nextToken();
|
|
25
|
-
types.push(tok.type);
|
|
26
|
-
if (tok.type === TokenType.EOF) break;
|
|
27
|
-
}
|
|
28
|
-
return types;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function parse(src: string) {
|
|
32
|
-
return new DriftParser(new DriftLexer(src)).parse();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function compile(src: string) {
|
|
36
|
-
const ast = parse(src);
|
|
37
|
-
const transformed = new DriftTransformer(ast).transform();
|
|
38
|
-
return new DriftGenerator(transformed).generate();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── 1. Only @if ─────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
describe('Only @if (no else branch)', () => {
|
|
44
|
-
it('lexer emits DirectiveIf token with the condition as value', () => {
|
|
45
|
-
const types = collectTokenTypes('@if isVisible { <p>Hello</p> }');
|
|
46
|
-
expect(types[0]).toBe(TokenType.DirectiveIf);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('lexer captures the full condition expression as the token value', () => {
|
|
50
|
-
const lexer = new DriftLexer('@if count > 0 { <span>positive</span> }');
|
|
51
|
-
const tok = lexer.nextToken();
|
|
52
|
-
expect(tok.type).toBe(TokenType.DirectiveIf);
|
|
53
|
-
expect(tok.value).toBe('count > 0');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('parser builds an IfNode with correct test and null alternate', () => {
|
|
57
|
-
const ast = parse('@if show { <div>visible</div> }');
|
|
58
|
-
expect(ast.body).toHaveLength(1);
|
|
59
|
-
const ifNode = ast.body[0] as any;
|
|
60
|
-
expect(ifNode.type).toBe(ASTNodeType.If);
|
|
61
|
-
expect(ifNode.test).toBe('show');
|
|
62
|
-
expect(ifNode.alternate).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('parser puts child nodes inside consequent array', () => {
|
|
66
|
-
const ast = parse('@if flag { <p>text</p> }');
|
|
67
|
-
const ifNode = ast.body[0] as any;
|
|
68
|
-
const elem = ifNode.consequent.find((n: any) => n.type === ASTNodeType.Element);
|
|
69
|
-
expect(elem).toBeDefined();
|
|
70
|
-
expect(elem.tagName).toBe('p');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('parser handles a complex JS condition expression', () => {
|
|
74
|
-
const ast = parse('@if user.role === "admin" && isActive { <span>ok</span> }');
|
|
75
|
-
const ifNode = ast.body[0] as any;
|
|
76
|
-
expect(ifNode.test).toBe('user.role === "admin" && isActive');
|
|
77
|
-
expect(ifNode.alternate).toBeNull();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('generator emits REACTIVE_IF opcode for only-@if', () => {
|
|
81
|
-
const mod = compile('@if isReady { <button>Go</button> }');
|
|
82
|
-
expect(mod.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
83
|
-
// No JUMP_IF_FALSE — only-@if uses reactive sub-module encoding
|
|
84
|
-
expect(mod.bytecode).not.toContain(Opcode.JUMP_IF_FALSE);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('generator stores the condition string in the constant pool', () => {
|
|
88
|
-
const mod = compile('@if isReady { <button>Go</button> }');
|
|
89
|
-
// The condition "isReady" or its AST form must be in the constants
|
|
90
|
-
const condPresent = mod.constants.some(
|
|
91
|
-
(c) => c === 'isReady' || (typeof c === 'object' && JSON.stringify(c).includes('isReady'))
|
|
92
|
-
);
|
|
93
|
-
expect(condPresent).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('generator altIdx is 0xFF when there is no alternate branch', () => {
|
|
97
|
-
const mod = compile('@if show { <p>hi</p> }');
|
|
98
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
99
|
-
expect(ifPos).toBeGreaterThan(-1);
|
|
100
|
-
// Operand layout: REACTIVE_IF parentReg condIdx consIdx altIdx depsIdx
|
|
101
|
-
const altIdx = mod.bytecode[ifPos + 4];
|
|
102
|
-
expect(altIdx).toBe(0xFF);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('generator works for only-@if with an interpolation inside the block', () => {
|
|
106
|
-
const mod = compile('@if show { <p>{message}</p> }');
|
|
107
|
-
expect(mod.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
108
|
-
// The consequent sub-module will include INTERPOLATE_TEXT
|
|
109
|
-
const consModIdx = mod.bytecode[mod.bytecode.indexOf(Opcode.REACTIVE_IF) + 3];
|
|
110
|
-
const consMod = mod.constants[consModIdx] as any;
|
|
111
|
-
expect(consMod.bytecode).toContain(Opcode.INTERPOLATE_TEXT);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// ─── 2. @if / @else ──────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
describe('@if / @else (simple two-branch)', () => {
|
|
118
|
-
it('lexer emits DirectiveIf then DirectiveElse tokens', () => {
|
|
119
|
-
const types = collectTokenTypes('@if ok { <b>yes</b> } @else { <b>no</b> }');
|
|
120
|
-
expect(types).toContain(TokenType.DirectiveIf);
|
|
121
|
-
expect(types).toContain(TokenType.DirectiveElse);
|
|
122
|
-
expect(types.indexOf(TokenType.DirectiveIf)).toBeLessThan(
|
|
123
|
-
types.indexOf(TokenType.DirectiveElse)
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('parser builds an IfNode with an array alternate for @else', () => {
|
|
128
|
-
const ast = parse('@if loggedIn { <span>Hi</span> } @else { <span>Login</span> }');
|
|
129
|
-
const ifNode = ast.body[0] as any;
|
|
130
|
-
expect(ifNode.type).toBe(ASTNodeType.If);
|
|
131
|
-
expect(Array.isArray(ifNode.alternate)).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('parser puts the correct elements inside the alternate branch', () => {
|
|
135
|
-
const ast = parse('@if a { <div>A</div> } @else { <div>B</div> }');
|
|
136
|
-
const ifNode = ast.body[0] as any;
|
|
137
|
-
const elseElem = ifNode.alternate.find((n: any) => n.type === ASTNodeType.Element);
|
|
138
|
-
expect(elseElem).toBeDefined();
|
|
139
|
-
expect(elseElem.tagName).toBe('div');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('parser correctly sets test condition for @if', () => {
|
|
143
|
-
const ast = parse('@if x > 5 { <p>big</p> } @else { <p>small</p> }');
|
|
144
|
-
const ifNode = ast.body[0] as any;
|
|
145
|
-
expect(ifNode.test).toBe('x > 5');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('generator emits REACTIVE_IF with a valid altIdx (not 0xFF)', () => {
|
|
149
|
-
const mod = compile('@if on { <p>on</p> } @else { <p>off</p> }');
|
|
150
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
151
|
-
expect(ifPos).toBeGreaterThan(-1);
|
|
152
|
-
const altIdx = mod.bytecode[ifPos + 4];
|
|
153
|
-
expect(altIdx).not.toBe(0xFF);
|
|
154
|
-
// altIdx must point to a valid constant
|
|
155
|
-
expect(mod.constants[altIdx]).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('generator packages consequent and alternate as separate sub-modules', () => {
|
|
159
|
-
const mod = compile('@if flag { <i>A</i> } @else { <b>B</b> }');
|
|
160
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
161
|
-
const consIdx = mod.bytecode[ifPos + 3];
|
|
162
|
-
const altIdx = mod.bytecode[ifPos + 4];
|
|
163
|
-
const consMod = mod.constants[consIdx] as any;
|
|
164
|
-
const altMod = mod.constants[altIdx] as any;
|
|
165
|
-
// Both sub-modules should have their own bytecode arrays
|
|
166
|
-
expect(Array.isArray(consMod.bytecode)).toBe(true);
|
|
167
|
-
expect(Array.isArray(altMod.bytecode)).toBe(true);
|
|
168
|
-
// They should be distinct objects
|
|
169
|
-
expect(consMod).not.toBe(altMod);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('generator produces REACTIVE_IF and no JUMP_IF_FALSE for @if/@else', () => {
|
|
173
|
-
const mod = compile('@if cond { <p>yes</p> } @else { <p>no</p> }');
|
|
174
|
-
expect(mod.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
175
|
-
expect(mod.bytecode).not.toContain(Opcode.JUMP_IF_FALSE);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ─── 3. @else if ladder ──────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
describe('@else if ladder (chained conditions)', () => {
|
|
182
|
-
it('lexer emits DirectiveIf and DirectiveElseIf tokens', () => {
|
|
183
|
-
const types = collectTokenTypes(
|
|
184
|
-
'@if a { <p>a</p> } @else if b { <p>b</p> } @else { <p>c</p> }'
|
|
185
|
-
);
|
|
186
|
-
expect(types).toContain(TokenType.DirectiveIf);
|
|
187
|
-
expect(types).toContain(TokenType.DirectiveElseIf);
|
|
188
|
-
expect(types).toContain(TokenType.DirectiveElse);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('lexer captures the condition in the @else if token value', () => {
|
|
192
|
-
const lexer = new DriftLexer('@if a { <p>a</p> } @else if b > 10 { <p>b</p> }');
|
|
193
|
-
const tokens: any[] = [];
|
|
194
|
-
while (true) {
|
|
195
|
-
const tok = lexer.nextToken();
|
|
196
|
-
tokens.push(tok);
|
|
197
|
-
if (tok.type === TokenType.EOF) break;
|
|
198
|
-
}
|
|
199
|
-
const elseIfTok = tokens.find((t) => t.type === TokenType.DirectiveElseIf);
|
|
200
|
-
expect(elseIfTok).toBeDefined();
|
|
201
|
-
expect(elseIfTok.value).toBe('b > 10');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('parser nests @else if as an IfNode inside the alternate', () => {
|
|
205
|
-
const ast = parse('@if a { <p>A</p> } @else if b { <p>B</p> } @else { <p>C</p> }');
|
|
206
|
-
const ifNode = ast.body[0] as any;
|
|
207
|
-
// The alternate of the first @if should be another IfNode
|
|
208
|
-
expect(ifNode.alternate).not.toBeNull();
|
|
209
|
-
expect(ifNode.alternate.type).toBe(ASTNodeType.If);
|
|
210
|
-
expect(ifNode.alternate.test).toBe('b');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('parser chains three @else if conditions correctly', () => {
|
|
214
|
-
const src = '@if x === 1 { <p>one</p> } @else if x === 2 { <p>two</p> } @else if x === 3 { <p>three</p> } @else { <p>other</p> }';
|
|
215
|
-
const ast = parse(src);
|
|
216
|
-
const n1 = ast.body[0] as any;
|
|
217
|
-
expect(n1.test).toBe('x === 1');
|
|
218
|
-
|
|
219
|
-
const n2 = n1.alternate;
|
|
220
|
-
expect(n2.type).toBe(ASTNodeType.If);
|
|
221
|
-
expect(n2.test).toBe('x === 2');
|
|
222
|
-
|
|
223
|
-
const n3 = n2.alternate;
|
|
224
|
-
expect(n3.type).toBe(ASTNodeType.If);
|
|
225
|
-
expect(n3.test).toBe('x === 3');
|
|
226
|
-
|
|
227
|
-
// The final @else is an array alternate on n3
|
|
228
|
-
expect(Array.isArray(n3.alternate)).toBe(true);
|
|
229
|
-
const finalElem = n3.alternate.find((n: any) => n.type === ASTNodeType.Element);
|
|
230
|
-
expect(finalElem.tagName).toBe('p');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('parser handles @else if ladder without a trailing @else', () => {
|
|
234
|
-
const ast = parse('@if p { <p>p</p> } @else if q { <p>q</p> }');
|
|
235
|
-
const ifNode = ast.body[0] as any;
|
|
236
|
-
const elseIfNode = ifNode.alternate;
|
|
237
|
-
expect(elseIfNode.type).toBe(ASTNodeType.If);
|
|
238
|
-
expect(elseIfNode.test).toBe('q');
|
|
239
|
-
// No trailing @else means null alternate on the last @else if node
|
|
240
|
-
expect(elseIfNode.alternate).toBeNull();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('generator emits exactly one REACTIVE_IF for a two-branch @else if ladder', () => {
|
|
244
|
-
const mod = compile('@if a { <p>a</p> } @else if b { <p>b</p> } @else { <p>c</p> }');
|
|
245
|
-
// The outer IfNode produces one REACTIVE_IF;
|
|
246
|
-
// the nested IfNode is compiled inside the alternate sub-module (no extra top-level REACTIVE_IF)
|
|
247
|
-
const count = mod.bytecode.filter((b) => b === Opcode.REACTIVE_IF).length;
|
|
248
|
-
expect(count).toBe(1);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('generator packs @else if inside the alternate sub-module', () => {
|
|
252
|
-
const mod = compile('@if a { <p>A</p> } @else if b { <p>B</p> } @else { <p>C</p> }');
|
|
253
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
254
|
-
const altIdx = mod.bytecode[ifPos + 4];
|
|
255
|
-
const altMod = mod.constants[altIdx] as any;
|
|
256
|
-
// The alternate sub-module must itself contain a REACTIVE_IF for the @else if branch
|
|
257
|
-
expect(altMod.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// ─── 4. Nested @if / @else ───────────────────────────────────────────────────
|
|
262
|
-
|
|
263
|
-
describe('Nested @if / @else (if inside if)', () => {
|
|
264
|
-
it('parser builds doubly-nested IfNodes correctly', () => {
|
|
265
|
-
const src = `
|
|
266
|
-
@if outer {
|
|
267
|
-
@if inner { <span>inner-true</span> } @else { <span>inner-false</span> }
|
|
268
|
-
} @else {
|
|
269
|
-
<p>outer-false</p>
|
|
270
|
-
}
|
|
271
|
-
`;
|
|
272
|
-
const ast = parse(src);
|
|
273
|
-
const outerIf = ast.body.find((n: any) => n.type === ASTNodeType.If) as any;
|
|
274
|
-
expect(outerIf).toBeDefined();
|
|
275
|
-
expect(outerIf.test).toBe('outer');
|
|
276
|
-
|
|
277
|
-
const innerIf = outerIf.consequent.find((n: any) => n.type === ASTNodeType.If);
|
|
278
|
-
expect(innerIf).toBeDefined();
|
|
279
|
-
expect(innerIf.test).toBe('inner');
|
|
280
|
-
expect(Array.isArray(innerIf.alternate)).toBe(true);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('parser preserves the outer @else when the inner @if/@else is in the consequent', () => {
|
|
284
|
-
const src = `
|
|
285
|
-
@if outer {
|
|
286
|
-
@if inner { <span>yes</span> }
|
|
287
|
-
} @else {
|
|
288
|
-
<p>no</p>
|
|
289
|
-
}
|
|
290
|
-
`;
|
|
291
|
-
const ast = parse(src);
|
|
292
|
-
const outerIf = ast.body.find((n: any) => n.type === ASTNodeType.If) as any;
|
|
293
|
-
// Outer alternate should be an array (plain @else), not an IfNode
|
|
294
|
-
expect(Array.isArray(outerIf.alternate)).toBe(true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('generator wraps the inner @if into the outer consequent sub-module', () => {
|
|
298
|
-
const mod = compile('@if outer { @if inner { <b>yes</b> } } @else { <i>no</i> }');
|
|
299
|
-
// Top-level bytecode should have exactly one REACTIVE_IF
|
|
300
|
-
const topLevelCount = mod.bytecode.filter((b) => b === Opcode.REACTIVE_IF).length;
|
|
301
|
-
expect(topLevelCount).toBe(1);
|
|
302
|
-
|
|
303
|
-
// The consequent sub-module should itself contain a REACTIVE_IF for the inner @if
|
|
304
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
305
|
-
const consIdx = mod.bytecode[ifPos + 3];
|
|
306
|
-
const consMod = mod.constants[consIdx] as any;
|
|
307
|
-
expect(consMod.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('parser handles triple nesting depth', () => {
|
|
311
|
-
const src = `
|
|
312
|
-
@if a {
|
|
313
|
-
@if b {
|
|
314
|
-
@if c { <span>deep</span> }
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
`;
|
|
318
|
-
const ast = parse(src);
|
|
319
|
-
const n1 = ast.body.find((n: any) => n.type === ASTNodeType.If) as any;
|
|
320
|
-
const n2 = n1.consequent.find((n: any) => n.type === ASTNodeType.If);
|
|
321
|
-
const n3 = n2.consequent.find((n: any) => n.type === ASTNodeType.If);
|
|
322
|
-
expect(n1.test).toBe('a');
|
|
323
|
-
expect(n2.test).toBe('b');
|
|
324
|
-
expect(n3.test).toBe('c');
|
|
325
|
-
expect(n3.alternate).toBeNull();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('generator produces correct opcodes for triply-nested @if', () => {
|
|
329
|
-
const mod = compile('@if a { @if b { @if c { <span>deep</span> } } }');
|
|
330
|
-
// Top-level has exactly 1 REACTIVE_IF
|
|
331
|
-
const topCount = mod.bytecode.filter((b) => b === Opcode.REACTIVE_IF).length;
|
|
332
|
-
expect(topCount).toBe(1);
|
|
333
|
-
|
|
334
|
-
// Consequent of top contains 1 REACTIVE_IF (for @if b)
|
|
335
|
-
const ifPos = mod.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
336
|
-
const consIdx1 = mod.bytecode[ifPos + 3];
|
|
337
|
-
const consMod1 = mod.constants[consIdx1] as any;
|
|
338
|
-
expect(consMod1.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
339
|
-
|
|
340
|
-
// Consequent of second level contains 1 REACTIVE_IF (for @if c)
|
|
341
|
-
const innerIfPos = consMod1.bytecode.indexOf(Opcode.REACTIVE_IF);
|
|
342
|
-
const consIdx2 = consMod1.bytecode[innerIfPos + 3];
|
|
343
|
-
const consMod2 = consMod1.constants[consIdx2] as any;
|
|
344
|
-
expect(consMod2.bytecode).toContain(Opcode.REACTIVE_IF);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('nested @if with @else if at the outer level parses correctly', () => {
|
|
348
|
-
const src = `
|
|
349
|
-
@if role === "admin" {
|
|
350
|
-
@if hasPermission { <p>Admin+Permission</p> } @else { <p>Admin only</p> }
|
|
351
|
-
} @else if role === "user" {
|
|
352
|
-
<p>User</p>
|
|
353
|
-
} @else {
|
|
354
|
-
<p>Guest</p>
|
|
355
|
-
}
|
|
356
|
-
`;
|
|
357
|
-
const ast = parse(src);
|
|
358
|
-
const rootIf = ast.body.find((n: any) => n.type === ASTNodeType.If) as any;
|
|
359
|
-
expect(rootIf.test).toContain('admin');
|
|
360
|
-
|
|
361
|
-
// Consequent contains a nested IfNode
|
|
362
|
-
const innerIf = rootIf.consequent.find((n: any) => n.type === ASTNodeType.If);
|
|
363
|
-
expect(innerIf).toBeDefined();
|
|
364
|
-
expect(innerIf.test).toBe('hasPermission');
|
|
365
|
-
expect(Array.isArray(innerIf.alternate)).toBe(true);
|
|
366
|
-
|
|
367
|
-
// Alternate of root is an @else if, so it's an IfNode
|
|
368
|
-
expect(rootIf.alternate.type).toBe(ASTNodeType.If);
|
|
369
|
-
expect(rootIf.alternate.test).toContain('user');
|
|
370
|
-
});
|
|
371
|
-
});
|
package/tests/lexer.test.ts
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { DriftLexer } from '../src/lexer.js';
|
|
3
|
-
import {
|
|
4
|
-
Token,
|
|
5
|
-
TokenType,
|
|
6
|
-
DriftLexerError,
|
|
7
|
-
LexerStateKind,
|
|
8
|
-
} from '../types/index.js';
|
|
9
|
-
|
|
10
|
-
function collectTokens(lexer: DriftLexer): Token[] {
|
|
11
|
-
const tokens: Token[] = [];
|
|
12
|
-
|
|
13
|
-
while (true) {
|
|
14
|
-
const token = lexer.nextToken();
|
|
15
|
-
tokens.push(token);
|
|
16
|
-
if (token.type === TokenType.EOF) {
|
|
17
|
-
return tokens;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('DriftLexer', () => {
|
|
23
|
-
it('returns one token per invocation and tracks lexical state transitions', () => {
|
|
24
|
-
const lexer = new DriftLexer('<div class="hero">Hello</div>');
|
|
25
|
-
|
|
26
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.Data);
|
|
27
|
-
|
|
28
|
-
expect(lexer.nextToken().type).toBe(TokenType.TagOpen);
|
|
29
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.TagOpen);
|
|
30
|
-
|
|
31
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.Identifier, value: 'div' });
|
|
32
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.BeforeAttributeName);
|
|
33
|
-
|
|
34
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.Identifier, value: 'class' });
|
|
35
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.AfterAttributeName);
|
|
36
|
-
|
|
37
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.Equals, value: '=' });
|
|
38
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.BeforeAttributeValue);
|
|
39
|
-
|
|
40
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.StringLiteral, value: 'hero' });
|
|
41
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.BeforeAttributeName);
|
|
42
|
-
|
|
43
|
-
expect(lexer.nextToken().type).toBe(TokenType.TagClose);
|
|
44
|
-
expect(lexer.getCurrentState().kind).toBe(LexerStateKind.Data);
|
|
45
|
-
|
|
46
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.Text, value: 'Hello' });
|
|
47
|
-
expect(lexer.nextToken().type).toBe(TokenType.TagOpenSlash);
|
|
48
|
-
expect(lexer.nextToken()).toMatchObject({ type: TokenType.Identifier, value: 'div' });
|
|
49
|
-
expect(lexer.nextToken().type).toBe(TokenType.TagClose);
|
|
50
|
-
expect(lexer.nextToken().type).toBe(TokenType.EOF);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('handles empty input and whitespace-only input', () => {
|
|
54
|
-
const emptyLexer = new DriftLexer('');
|
|
55
|
-
const emptyTokens = collectTokens(emptyLexer);
|
|
56
|
-
expect(emptyTokens).toHaveLength(1);
|
|
57
|
-
expect(emptyTokens[0]?.type).toBe(TokenType.EOF);
|
|
58
|
-
|
|
59
|
-
const wsLexer = new DriftLexer(' \n\t ');
|
|
60
|
-
const wsTokens = collectTokens(wsLexer);
|
|
61
|
-
expect(wsTokens).toHaveLength(2);
|
|
62
|
-
expect(wsTokens[0]).toMatchObject({ type: TokenType.Text, value: ' \n\t ' });
|
|
63
|
-
expect(wsTokens[1]?.type).toBe(TokenType.EOF);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('lexes nested braces and strings inside interpolations correctly', () => {
|
|
67
|
-
const lexer = new DriftLexer('<div>{ { a: { b: "hello } world" } }.a.b }</div>');
|
|
68
|
-
const tokens = collectTokens(lexer);
|
|
69
|
-
|
|
70
|
-
const interpolation = tokens.find((token) => token.type === TokenType.Interpolation);
|
|
71
|
-
expect(interpolation?.value).toBe(' { a: { b: "hello } world" } }.a.b ');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('lexes template literals with backticks inside interpolations', () => {
|
|
75
|
-
const lexer = new DriftLexer('<span>{ `items: ${count}` }</span>');
|
|
76
|
-
const tokens = collectTokens(lexer);
|
|
77
|
-
|
|
78
|
-
const interpolation = tokens.find((token) => token.type === TokenType.Interpolation);
|
|
79
|
-
expect(interpolation?.value).toBe(' `items: ${count}` ');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('lexes adjacent interpolations without text in between', () => {
|
|
83
|
-
const lexer = new DriftLexer('{first}{second}');
|
|
84
|
-
const tokens = collectTokens(lexer);
|
|
85
|
-
|
|
86
|
-
expect(tokens.map((token) => token.type)).toEqual([
|
|
87
|
-
TokenType.Interpolation,
|
|
88
|
-
TokenType.Interpolation,
|
|
89
|
-
TokenType.EOF,
|
|
90
|
-
]);
|
|
91
|
-
expect(tokens[0]?.value).toBe('first');
|
|
92
|
-
expect(tokens[1]?.value).toBe('second');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('lexes comments containing tags and special characters', () => {
|
|
96
|
-
const lexer = new DriftLexer('<!-- <div class="hidden">Ignore me & my {stuff}</div> -->');
|
|
97
|
-
const tokens = collectTokens(lexer);
|
|
98
|
-
|
|
99
|
-
expect(tokens[0]).toMatchObject({
|
|
100
|
-
type: TokenType.Comment,
|
|
101
|
-
value: ' <div class="hidden">Ignore me & my {stuff}</div> ',
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('lexes attributes with hyphen and underscore identifiers', () => {
|
|
106
|
-
const lexer = new DriftLexer('<custom-button data-test-id="123" class_name="primary">Click</custom-button>');
|
|
107
|
-
const tokens = collectTokens(lexer);
|
|
108
|
-
|
|
109
|
-
const identifiers = tokens
|
|
110
|
-
.filter((token) => token.type === TokenType.Identifier)
|
|
111
|
-
.map((token) => token.value);
|
|
112
|
-
|
|
113
|
-
expect(identifiers).toEqual(['custom-button', 'data-test-id', 'class_name', 'custom-button']);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('treats script and style contents as raw text blocks', () => {
|
|
117
|
-
const lexer = new DriftLexer('<script>if (a < b) { console.log("ok"); }</script><style>.x { color: red; }</style>');
|
|
118
|
-
const tokens = collectTokens(lexer);
|
|
119
|
-
|
|
120
|
-
expect(tokens.map((token) => token.type)).toEqual([
|
|
121
|
-
TokenType.TagOpen,
|
|
122
|
-
TokenType.Identifier,
|
|
123
|
-
TokenType.TagClose,
|
|
124
|
-
TokenType.Text,
|
|
125
|
-
TokenType.TagOpenSlash,
|
|
126
|
-
TokenType.Identifier,
|
|
127
|
-
TokenType.TagClose,
|
|
128
|
-
TokenType.TagOpen,
|
|
129
|
-
TokenType.Identifier,
|
|
130
|
-
TokenType.TagClose,
|
|
131
|
-
TokenType.Text,
|
|
132
|
-
TokenType.TagOpenSlash,
|
|
133
|
-
TokenType.Identifier,
|
|
134
|
-
TokenType.TagClose,
|
|
135
|
-
TokenType.EOF,
|
|
136
|
-
]);
|
|
137
|
-
expect(tokens[3]?.value).toBe('if (a < b) { console.log("ok"); }');
|
|
138
|
-
expect(tokens[10]?.value).toBe('.x { color: red; }');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('throws on unterminated string literal in attributes', () => {
|
|
142
|
-
const lexer = new DriftLexer('<div class="unclosed></div>');
|
|
143
|
-
expect(() => collectTokens(lexer)).toThrow(DriftLexerError);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('throws on unterminated XML comments', () => {
|
|
147
|
-
const lexer = new DriftLexer('<!-- unclosed comment');
|
|
148
|
-
expect(() => collectTokens(lexer)).toThrow(DriftLexerError);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('throws on unexpected characters inside tag headers', () => {
|
|
152
|
-
const lexer = new DriftLexer('<div %invalid>');
|
|
153
|
-
expect(() => collectTokens(lexer)).toThrow(DriftLexerError);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('lexes directive headers containing braces inside quotes cleanly', () => {
|
|
157
|
-
const lexer = new DriftLexer('@if name === "{admin}" { <span>Admin</span> }');
|
|
158
|
-
const tokens = collectTokens(lexer);
|
|
159
|
-
|
|
160
|
-
expect(tokens[0]?.type).toBe(TokenType.DirectiveIf);
|
|
161
|
-
expect(tokens[0]?.value).toBe('name === "{admin}"');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('throws on unknown directive names', () => {
|
|
165
|
-
const lexer = new DriftLexer('@unknownDirective { content }');
|
|
166
|
-
expect(() => collectTokens(lexer)).toThrow(DriftLexerError);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('throws on unterminated directive header', () => {
|
|
170
|
-
const lexer = new DriftLexer('@if (a > b) <div>No block open</div>');
|
|
171
|
-
expect(() => collectTokens(lexer)).toThrow(DriftLexerError);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('lexes escaped quotes inside string literals within interpolations', () => {
|
|
175
|
-
const lexer = new DriftLexer('<span>{ "Hello \\"World\\"" }</span>');
|
|
176
|
-
const tokens = collectTokens(lexer);
|
|
177
|
-
|
|
178
|
-
const interpToken = tokens.find((t) => t.type === TokenType.Interpolation);
|
|
179
|
-
expect(interpToken?.value).toBe(' "Hello \\"World\\"" ');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('lexes JS comments with braces inside interpolations without breaking brace depth', () => {
|
|
183
|
-
const lexer = new DriftLexer('<div>{ /* comment with } brace */ value + // line comment with } brace \n 10 }</div>');
|
|
184
|
-
const tokens = collectTokens(lexer);
|
|
185
|
-
|
|
186
|
-
const interpToken = tokens.find((t) => t.type === TokenType.Interpolation);
|
|
187
|
-
expect(interpToken?.value).toBe(' /* comment with } brace */ value + // line comment with } brace \n 10 ');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('lexes template literal nested expressions inside interpolations without breaking brace depth', () => {
|
|
191
|
-
const lexer = new DriftLexer('<div>{ `hello ${ { key: "val" }.key }` }</div>');
|
|
192
|
-
const tokens = collectTokens(lexer);
|
|
193
|
-
|
|
194
|
-
const interpToken = tokens.find((t) => t.type === TokenType.Interpolation);
|
|
195
|
-
expect(interpToken?.value).toBe(' `hello ${ { key: "val" }.key }` ');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('lexes escaped quotes inside directive headers cleanly', () => {
|
|
199
|
-
const lexer = new DriftLexer('@if name === "foo{\\"bar" { <span>Escaped</span> }');
|
|
200
|
-
const tokens = collectTokens(lexer);
|
|
201
|
-
|
|
202
|
-
expect(tokens[0]?.type).toBe(TokenType.DirectiveIf);
|
|
203
|
-
expect(tokens[0]?.value).toBe('name === "foo{\\"bar"');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('lexes regular expression literals containing braces inside interpolations correctly', () => {
|
|
207
|
-
const lexer = new DriftLexer('<div>{ text.replace(/{/g, "") }</div>');
|
|
208
|
-
const tokens = collectTokens(lexer);
|
|
209
|
-
|
|
210
|
-
const interpToken = tokens.find((t) => t.type === TokenType.Interpolation);
|
|
211
|
-
expect(interpToken?.value).toBe(' text.replace(/{/g, "") ');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('lexes regular expression literals containing braces inside directive headers cleanly', () => {
|
|
215
|
-
const lexer = new DriftLexer('@if (/{/g.test(val)) { <span>Match</span> }');
|
|
216
|
-
const tokens = collectTokens(lexer);
|
|
217
|
-
|
|
218
|
-
expect(tokens[0]?.type).toBe(TokenType.DirectiveIf);
|
|
219
|
-
expect(tokens[0]?.value).toBe('(/{/g.test(val))');
|
|
220
|
-
});
|
|
221
|
-
});
|