harmonyc 0.10.5 → 0.11.1
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/README.md +51 -2
- package/code_generator/{JavaScript.js → VitestGenerator.js} +26 -11
- package/code_generator/outFile.js +3 -1
- package/compiler/compile.js +2 -2
- package/model/model.js +72 -35
- package/package.json +1 -1
- package/parser/lexer.js +121 -2
- package/parser/lexer_rules.js +28 -26
- package/parser/parser.js +12 -45
package/README.md
CHANGED
|
@@ -64,11 +64,31 @@ Both actions and responses get compiled to simple function calls - in JavaScript
|
|
|
64
64
|
|
|
65
65
|
### Arguments
|
|
66
66
|
|
|
67
|
-
Phrases can have arguments which are passed to the implementation function. There are two types of arguments:
|
|
67
|
+
Phrases (actions and responses) can have arguments which are passed to the implementation function. There are two types of arguments: strings and code fragments:
|
|
68
|
+
|
|
69
|
+
```harmony
|
|
70
|
+
+ strings:
|
|
71
|
+
+ hello "John"
|
|
72
|
+
+ code fragment:
|
|
73
|
+
+ greet `3` times
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
becomes
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
test('T1 - strings', async () => {
|
|
80
|
+
const P = new Phrases();
|
|
81
|
+
await P.When_hello_("John");
|
|
82
|
+
})
|
|
83
|
+
test('T2 - code fragment', async () => {
|
|
84
|
+
const P = new Phrases();
|
|
85
|
+
await P.When_greet__times(3);
|
|
86
|
+
})
|
|
87
|
+
```
|
|
68
88
|
|
|
69
89
|
### Labels
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
Labels are lines that start with `-` or `+` and end with `:`. You can use them to structure your test design.
|
|
72
92
|
They are not included in the test case, but the test case name is generated from the labels.
|
|
73
93
|
|
|
74
94
|
### Comments
|
|
@@ -79,6 +99,35 @@ Lines starting with `#` or `//` are comments and are ignored.
|
|
|
79
99
|
|
|
80
100
|
You can use `!!` to denote an error response. This will verify that the action throws an error. You can specify the error message after the `!!`.
|
|
81
101
|
|
|
102
|
+
### Variables
|
|
103
|
+
|
|
104
|
+
You can set variables in the tests and use them in strings and code fragments:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
+ set variable:
|
|
108
|
+
+ ${name} "John"
|
|
109
|
+
+ greet "${name}" => "hello John"
|
|
110
|
+
+ store result into variable:
|
|
111
|
+
+ run process => ${result}
|
|
112
|
+
+ "${result}" is "success"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
becomes
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
test('T1 - set variable', (context) => {
|
|
119
|
+
const P = new Phrases();
|
|
120
|
+
(context.task.meta.variables ??= {})['name'] = "John";
|
|
121
|
+
await P.When_greet_(context.task.meta.variables?.['name']);
|
|
122
|
+
})
|
|
123
|
+
test('T2 - store result in variable', (context) => {
|
|
124
|
+
const P = new Phrases();
|
|
125
|
+
const r = await P.When_run_process();
|
|
126
|
+
(context.task.meta.variables ??= {})['result'] = r;
|
|
127
|
+
await P.Then__is_(`${context.task.meta.variables?.['result']});
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
82
131
|
## Running the tests
|
|
83
132
|
|
|
84
133
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from 'path';
|
|
2
2
|
import { Arg, Word, } from "../model/model.js";
|
|
3
|
-
export class
|
|
3
|
+
export class VitestGenerator {
|
|
4
4
|
constructor(tf, sf) {
|
|
5
5
|
this.tf = tf;
|
|
6
6
|
this.sf = sf;
|
|
@@ -59,24 +59,28 @@ export class NodeTest {
|
|
|
59
59
|
});
|
|
60
60
|
this.tf.print('});');
|
|
61
61
|
}
|
|
62
|
-
errorStep(action,
|
|
62
|
+
errorStep(action, errorResponse) {
|
|
63
63
|
var _a;
|
|
64
64
|
this.declareFeatureVariables([action]);
|
|
65
|
-
this.tf.print(`context.task.meta.phrases.push(${str(
|
|
66
|
-
this.tf.print(`context.task.meta.phrases.push(${str(errorMessage ? '!! => ' + JSON.stringify(errorMessage.text) : '!!')});`);
|
|
65
|
+
this.tf.print(`context.task.meta.phrases.push(${str(errorResponse.toSingleLineString())});`);
|
|
67
66
|
this.tf.print(`await expect(async () => {`);
|
|
68
67
|
this.tf.indent(() => {
|
|
69
68
|
action.toCode(this);
|
|
70
69
|
});
|
|
71
|
-
this.tf.print(`}).rejects.toThrow(${(_a =
|
|
70
|
+
this.tf.print(`}).rejects.toThrow(${(_a = errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.toCode(this)) !== null && _a !== void 0 ? _a : ''});`);
|
|
72
71
|
}
|
|
73
72
|
step(action, responses) {
|
|
74
73
|
this.declareFeatureVariables([action, ...responses]);
|
|
75
|
-
this.tf.print(`context.task.meta.phrases.push(${str(action.toString())});`);
|
|
76
74
|
if (responses.length === 0) {
|
|
77
75
|
action.toCode(this);
|
|
78
76
|
return;
|
|
79
77
|
}
|
|
78
|
+
if (action.isEmpty) {
|
|
79
|
+
for (const response of responses) {
|
|
80
|
+
response.toCode(this);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
80
84
|
const res = `r${this.resultCount++ || ''}`;
|
|
81
85
|
this.tf.print(`const ${res} =`);
|
|
82
86
|
this.tf.indent(() => {
|
|
@@ -84,7 +88,6 @@ export class NodeTest {
|
|
|
84
88
|
try {
|
|
85
89
|
this.extraArgs = [res];
|
|
86
90
|
for (const response of responses) {
|
|
87
|
-
this.tf.print(`context.task.meta.phrases.push(${str(`=> ${response.toString()}`)});`);
|
|
88
91
|
response.toCode(this);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
@@ -110,13 +113,25 @@ export class NodeTest {
|
|
|
110
113
|
const f = this.featureVars.get(p.feature.name);
|
|
111
114
|
const args = p.args.map((a) => a.toCode(this));
|
|
112
115
|
args.push(...this.extraArgs);
|
|
113
|
-
this.tf.print(`
|
|
116
|
+
this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`);
|
|
117
|
+
this.tf.print(`await ${f}.${functionName(p)}(${args.join(', ')}));`);
|
|
118
|
+
}
|
|
119
|
+
setVariable(action) {
|
|
120
|
+
this.tf.print(`(context.task.meta.variables ??= {})[${str(action.variableName)}] = ${action.value.toCode(this)};`);
|
|
114
121
|
}
|
|
115
|
-
|
|
122
|
+
saveToVariable(s) {
|
|
123
|
+
if (this.extraArgs.length !== 1)
|
|
124
|
+
return;
|
|
125
|
+
this.tf.print(`(context.task.meta.variables ??= {})[${str(s.variableName)}] = ${this.extraArgs[0]};`);
|
|
126
|
+
}
|
|
127
|
+
stringLiteral(text, { withVariables }) {
|
|
128
|
+
if (withVariables && text.match(/\$\{/)) {
|
|
129
|
+
return templateStr(text).replace(/\\\$\{([^\s}]+)\}/g, (_, x) => `\${context.task.meta.variables?.[${str(x)}]}`);
|
|
130
|
+
}
|
|
116
131
|
return str(text);
|
|
117
132
|
}
|
|
118
133
|
codeLiteral(src) {
|
|
119
|
-
return src;
|
|
134
|
+
return src.replace(/\$\{([^\s}]+)\}/g, (_, x) => `context.task.meta.variables?.[${str(x)}]`);
|
|
120
135
|
}
|
|
121
136
|
paramName(index) {
|
|
122
137
|
return 'xyz'.charAt(index) || `a${index + 1}`;
|
|
@@ -171,7 +186,7 @@ function abbrev(s) {
|
|
|
171
186
|
export function functionName(phrase) {
|
|
172
187
|
const { kind } = phrase;
|
|
173
188
|
return ((kind === 'response' ? 'Then_' : 'When_') +
|
|
174
|
-
(
|
|
189
|
+
(phrase.parts
|
|
175
190
|
.flatMap((c) => c instanceof Word
|
|
176
191
|
? words(c.text).filter((x) => x)
|
|
177
192
|
: c instanceof Arg
|
|
@@ -42,7 +42,9 @@ export class OutFile {
|
|
|
42
42
|
get value() {
|
|
43
43
|
let res = this.lines.join('\n');
|
|
44
44
|
if (this.currentLoc) {
|
|
45
|
-
res +=
|
|
45
|
+
res +=
|
|
46
|
+
`\n\n//# sour` + // not for this file ;)
|
|
47
|
+
`ceMappingURL=data:application/json,${encodeURIComponent(this.sm.toString())}`;
|
|
46
48
|
}
|
|
47
49
|
return res;
|
|
48
50
|
}
|
package/compiler/compile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
2
2
|
import { OutFile } from "../code_generator/outFile.js";
|
|
3
3
|
import { parse } from "../parser/parser.js";
|
|
4
4
|
import { base, phrasesFileName, testFileName } from "../filenames/filenames.js";
|
|
@@ -26,7 +26,7 @@ export function compileFeature(fileName, src) {
|
|
|
26
26
|
const testFile = new OutFile(testFn);
|
|
27
27
|
const phrasesFn = phrasesFileName(fileName);
|
|
28
28
|
const phrasesFile = new OutFile(phrasesFn);
|
|
29
|
-
const cg = new
|
|
29
|
+
const cg = new VitestGenerator(testFile, phrasesFile);
|
|
30
30
|
feature.toCode(cg);
|
|
31
31
|
return { outFile: testFile, phrasesFile };
|
|
32
32
|
}
|
package/model/model.js
CHANGED
|
@@ -94,7 +94,7 @@ export class Step extends Branch {
|
|
|
94
94
|
}
|
|
95
95
|
toCode(cg) {
|
|
96
96
|
if (this.responses[0] instanceof ErrorResponse) {
|
|
97
|
-
cg.errorStep(this.action, this.responses[0]
|
|
97
|
+
cg.errorStep(this.action, this.responses[0]);
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
100
|
cg.step(this.action, this.responses);
|
|
@@ -107,7 +107,7 @@ export class Step extends Branch {
|
|
|
107
107
|
return super.setFeature(feature);
|
|
108
108
|
}
|
|
109
109
|
headToString() {
|
|
110
|
-
return
|
|
110
|
+
return this.phrases.join(' ');
|
|
111
111
|
}
|
|
112
112
|
toString() {
|
|
113
113
|
return this.headToString() + indent(super.toString());
|
|
@@ -145,6 +145,18 @@ export class Section extends Branch {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
export class Part {
|
|
148
|
+
toSingleLineString() {
|
|
149
|
+
return this.toString();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export class DummyKeyword extends Part {
|
|
153
|
+
constructor(text = '') {
|
|
154
|
+
super();
|
|
155
|
+
this.text = text;
|
|
156
|
+
}
|
|
157
|
+
toString() {
|
|
158
|
+
return this.text;
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
161
|
export class Word extends Part {
|
|
150
162
|
constructor(text = '') {
|
|
@@ -165,13 +177,30 @@ export class StringLiteral extends Arg {
|
|
|
165
177
|
toString() {
|
|
166
178
|
return JSON.stringify(this.text);
|
|
167
179
|
}
|
|
180
|
+
toSingleLineString() {
|
|
181
|
+
return this.toString();
|
|
182
|
+
}
|
|
168
183
|
toCode(cg) {
|
|
169
|
-
return cg.stringLiteral(this.text);
|
|
184
|
+
return cg.stringLiteral(this.text, { withVariables: true });
|
|
170
185
|
}
|
|
171
186
|
toDeclaration(cg, index) {
|
|
172
187
|
return cg.stringParamDeclaration(index);
|
|
173
188
|
}
|
|
174
189
|
}
|
|
190
|
+
export class Docstring extends StringLiteral {
|
|
191
|
+
toCode(cg) {
|
|
192
|
+
return cg.stringLiteral(this.text, { withVariables: false });
|
|
193
|
+
}
|
|
194
|
+
toString() {
|
|
195
|
+
return this.text
|
|
196
|
+
.split('\n')
|
|
197
|
+
.map((l) => '| ' + l)
|
|
198
|
+
.join('\n');
|
|
199
|
+
}
|
|
200
|
+
toSingleLineString() {
|
|
201
|
+
return super.toString();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
175
204
|
export class CodeLiteral extends Arg {
|
|
176
205
|
constructor(src = '') {
|
|
177
206
|
super();
|
|
@@ -188,37 +217,31 @@ export class CodeLiteral extends Arg {
|
|
|
188
217
|
}
|
|
189
218
|
}
|
|
190
219
|
export class Phrase {
|
|
191
|
-
constructor(
|
|
192
|
-
this.
|
|
193
|
-
this.docstring =
|
|
194
|
-
docstring === undefined ? undefined : new StringLiteral(docstring);
|
|
220
|
+
constructor(parts) {
|
|
221
|
+
this.parts = parts;
|
|
195
222
|
}
|
|
196
223
|
setFeature(feature) {
|
|
197
224
|
this.feature = feature;
|
|
225
|
+
return this;
|
|
198
226
|
}
|
|
199
227
|
get keyword() {
|
|
200
228
|
return this.kind === 'action' ? 'When' : 'Then';
|
|
201
229
|
}
|
|
202
230
|
get args() {
|
|
203
|
-
return
|
|
231
|
+
return this.parts.filter((c) => c instanceof Arg);
|
|
204
232
|
}
|
|
205
233
|
get isEmpty() {
|
|
206
|
-
return this.
|
|
234
|
+
return this.parts.length === 0;
|
|
207
235
|
}
|
|
208
236
|
toString() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
? this.docstring.text.split('\n').map((l) => '| ' + l)
|
|
215
|
-
: []),
|
|
216
|
-
].join('\n');
|
|
237
|
+
const parts = this.parts.map((p) => p.toString());
|
|
238
|
+
const isMultiline = parts.map((p) => p.includes('\n'));
|
|
239
|
+
return parts
|
|
240
|
+
.map((p, i) => i === 0 ? p : isMultiline[i - 1] || isMultiline[i] ? '\n' + p : ' ' + p)
|
|
241
|
+
.join('');
|
|
217
242
|
}
|
|
218
243
|
toSingleLineString() {
|
|
219
|
-
return
|
|
220
|
-
.map((c) => c.toString())
|
|
221
|
-
.join(' ');
|
|
244
|
+
return this.parts.map((p) => p.toSingleLineString()).join(' ');
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
export class Action extends Phrase {
|
|
@@ -227,7 +250,7 @@ export class Action extends Phrase {
|
|
|
227
250
|
this.kind = 'action';
|
|
228
251
|
}
|
|
229
252
|
toCode(cg) {
|
|
230
|
-
if (
|
|
253
|
+
if (this.isEmpty)
|
|
231
254
|
return;
|
|
232
255
|
cg.phrase(this);
|
|
233
256
|
}
|
|
@@ -237,27 +260,41 @@ export class Response extends Phrase {
|
|
|
237
260
|
super(...arguments);
|
|
238
261
|
this.kind = 'response';
|
|
239
262
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (this.content.length === 2 &&
|
|
246
|
-
this.content[0] instanceof Word &&
|
|
247
|
-
this.content[0].text === '!!' &&
|
|
248
|
-
this.content[1] instanceof StringLiteral)
|
|
249
|
-
return true;
|
|
263
|
+
toString() {
|
|
264
|
+
return `=> ${super.toString()}`;
|
|
265
|
+
}
|
|
266
|
+
toSingleLineString() {
|
|
267
|
+
return `=> ${super.toSingleLineString()}`;
|
|
250
268
|
}
|
|
251
269
|
toCode(cg) {
|
|
252
|
-
if (
|
|
270
|
+
if (this.isEmpty)
|
|
253
271
|
return;
|
|
254
272
|
cg.phrase(this);
|
|
255
273
|
}
|
|
256
274
|
}
|
|
257
275
|
export class ErrorResponse extends Response {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
276
|
+
constructor(message) {
|
|
277
|
+
super(message ? [new DummyKeyword('!!'), message] : [new DummyKeyword('!!')]);
|
|
278
|
+
this.message = message;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export class SetVariable extends Action {
|
|
282
|
+
constructor(variableName, value) {
|
|
283
|
+
super([new DummyKeyword(`\${${variableName}}`), value]);
|
|
284
|
+
this.variableName = variableName;
|
|
285
|
+
this.value = value;
|
|
286
|
+
}
|
|
287
|
+
toCode(cg) {
|
|
288
|
+
cg.setVariable(this);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export class SaveToVariable extends Response {
|
|
292
|
+
constructor(variableName) {
|
|
293
|
+
super([new DummyKeyword(`\${${variableName}}`)]);
|
|
294
|
+
this.variableName = variableName;
|
|
295
|
+
}
|
|
296
|
+
toCode(cg) {
|
|
297
|
+
cg.saveToVariable(this);
|
|
261
298
|
}
|
|
262
299
|
}
|
|
263
300
|
export class Precondition extends Branch {
|
package/package.json
CHANGED
package/parser/lexer.js
CHANGED
|
@@ -1,4 +1,123 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TokenError } from 'typescript-parsec';
|
|
2
2
|
import rules from './lexer_rules.js';
|
|
3
3
|
export { T } from './lexer_rules.js';
|
|
4
|
-
|
|
4
|
+
// based on https://github.com/microsoft/ts-parsec/blob/3350fcb/packages/ts-parsec/src/Lexer.ts
|
|
5
|
+
/*
|
|
6
|
+
MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) Microsoft Corporation.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE
|
|
27
|
+
*/
|
|
28
|
+
class TokenImpl {
|
|
29
|
+
constructor(lexer, input, kind, text, pos, keep) {
|
|
30
|
+
this.lexer = lexer;
|
|
31
|
+
this.input = input;
|
|
32
|
+
this.kind = kind;
|
|
33
|
+
this.text = text;
|
|
34
|
+
this.pos = pos;
|
|
35
|
+
this.keep = keep;
|
|
36
|
+
}
|
|
37
|
+
get next() {
|
|
38
|
+
if (this.nextToken === undefined) {
|
|
39
|
+
this.nextToken = this.lexer.parseNextAvailable(this.input, this.pos.index + this.text.length, this.pos.rowEnd, this.pos.columnEnd);
|
|
40
|
+
if (this.nextToken === undefined) {
|
|
41
|
+
this.nextToken = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return this.nextToken === null ? undefined : this.nextToken;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
class LexerImpl {
|
|
48
|
+
constructor(rules) {
|
|
49
|
+
this.rules = rules;
|
|
50
|
+
for (const rule of this.rules) {
|
|
51
|
+
if (!rule[1].sticky) {
|
|
52
|
+
throw new Error(`Regular expression patterns for a tokenizer should be sticky: ${rule[1].source}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
parse(input) {
|
|
57
|
+
return this.parseNextAvailable(input, 0, 1, 1);
|
|
58
|
+
}
|
|
59
|
+
parseNext(input, indexStart, rowBegin, columnBegin) {
|
|
60
|
+
if (indexStart === input.length) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
64
|
+
const lastIndex = indexStart;
|
|
65
|
+
// let result: TokenImpl<T> | undefined
|
|
66
|
+
for (const [keep, regexp, kind] of this.rules) {
|
|
67
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
68
|
+
regexp.lastIndex = lastIndex;
|
|
69
|
+
if (regexp.test(input)) {
|
|
70
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
71
|
+
const text = input.slice(lastIndex, regexp.lastIndex);
|
|
72
|
+
let rowEnd = rowBegin;
|
|
73
|
+
let columnEnd = columnBegin;
|
|
74
|
+
for (const c of text) {
|
|
75
|
+
switch (c) {
|
|
76
|
+
case '\r':
|
|
77
|
+
break;
|
|
78
|
+
case '\n':
|
|
79
|
+
rowEnd++;
|
|
80
|
+
columnEnd = 1;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
columnEnd++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const newResult = new TokenImpl(this, input, kind, text, { index: indexStart, rowBegin, columnBegin, rowEnd, columnEnd }, keep);
|
|
87
|
+
// changed here: instead of keeping the longest token, we keep the first one
|
|
88
|
+
return newResult;
|
|
89
|
+
// if (
|
|
90
|
+
// result === undefined ||
|
|
91
|
+
// result.text.length < newResult.text.length
|
|
92
|
+
// ) {
|
|
93
|
+
// result = newResult
|
|
94
|
+
// }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// changed here: instead of keeping the longest token, we keep the first one
|
|
98
|
+
// if (result === undefined) {
|
|
99
|
+
throw new TokenError({
|
|
100
|
+
index: indexStart,
|
|
101
|
+
rowBegin,
|
|
102
|
+
columnBegin,
|
|
103
|
+
rowEnd: rowBegin,
|
|
104
|
+
columnEnd: columnBegin,
|
|
105
|
+
}, `Unable to tokenize the rest of the input: ${input.substr(indexStart)}`);
|
|
106
|
+
// } else {
|
|
107
|
+
// return result
|
|
108
|
+
// }
|
|
109
|
+
}
|
|
110
|
+
parseNextAvailable(input, index, rowBegin, columnBegin) {
|
|
111
|
+
let token;
|
|
112
|
+
while (true) {
|
|
113
|
+
token = this.parseNext(input, token === undefined ? index : token.pos.index + token.text.length, token === undefined ? rowBegin : token.pos.rowEnd, token === undefined ? columnBegin : token.pos.columnEnd);
|
|
114
|
+
if (token === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
else if (token.keep) {
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export const lexer = new LexerImpl(rules);
|
package/parser/lexer_rules.js
CHANGED
|
@@ -21,42 +21,44 @@ export var T;
|
|
|
21
21
|
T["InvalidTab"] = "invalid tab";
|
|
22
22
|
T["MultilineString"] = "multiline string";
|
|
23
23
|
T["InvalidMultilineStringMark"] = "invalid multiline string mark";
|
|
24
|
+
T["Variable"] = "variable";
|
|
25
|
+
T["InvalidEmptyVariable"] = "invalid empty variable";
|
|
26
|
+
T["UnclosedVariable"] = "unclosed variable";
|
|
24
27
|
})(T || (T = {}));
|
|
25
28
|
const rules = [
|
|
26
29
|
// false = ignore token
|
|
27
|
-
// if multiple patterns match, the
|
|
28
|
-
// patterns must
|
|
29
|
-
[true,
|
|
30
|
-
[true,
|
|
31
|
-
[true,
|
|
32
|
-
[true, /^ /
|
|
33
|
-
[
|
|
34
|
-
[
|
|
30
|
+
// if multiple patterns match, the former wins
|
|
31
|
+
// patterns must be y (sticky)
|
|
32
|
+
[true, /\n/y, T.Newline],
|
|
33
|
+
[true, /\t/y, T.InvalidTab],
|
|
34
|
+
[true, /[\x00-\x1f]/y, T.InvalidWhitespace],
|
|
35
|
+
[true, /^( )*[+]( |$)/my, T.Plus],
|
|
36
|
+
[true, /^( )*[-]( |$)/my, T.Minus],
|
|
37
|
+
[false, / /y, T.Space],
|
|
38
|
+
[false, /(#|\/\/).*?(?=\n|$)/y, T.Comment],
|
|
39
|
+
[true, /:(?=\s*(?:\n|$))/y, T.Colon],
|
|
40
|
+
[true, /\[/y, T.OpeningBracket],
|
|
41
|
+
[true, /\]/y, T.ClosingBracket],
|
|
42
|
+
[true, /!!/y, T.ErrorMark],
|
|
43
|
+
[true, /=>/y, T.ResponseArrow],
|
|
35
44
|
[
|
|
36
45
|
true,
|
|
37
|
-
|
|
38
|
-
T.Words,
|
|
39
|
-
],
|
|
40
|
-
[true, /^-/g, T.Minus],
|
|
41
|
-
[true, /^\+/g, T.Plus],
|
|
42
|
-
[true, /^\[/g, T.OpeningBracket],
|
|
43
|
-
[true, /^\]/g, T.ClosingBracket],
|
|
44
|
-
[true, /^!!/g, T.ErrorMark],
|
|
45
|
-
[true, /^=>/g, T.ResponseArrow],
|
|
46
|
-
[
|
|
47
|
-
true,
|
|
48
|
-
/^"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/g,
|
|
46
|
+
/"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/y,
|
|
49
47
|
T.DoubleQuoteString,
|
|
50
48
|
],
|
|
51
49
|
[
|
|
52
50
|
true,
|
|
53
|
-
|
|
51
|
+
/"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*/y,
|
|
54
52
|
T.UnclosedDoubleQuoteString,
|
|
55
53
|
],
|
|
56
|
-
[true,
|
|
57
|
-
[true,
|
|
58
|
-
[true,
|
|
59
|
-
[true,
|
|
60
|
-
[true,
|
|
54
|
+
[true, /``/y, T.InvalidEmptyBacktickString],
|
|
55
|
+
[true, /`[^`]+`/y, T.BacktickString],
|
|
56
|
+
[true, /`[^`]*/y, T.UnclosedBacktickString],
|
|
57
|
+
[true, /\$\{[^}\n]+\}/y, T.Variable],
|
|
58
|
+
[true, /\$\{\}/y, T.InvalidEmptyVariable],
|
|
59
|
+
[true, /\$\{[^}\n]*/y, T.UnclosedVariable],
|
|
60
|
+
[true, /\|(?: .*|(?=\n|$))/y, T.MultilineString],
|
|
61
|
+
[true, /\|[^ \n]/y, T.InvalidMultilineStringMark],
|
|
62
|
+
[true, /.+?(?=[\[\]"`|#]|\/\/|\$\{|$|=>|!!|:\s*$)/my, T.Words],
|
|
61
63
|
];
|
|
62
64
|
export default rules;
|
package/parser/parser.js
CHANGED
|
@@ -1,49 +1,25 @@
|
|
|
1
|
-
import { alt_sc, apply, expectEOF, expectSingleResult, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, } from 'typescript-parsec';
|
|
1
|
+
import { alt_sc, apply, expectEOF, expectSingleResult, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, nil, } from 'typescript-parsec';
|
|
2
2
|
import { T, lexer } from "./lexer.js";
|
|
3
|
-
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Word, Label, ErrorResponse, } from "../model/model.js";
|
|
3
|
+
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, } 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)));
|
|
7
7
|
}
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const DOCSTRING = apply(list_sc(tok(T.MultilineString), seq(tok(T.Newline), S)), (lines) => lines.map(({ text }) => text.slice(2)).join('\n'));
|
|
16
|
-
export const PART = alt_sc(WORDS, ERROR_MARK, BULLET_POINT_LIKE_WORD, DOUBLE_QUOTE_STRING, BACKTICK_STRING);
|
|
17
|
-
export const PHRASE = seq(opt_sc(list_sc(PART, S)), opt_sc(kright(opt_sc(NEWLINES), kright(S, DOCSTRING))));
|
|
18
|
-
export const ACTION = apply(PHRASE, ([parts, docstring]) => new Action(parts, docstring));
|
|
19
|
-
export const RESPONSE = apply(PHRASE, ([parts, docstring]) => {
|
|
20
|
-
if ((parts === null || parts === void 0 ? void 0 : parts[0]) instanceof Word && parts[0].text === '!!') {
|
|
21
|
-
return new ErrorResponse(parts.slice(1), docstring);
|
|
22
|
-
}
|
|
23
|
-
return new Response(parts, docstring);
|
|
24
|
-
});
|
|
25
|
-
export const ARROW = kmid(seq(opt_sc(NEWLINES), S), tok(T.ResponseArrow), S);
|
|
26
|
-
export const RESPONSE_ITEM = kright(ARROW, RESPONSE);
|
|
27
|
-
export const STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true));
|
|
28
|
-
export const LABEL = apply(kleft(list_sc(PART, S), seq(tok(T.Colon), S)), (words) => new Label(words.map((w) => w.toString()).join(' ')));
|
|
29
|
-
export const SECTION = apply(LABEL, (text) => new Section(text));
|
|
30
|
-
export const BRANCH = alt_sc(SECTION, STEP); // section first, to make sure there is no colon after step
|
|
31
|
-
export const DENTS = apply(opt_sc(seq(S, alt_sc(tok(T.Plus), tok(T.Minus)), tok(T.Space))), (lineHead) => {
|
|
32
|
-
if (!lineHead)
|
|
33
|
-
return { dent: 0, isFork: true };
|
|
34
|
-
const [dents, seqOrFork] = lineHead;
|
|
35
|
-
return { dent: dents.length / 2, isFork: seqOrFork.kind === T.Plus };
|
|
36
|
-
});
|
|
37
|
-
export const LINE = apply(seq(DENTS, BRANCH, S), ([{ dent, isFork }, branch], [start, end]) => ({
|
|
8
|
+
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)), PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), 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(PHRASE, (parts) => new Response(parts)), 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 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
|
|
9
|
+
DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
|
|
10
|
+
return {
|
|
11
|
+
dent: (seqOrFork.text.length - 2) / 2,
|
|
12
|
+
isFork: seqOrFork.kind === T.Plus,
|
|
13
|
+
};
|
|
14
|
+
}), LINE = apply(seq(DENTS, BRANCH), ([{ dent, isFork }, branch], [start, end]) => ({
|
|
38
15
|
dent,
|
|
39
16
|
branch: branch.setFork(isFork),
|
|
40
|
-
}))
|
|
41
|
-
export const TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(list_sc(apply(LINE, (line, [start, end]) => ({ line, start, end })), NEWLINES), (lines) => {
|
|
17
|
+
})), TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(opt_sc(list_sc(apply(LINE, (line, [start, end]) => ({ line, start, end })), NEWLINES)), (lines) => {
|
|
42
18
|
const startDent = 0;
|
|
43
19
|
let dent = startDent;
|
|
44
20
|
const root = new Section(new Label(''));
|
|
45
21
|
let parent = root;
|
|
46
|
-
for (const { line, start } of lines) {
|
|
22
|
+
for (const { line, start } of lines !== null && lines !== void 0 ? lines : []) {
|
|
47
23
|
const { dent: d, branch } = line;
|
|
48
24
|
if (Math.round(d) !== d) {
|
|
49
25
|
throw new Error(`invalid odd indent of ${d * 2} at line ${start.pos.rowBegin}`);
|
|
@@ -66,13 +42,4 @@ export const TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(list_sc(apply(LINE, (lin
|
|
|
66
42
|
parent.addChild(branch);
|
|
67
43
|
}
|
|
68
44
|
return root;
|
|
69
|
-
}),
|
|
70
|
-
function inputText(start, end) {
|
|
71
|
-
let text = '';
|
|
72
|
-
let t = start;
|
|
73
|
-
while (t && t !== end) {
|
|
74
|
-
text += t.text;
|
|
75
|
-
t = t.next;
|
|
76
|
-
}
|
|
77
|
-
return text;
|
|
78
|
-
}
|
|
45
|
+
}), rep_sc(NEWLINES));
|