harmonyc 0.14.1 → 0.16.0
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/model/model.js +72 -3
- package/package.json +1 -1
- package/parser/lexer_rules.js +11 -1
- package/parser/parser.js +16 -8
package/model/model.js
CHANGED
|
@@ -81,6 +81,9 @@ export class Branch {
|
|
|
81
81
|
this.parent = undefined;
|
|
82
82
|
return this;
|
|
83
83
|
}
|
|
84
|
+
switch(_i) {
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
export class Step extends Branch {
|
|
86
89
|
constructor(action, responses = [], children, isFork = false) {
|
|
@@ -115,6 +118,9 @@ export class Step extends Branch {
|
|
|
115
118
|
get isEmpty() {
|
|
116
119
|
return this.phrases.every((phrase) => phrase.isEmpty);
|
|
117
120
|
}
|
|
121
|
+
switch(i) {
|
|
122
|
+
return new Step(this.action.switch(i), this.responses.map((r) => r.switch(i)));
|
|
123
|
+
}
|
|
118
124
|
}
|
|
119
125
|
export class State {
|
|
120
126
|
constructor(text = '') {
|
|
@@ -167,6 +173,44 @@ export class Word extends Part {
|
|
|
167
173
|
return this.text;
|
|
168
174
|
}
|
|
169
175
|
}
|
|
176
|
+
export class Repeater extends Part {
|
|
177
|
+
constructor(choices) {
|
|
178
|
+
super();
|
|
179
|
+
this.choices = choices;
|
|
180
|
+
}
|
|
181
|
+
toString() {
|
|
182
|
+
return `{${this.choices.map((ps) => ps.join(' ')).join(' & ')}}`;
|
|
183
|
+
}
|
|
184
|
+
toSingleLineString() {
|
|
185
|
+
return `{${this.choices
|
|
186
|
+
.map((ps) => ps.map((p) => p.toSingleLineString()).join(' '))
|
|
187
|
+
.join(' & ')}}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export class Switch extends Part {
|
|
191
|
+
constructor(choices) {
|
|
192
|
+
super();
|
|
193
|
+
this.choices = choices;
|
|
194
|
+
}
|
|
195
|
+
toString() {
|
|
196
|
+
return `{ ${this.choices.join(' / ')} }`;
|
|
197
|
+
}
|
|
198
|
+
toSingleLineString() {
|
|
199
|
+
return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' / ')} }`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export class Router extends Part {
|
|
203
|
+
constructor(choices) {
|
|
204
|
+
super();
|
|
205
|
+
this.choices = choices;
|
|
206
|
+
}
|
|
207
|
+
toString() {
|
|
208
|
+
return `{ ${this.choices.join(' ; ')} }`;
|
|
209
|
+
}
|
|
210
|
+
toSingleLineString() {
|
|
211
|
+
return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' ; ')} }`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
170
214
|
export class Arg extends Part {
|
|
171
215
|
}
|
|
172
216
|
export class StringLiteral extends Arg {
|
|
@@ -243,6 +287,9 @@ export class Phrase {
|
|
|
243
287
|
toSingleLineString() {
|
|
244
288
|
return this.parts.map((p) => p.toSingleLineString()).join(' ');
|
|
245
289
|
}
|
|
290
|
+
switch(i) {
|
|
291
|
+
return new this.constructor(this.parts.map((p) => (p instanceof Switch ? p.choices[i] : p)));
|
|
292
|
+
}
|
|
246
293
|
}
|
|
247
294
|
export class Action extends Phrase {
|
|
248
295
|
constructor() {
|
|
@@ -306,6 +353,9 @@ export class SaveToVariable extends Part {
|
|
|
306
353
|
toString() {
|
|
307
354
|
return `\${${this.variableName}}`;
|
|
308
355
|
}
|
|
356
|
+
get words() {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
309
359
|
}
|
|
310
360
|
export class Precondition extends Branch {
|
|
311
361
|
constructor(state = '') {
|
|
@@ -323,7 +373,7 @@ export function makeTests(root) {
|
|
|
323
373
|
let ic = routers.getIncompleteCount();
|
|
324
374
|
let newIc;
|
|
325
375
|
do {
|
|
326
|
-
const newTest = new Test(
|
|
376
|
+
const newTest = new Test(routers.nextWalk());
|
|
327
377
|
newIc = routers.getIncompleteCount();
|
|
328
378
|
if (newIc < ic)
|
|
329
379
|
tests.push(newTest);
|
|
@@ -340,12 +390,28 @@ export function makeTests(root) {
|
|
|
340
390
|
walk(root);
|
|
341
391
|
tests = tests.filter((t) => t.steps.length > 0);
|
|
342
392
|
tests.sort((a, b) => branchIndex.get(a.last) - branchIndex.get(b.last));
|
|
393
|
+
resolveSwitches(tests);
|
|
343
394
|
tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
|
|
344
395
|
return tests;
|
|
345
396
|
}
|
|
397
|
+
function resolveSwitches(tests) {
|
|
398
|
+
for (let i = 0; i < tests.length; ++i) {
|
|
399
|
+
const test = tests[i];
|
|
400
|
+
const phrases = test.steps.flatMap((s) => s.phrases);
|
|
401
|
+
const switches = phrases.flatMap((p) => p.parts.filter((p) => p instanceof Switch));
|
|
402
|
+
if (switches.length === 0)
|
|
403
|
+
continue;
|
|
404
|
+
const count = switches[0].choices.length;
|
|
405
|
+
if (switches.some((s) => s.choices.length !== count)) {
|
|
406
|
+
throw new Error(`all switches in a test case must have the same number of choices: ${test.name} has ${switches.map((s) => s.choices.length)} choices`);
|
|
407
|
+
}
|
|
408
|
+
const newTests = switches[0].choices.map((_, j) => test.switch(j));
|
|
409
|
+
tests.splice(i, 1, ...newTests);
|
|
410
|
+
i += count - 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
346
413
|
export class Test {
|
|
347
|
-
constructor(
|
|
348
|
-
this.root = root;
|
|
414
|
+
constructor(branches) {
|
|
349
415
|
this.branches = branches;
|
|
350
416
|
this.branches = this.branches.filter((b) => !b.isEmpty);
|
|
351
417
|
this.labels = this.branches
|
|
@@ -370,6 +436,9 @@ export class Test {
|
|
|
370
436
|
.map((s) => ` - ${s.headToString()}`)
|
|
371
437
|
.join('\n')}`;
|
|
372
438
|
}
|
|
439
|
+
switch(j) {
|
|
440
|
+
return new Test(this.branches.map((b) => b.switch(j)));
|
|
441
|
+
}
|
|
373
442
|
}
|
|
374
443
|
export function makeGroups(tests) {
|
|
375
444
|
if (tests.length === 0)
|
package/package.json
CHANGED
package/parser/lexer_rules.js
CHANGED
|
@@ -12,6 +12,11 @@ export var T;
|
|
|
12
12
|
T["Space"] = "space";
|
|
13
13
|
T["OpeningBracket"] = "[";
|
|
14
14
|
T["ClosingBracket"] = "]";
|
|
15
|
+
T["OpeningBrace"] = "{";
|
|
16
|
+
T["ClosingBrace"] = "}";
|
|
17
|
+
T["And"] = "&";
|
|
18
|
+
T["Slash"] = "/";
|
|
19
|
+
T["Semicolon"] = ";";
|
|
15
20
|
T["DoubleQuoteString"] = "double-quote string";
|
|
16
21
|
T["UnclosedDoubleQuoteString"] = "unclosed double-quote string";
|
|
17
22
|
T["BacktickString"] = "backtick string";
|
|
@@ -39,6 +44,11 @@ const rules = [
|
|
|
39
44
|
[true, /:(?=\s*(?:\n|$))/y, T.Colon],
|
|
40
45
|
[true, /\[/y, T.OpeningBracket],
|
|
41
46
|
[true, /\]/y, T.ClosingBracket],
|
|
47
|
+
[true, /\{/y, T.OpeningBrace],
|
|
48
|
+
[true, /\}/y, T.ClosingBrace],
|
|
49
|
+
[true, /\//y, T.Slash],
|
|
50
|
+
[true, /&/y, T.And],
|
|
51
|
+
[true, /;/y, T.Semicolon],
|
|
42
52
|
[true, /!!/y, T.ErrorMark],
|
|
43
53
|
[true, /=>/y, T.ResponseArrow],
|
|
44
54
|
[
|
|
@@ -59,6 +69,6 @@ const rules = [
|
|
|
59
69
|
[true, /\$\{[^}\n]*/y, T.UnclosedVariable],
|
|
60
70
|
[true, /\|(?: .*|(?=\n|$))/y, T.MultilineString],
|
|
61
71
|
[true, /\|[^ \n]/y, T.InvalidMultilineStringMark],
|
|
62
|
-
[true, /.+?(?=[\[\]"`|#]
|
|
72
|
+
[true, /.+?(?=[\[\]"`|#{}&/;]|\$\{|$|=>|!!|:\s*$)/my, T.Words],
|
|
63
73
|
];
|
|
64
74
|
export default rules;
|
package/parser/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { alt_sc, apply, expectEOF, expectSingleResult, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, nil, unableToConsumeToken, } from 'typescript-parsec';
|
|
2
2
|
import { T, lexer } from "./lexer.js";
|
|
3
|
-
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, } from "../model/model.js";
|
|
3
|
+
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, Switch, } from "../model/model.js";
|
|
4
4
|
export function parse(input, production = TEST_DESIGN) {
|
|
5
5
|
const tokens = lexer.parse(input);
|
|
6
6
|
return expectSingleResult(expectEOF(production.parse(tokens)));
|
|
@@ -26,7 +26,14 @@ function anythingBut(kind) {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)),
|
|
29
|
+
export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)), SIMPLE_PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING),
|
|
30
|
+
//REPEATER = apply(
|
|
31
|
+
//list_sc(rep_sc(SIMPLE_PART), tok(T.And)),
|
|
32
|
+
//(cs) => new Repeater(cs)
|
|
33
|
+
//),
|
|
34
|
+
SWITCH = apply(list_sc(SIMPLE_PART, tok(T.Slash)), (cs) => new Switch(cs)),
|
|
35
|
+
//ROUTER = apply(list_sc(REPEATER, tok(T.Semicolon)), (cs) => new Router(cs)),
|
|
36
|
+
BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts) => new Action(parts))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable]) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words) => new Label(words.map((w) => w.toString()).join(' '))), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = alt_sc(SECTION, STEP), // section first, to make sure there is no colon after step
|
|
30
37
|
DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
|
|
31
38
|
return {
|
|
32
39
|
dent: (seqOrFork.text.length - 2) / 2,
|
|
@@ -36,15 +43,19 @@ DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
|
|
|
36
43
|
dent,
|
|
37
44
|
branch: branch.setFork(isFork),
|
|
38
45
|
})), ANYTHING_BUT_NEWLINE = anythingBut(T.Newline), TEXT = apply(seq(tok(T.Words), rep_sc(ANYTHING_BUT_NEWLINE)), () => undefined), LINE = alt_sc(NODE, TEXT), TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(opt_sc(list_sc(apply(LINE, (line, [start, end]) => ({ line, start, end })), NEWLINES)), (lines) => {
|
|
39
|
-
|
|
40
|
-
let dent = startDent;
|
|
46
|
+
let dent;
|
|
41
47
|
const root = new Section(new Label(''));
|
|
42
48
|
let parent = root;
|
|
43
49
|
for (const { line, start } of lines !== null && lines !== void 0 ? lines : []) {
|
|
44
50
|
if (line === undefined)
|
|
45
51
|
continue;
|
|
46
52
|
const { dent: d, branch } = line;
|
|
47
|
-
if (
|
|
53
|
+
if (dent === undefined) {
|
|
54
|
+
if (d !== 0)
|
|
55
|
+
throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}: first step must not be indented`);
|
|
56
|
+
dent = 0;
|
|
57
|
+
}
|
|
58
|
+
else if (Math.round(d) !== d) {
|
|
48
59
|
throw new Error(`invalid odd indent of ${d * 2} at line ${start.pos.rowBegin}`);
|
|
49
60
|
}
|
|
50
61
|
else if (d > dent + 1) {
|
|
@@ -54,9 +65,6 @@ DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
|
|
|
54
65
|
parent = parent.children[parent.children.length - 1];
|
|
55
66
|
++dent;
|
|
56
67
|
}
|
|
57
|
-
else if (d < startDent) {
|
|
58
|
-
throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}`);
|
|
59
|
-
}
|
|
60
68
|
else
|
|
61
69
|
while (d < dent) {
|
|
62
70
|
parent = parent.parent;
|