const assert = require('node:assert/strict'); const test = require('node:test'); const { EftlParser } = require('../out/parser'); const { EftlTokenizer } = require('../out/tokenizer'); function parse(source) { const { tokens, errors: tokenizerErrors } = new EftlTokenizer(source).tokenize(); assert.deepEqual(tokenizerErrors, []); return new EftlParser(tokens).parse().errors; } function messages(source) { return parse(source).map(error => error.message); } test('accepts a correctly closed VAR tag', () => { assert.deepEqual(parse('[EFTL][VAR][/VAR][/EFTL]'), []); }); test('accepts sibling VAR tags', () => { assert.deepEqual(parse('[EFTL][VAR][/VAR][VAR][/VAR][/EFTL]'), []); }); test('reports a VAR nested directly inside another VAR', () => { assert.deepEqual(parse('[EFTL][VAR][VAR][/VAR][/VAR][/EFTL]'), [{ message: 'VAR tags cannot be nested inside another VAR tag', line: 1, column: 12, length: 5 }]); }); test('reports a VAR nested indirectly inside another VAR', () => { assert.deepEqual(parse('[EFTL][VAR][SPLIT][VAR][/VAR][/SPLIT][/VAR][/EFTL]'), [{ message: 'VAR tags cannot be nested inside another VAR tag', line: 1, column: 19, length: 5 }]); }); test('reports a VAR tag without its closing tag', () => { assert.ok(messages('[EFTL][VAR]').includes('Unclosed tag: [VAR] (expected [/VAR])')); }); test('reports a mismatched closing tag', () => { const errors = parse('[EFTL][/VAR][/EFTL]'); assert.equal(errors.length, 1); assert.equal(errors[0].message, 'Mismatched closing tag: expected [/EFTL], got [/VAR]'); }); test('reports every unclosed nested tag at its opening token', () => { assert.deepEqual(parse('[EFTL]\n[VAR]'), [ { message: 'Unclosed tag: [EFTL] (expected [/EFTL])', line: 1, column: 1, length: 6 }, { message: 'Unclosed tag: [VAR] (expected [/VAR])', line: 2, column: 1, length: 5 } ]); }); test('requires exactly one EFTL root block', () => { assert.deepEqual(parse(''), [{ message: 'Missing required [EFTL] root block', line: 1, column: 1, length: 0 }]); assert.deepEqual(parse('[EFTL][/EFTL]\n[EFTL][/EFTL]'), [{ message: 'Only one [EFTL] root block is allowed', line: 2, column: 1, length: 6 }]); }); test('allows comments and multiline whitespace around the EFTL root', () => { assert.deepEqual(parse(' \n[!-- before --]\n[EFTL][/EFTL]\n[!-- after --]\n '), []); }); test('rejects instructions outside the EFTL root', () => { assert.deepEqual(messages('[VAR][/VAR][EFTL][/EFTL]'), [ 'Only comments and whitespace are allowed outside the [EFTL] root block', 'Only comments and whitespace are allowed outside the [EFTL] root block' ]); }); test('rejects a nested EFTL root', () => { assert.deepEqual(parse('[EFTL][EFTL][/EFTL][/EFTL]'), [{ message: 'EFTL root blocks cannot be nested', line: 1, column: 7, length: 6 }]); }); test('accepts CONDITION, THEN, repeated ELSE IF, and one final ELSE in an IF', () => { const source = [ '[EFTL][IF]', ' [CONDITION][% true %][/CONDITION]', ' [THEN][/THEN]', ' [ELSE IF][CONDITION][% false %][/CONDITION][THEN][/THEN][/ELSE IF]', ' [ELSE IF][CONDITION][% false %][/CONDITION][THEN][/THEN][/ELSE IF]', ' [ELSE][/ELSE]', '[/IF][/EFTL]' ].join('\n'); assert.deepEqual(parse(source), []); }); test('allows an IF nested inside a THEN block', () => { const source = '[EFTL][IF][CONDITION][/CONDITION][THEN]' + '[IF][CONDITION][/CONDITION][THEN][/THEN][/IF]' + '[/THEN][/IF][/EFTL]'; assert.deepEqual(parse(source), []); }); test('rejects an IF nested directly inside another IF', () => { const source = '[EFTL][IF]' + '[IF][CONDITION][/CONDITION][THEN][/THEN][/IF]' + '[CONDITION][/CONDITION][THEN][/THEN][/IF][/EFTL]'; assert.deepEqual(messages(source), [ 'IF cannot be nested directly inside another IF; place it inside a branch block' ]); }); test('requires CONDITION followed by THEN as the first direct IF children', () => { assert.deepEqual(messages('[EFTL][IF][/IF][/EFTL]'), [ 'Expected [CONDITION] immediately after [IF]' ]); assert.deepEqual(messages('[EFTL][IF][THEN][/THEN][CONDITION][/CONDITION][THEN][/THEN][/IF][/EFTL]'), [ 'Expected [CONDITION] as the next direct child of [IF], got [THEN]' ]); assert.deepEqual(messages('[EFTL][IF][CONDITION][/CONDITION][/IF][/EFTL]'), [ 'Expected [THEN] immediately after [/CONDITION]' ]); }); test('allows only whitespace between direct IF child blocks', () => { const source = '[EFTL][IF][CONDITION][/CONDITION]' + '[VAR][/VAR][!-- comment --]text' + '[THEN][/THEN][/IF][/EFTL]'; assert.deepEqual(messages(source), [ 'Expected [THEN] as the next direct child of [IF], got [VAR]', 'Expected [THEN] as the next direct child of [IF], got [!--', 'Expected [THEN] as the next direct child of [IF], got text' ]); }); test('allows ELSE IF only before the optional ELSE', () => { const source = '[EFTL][IF][CONDITION][/CONDITION][THEN][/THEN]' + '[ELSE][/ELSE][ELSE IF][/ELSE IF][/IF][/EFTL]'; assert.deepEqual(messages(source), [ 'Expected [/IF] as the next direct child of [IF], got [ELSE IF]' ]); assert.deepEqual(messages( '[EFTL][IF][CONDITION][/CONDITION][THEN][/THEN][ELSE][/ELSE][ELSE][/ELSE][/IF][/EFTL]' ), [ 'Expected [/IF] as the next direct child of [IF], got [ELSE]' ]); }); test('accepts WHILE with an optional string threshold attribute', () => { assert.deepEqual(parse('[EFTL][WHILE][/WHILE][/EFTL]'), []); assert.deepEqual(parse('[EFTL][WHILE threshold="99"][/WHILE][/EFTL]'), []); const { tokens } = new EftlTokenizer('[WHILE threshold="99"][/WHILE]').tokenize(); assert.equal(tokens[0].type, 'WHILE_OPEN'); assert.equal(tokens[0].value, '[WHILE threshold="99"]'); }); test('rejects invalid WHILE attributes', () => { const expectedMessage = 'WHILE accepts only the optional string attribute threshold="..."'; assert.deepEqual(messages('[EFTL][WHILE threshold=99][/WHILE][/EFTL]'), [expectedMessage]); assert.deepEqual(messages('[EFTL][WHILE limit="99"][/WHILE][/EFTL]'), [expectedMessage]); assert.deepEqual(messages( '[EFTL][WHILE threshold="10" threshold="20"][/WHILE][/EFTL]' ), [expectedMessage]); });