harmonyc 0.10.4 → 0.11.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/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: double-quoted strings are passed to the code as strings, and backtick-quoted strings are passed as is. You can use backticks to pass numbers, booleans, null, or objects.
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
- Label are nodes that end with `:`. You can use them to structure your test design.
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 NodeTest {
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, errorMessage) {
62
+ errorStep(action, errorResponse) {
63
63
  var _a;
64
64
  this.declareFeatureVariables([action]);
65
- this.tf.print(`context.task.meta.phrases.push(${str(action.toString())});`);
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 = errorMessage === null || errorMessage === void 0 ? void 0 : errorMessage.toCode(this)) !== null && _a !== void 0 ? _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(`await ${f}.${functionName(p)}(${args.join(', ')});`);
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
- stringLiteral(text) {
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
- ([...phrase.content, phrase.docstring ? [phrase.docstring] : []]
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 += `\n\n//# sourceMappingURL=data:application/json,${encodeURIComponent(this.sm.toString())}`;
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
  }
@@ -1,4 +1,4 @@
1
- import { NodeTest } from "../code_generator/JavaScript.js";
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 NodeTest(testFile, phrasesFile);
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].message);
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 `${this.action}` + this.responses.map((r) => ` => ${r}`).join('');
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(content = [], docstring) {
192
- this.content = content;
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 [...this.content, this.docstring].filter((c) => c instanceof Arg);
231
+ return this.parts.filter((c) => c instanceof Arg);
204
232
  }
205
233
  get isEmpty() {
206
- return this.content.length === 0 && this.docstring === undefined;
234
+ return this.parts.length === 0;
207
235
  }
208
236
  toString() {
209
- return [
210
- ...(this.content.length > 0
211
- ? [this.content.map((c) => c.toString()).join(' ')]
212
- : []),
213
- ...(this.docstring !== undefined
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 [...this.content, ...(this.docstring ? [this.docstring] : [])]
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 (!this.content.length && this.docstring === undefined)
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
- get isErrorResponse() {
241
- if (this.content.length === 1 &&
242
- this.content[0] instanceof Word &&
243
- this.content[0].text === '!!')
244
- return true;
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 (!this.content.length && this.docstring === undefined)
270
+ if (this.isEmpty)
253
271
  return;
254
272
  cg.phrase(this);
255
273
  }
256
274
  }
257
275
  export class ErrorResponse extends Response {
258
- get message() {
259
- var _a;
260
- return (_a = this.content[0]) !== null && _a !== void 0 ? _a : this.docstring;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harmonyc",
3
3
  "description": "Harmony Code - model-driven BDD for Vitest",
4
- "version": "0.10.4",
4
+ "version": "0.11.0",
5
5
  "author": "Bernát Kalló",
6
6
  "type": "module",
7
7
  "bin": {
package/parser/lexer.js CHANGED
@@ -1,4 +1,123 @@
1
- import { buildLexer } from 'typescript-parsec';
1
+ import { TokenError } from 'typescript-parsec';
2
2
  import rules from './lexer_rules.js';
3
3
  export { T } from './lexer_rules.js';
4
- export const lexer = buildLexer(rules);
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);
@@ -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 longest one wins, if same, the former
28
- // patterns must start with ^ and be /g
29
- [true, /^\n/g, T.Newline],
30
- [true, /^\t/g, T.InvalidTab],
31
- [true, /^[\x00-\x1f]/g, T.InvalidWhitespace],
32
- [true, /^ /g, T.Space],
33
- [false, /^(#|>|\/\/).*?(?=\n|$)/g, T.Comment],
34
- [true, /^:(?=\s*(?:\n|$))/g, T.Colon],
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
- /^(?!\s|=>|!!|- |\+ |[\[\]"`|]|:\s*(?:\n|$)).+?(?=[\[\]"`|]|\n|$|=>|!!|:\s*(?:\n|$)|$)/g,
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
- /^"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*/g,
51
+ /"(?:[^"\\\n]|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*/y,
54
52
  T.UnclosedDoubleQuoteString,
55
53
  ],
56
- [true, /^``/g, T.InvalidEmptyBacktickString],
57
- [true, /^`[^`]+`/g, T.BacktickString],
58
- [true, /^`[^`]*/g, T.UnclosedBacktickString],
59
- [true, /^\|(?: .*|(?=\n|$))/g, T.MultilineString],
60
- [true, /^\|[^ \n]/g, T.InvalidMultilineStringMark],
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, alt, } 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 S = rep_sc(tok(T.Space));
9
- export const NEWLINES = list_sc(tok(T.Newline), S); // empty lines can have spaces
10
- export const WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' ')));
11
- export const ERROR_MARK = apply(tok(T.ErrorMark), ({ text }) => new Word(text));
12
- export const BULLET_POINT_LIKE_WORD = apply(alt_sc(tok(T.Plus), tok(T.Minus)), ({ text }) => new Word(text));
13
- export const DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string')));
14
- export const BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1)));
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), ([{ 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(apply(PHRASE, (parts) => new Action(parts)), SET_VARIABLE), 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(RESPONSE, ERROR_RESPONSE, SAVE_TO_VARIABLE)), 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
- }), seq(rep_sc(NEWLINES), S));
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));