197 lines
6.8 KiB
JavaScript
197 lines
6.8 KiB
JavaScript
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]);
|
|
}); |