harmonyc 0.2.0 → 0.4.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 +62 -85
- package/Router.js +4 -9
- package/cli.js +15 -7
- package/compile.js +9 -18
- package/compiler.js +16 -35
- package/config.js +1 -0
- package/definitions.js +22 -0
- package/frameworks/Gherkin.js +37 -0
- package/frameworks/NodeTest.js +35 -0
- package/languages/Gherkin.js +33 -0
- package/languages/JavaScript.js +31 -0
- package/model.js +81 -52
- package/outFile.js +45 -0
- package/package.json +39 -41
- package/syntax.js +114 -0
- package/util/indent.js +5 -7
- package/util/iterators.js +5 -0
- package/util/xmur3.js +1 -5
- package/watch.js +10 -0
- package/lexer.js +0 -31
- package/parser.js +0 -68
package/README.md
CHANGED
|
@@ -1,85 +1,62 @@
|
|
|
1
|
-
# Harmony Code
|
|
2
|
-
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
### Labels
|
|
65
|
-
|
|
66
|
-
Label are nodes that end with `:`. You can use them to structure your test design.
|
|
67
|
-
They are not included in the test case, but the test case name is generated from the labels.
|
|
68
|
-
|
|
69
|
-
### Comments
|
|
70
|
-
|
|
71
|
-
Lines starting with `#` or `//` are comments and are ignored.
|
|
72
|
-
|
|
73
|
-
### Steps
|
|
74
|
-
|
|
75
|
-
All other lines are steps. A step consists of an action, and one or more responses denoted by `=>`.
|
|
76
|
-
Actions will become `When` steps, and responses will become `Then` steps.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
## Running the tests
|
|
80
|
-
|
|
81
|
-
Currenlty `harmonyc` only compiles `.harmony` files to `.feature` files. To run the tests, you need to set up Cucumber or Specflow, depending on your environment.
|
|
82
|
-
|
|
83
|
-
## License
|
|
84
|
-
|
|
85
|
-
MIT
|
|
1
|
+
# Harmony Code
|
|
2
|
+
|
|
3
|
+
A test design & BDD tool that helps you separate the _what_ to test from the _how_ to automate it. You write test cases in a simple Markdown format, and then automate them with Vitest (and soon with many more frameworks and languages).
|
|
4
|
+
|
|
5
|
+
(**Note**: There have been big changes since v0.2.)
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
1. Have Node.js installed.
|
|
10
|
+
2. You can compile your `*.md` files in the `src` folder by running
|
|
11
|
+
|
|
12
|
+
```bash script
|
|
13
|
+
npx harmonyc src/**/*.md
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
3. Then you can run the generated tests with Vitest by running
|
|
17
|
+
|
|
18
|
+
```bash script
|
|
19
|
+
npx vitest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Syntax
|
|
23
|
+
|
|
24
|
+
A Harmony Code file is a Markdown file with a syntax that looks like this:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
# Products API
|
|
28
|
+
|
|
29
|
+
- **Create**:
|
|
30
|
+
- **Anonymous:**
|
|
31
|
+
- create product => error: unauthorized
|
|
32
|
+
- **Admin**:
|
|
33
|
+
1. authenticate admin
|
|
34
|
+
2. create product => product created
|
|
35
|
+
3. **Delete:**
|
|
36
|
+
- delete product => product deleted
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Actions and responses
|
|
40
|
+
|
|
41
|
+
List items (either in ordered or bulleted lists) consist of an **action** and zero or more **response**s, separated by a `=>`.
|
|
42
|
+
|
|
43
|
+
The generated steps contain the feature name after a `||`. Cucumber's steps are in one global namespace, but including the feature name makes it easy to scope step defintions by feature.
|
|
44
|
+
|
|
45
|
+
### Sequences and forks
|
|
46
|
+
|
|
47
|
+
An ordered list means a **sequence**: the list items are included int the tests in order.
|
|
48
|
+
|
|
49
|
+
A bulleted list (`-` or `*` or `+`) means a **fork**: the node directly follows its parent node. All list items are separate branches, they will generate separate scenarios.
|
|
50
|
+
|
|
51
|
+
### Labels
|
|
52
|
+
|
|
53
|
+
Label are nodes that end with `:`. You can use them to structure your test design.
|
|
54
|
+
They are not included in the test case, but the test case name is generated from the labels.
|
|
55
|
+
|
|
56
|
+
### Text
|
|
57
|
+
|
|
58
|
+
Paragraphs outside of lists are for humans only, they are ignored in the automated tests.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/Router.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.Routers = exports.Router = void 0;
|
|
4
|
-
const xmur3_1 = require("./util/xmur3");
|
|
5
|
-
class Router {
|
|
1
|
+
import { xmur3 } from './util/xmur3';
|
|
2
|
+
export class Router {
|
|
6
3
|
constructor(outs, seed = 'TODO') {
|
|
7
4
|
this.outs = outs;
|
|
8
5
|
this.index = 0;
|
|
9
6
|
this.started = new Set();
|
|
10
7
|
this.covered = new Set();
|
|
11
|
-
this.random =
|
|
8
|
+
this.random = xmur3(seed);
|
|
12
9
|
}
|
|
13
10
|
next() {
|
|
14
11
|
if (this.outs.length === 0)
|
|
@@ -22,8 +19,7 @@ class Router {
|
|
|
22
19
|
return this.outs.length - this.index;
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
|
-
|
|
26
|
-
class Routers {
|
|
22
|
+
export class Routers {
|
|
27
23
|
constructor(root) {
|
|
28
24
|
this.root = root;
|
|
29
25
|
this.routers = new Map();
|
|
@@ -56,4 +52,3 @@ class Routers {
|
|
|
56
52
|
return Array.from(this.routers.values()).reduce((sum, r) => sum + r.incompleteCount, 0);
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
|
-
exports.Routers = Routers;
|
package/cli.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { compileFiles } from './compiler';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { watchFiles } from './watch';
|
|
5
|
+
const args = parseArgs({
|
|
6
|
+
options: {
|
|
7
|
+
help: { type: 'boolean' },
|
|
8
|
+
watch: { type: 'boolean' },
|
|
9
|
+
},
|
|
10
|
+
allowPositionals: true,
|
|
11
|
+
});
|
|
12
|
+
if (args.positionals.length === 0 || args.values.help) {
|
|
13
|
+
console.error('Usage: harmonyc <input files glob pattern>');
|
|
7
14
|
process.exit(1);
|
|
8
15
|
}
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
if (args.values.watch)
|
|
17
|
+
watchFiles(args.positionals);
|
|
18
|
+
void compileFiles(args.positionals);
|
package/compile.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const tests = (0, model_1.makeTests)(root);
|
|
11
|
-
const lines = [
|
|
12
|
-
`Feature: ${name}`,
|
|
13
|
-
'',
|
|
14
|
-
...(0, indent_1.indent)(tests.flatMap((test) => test.toGherkin())),
|
|
15
|
-
];
|
|
16
|
-
lines[0] = `Feature: ${name}`;
|
|
17
|
-
return lines.join('\n');
|
|
1
|
+
import { NodeTest } from './languages/JavaScript';
|
|
2
|
+
import { OutFile } from './outFile';
|
|
3
|
+
import { parse } from './syntax';
|
|
4
|
+
export function compileFeature(fileName, src) {
|
|
5
|
+
const feature = parse({ fileName, src });
|
|
6
|
+
const of = new OutFile();
|
|
7
|
+
const cg = new NodeTest(of);
|
|
8
|
+
feature.toCode(cg);
|
|
9
|
+
return of.value;
|
|
18
10
|
}
|
|
19
|
-
exports.compileFeature = compileFeature;
|
package/compiler.js
CHANGED
|
@@ -1,37 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.compileFile = exports.compileFiles = void 0;
|
|
16
|
-
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
17
|
-
const fs_1 = require("fs");
|
|
18
|
-
const path_1 = require("path");
|
|
19
|
-
const compile_1 = require("./compile");
|
|
20
|
-
function compileFiles(pattern) {
|
|
21
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
22
|
-
const fns = yield (0, fast_glob_1.default)(pattern);
|
|
23
|
-
if (!fns.length)
|
|
24
|
-
throw new Error(`No files found for pattern: ${String(pattern)}`);
|
|
25
|
-
yield Promise.all(fns.map((fn) => compileFile(fn)));
|
|
26
|
-
});
|
|
1
|
+
import glob from 'fast-glob';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { compileFeature } from './compile';
|
|
5
|
+
export async function compileFiles(pattern) {
|
|
6
|
+
const fns = await glob(pattern);
|
|
7
|
+
if (!fns.length)
|
|
8
|
+
throw new Error(`No files found for pattern: ${String(pattern)}`);
|
|
9
|
+
await Promise.all(fns.map((fn) => compileFile(fn)));
|
|
10
|
+
console.log(`Compiled ${fns.length} file${fns.length === 1 ? '' : 's'}.`);
|
|
11
|
+
return fns;
|
|
27
12
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const outFn = fn.replace(/\.[a-z]+$/i, '.feature');
|
|
34
|
-
(0, fs_1.writeFileSync)(outFn, (0, compile_1.compileFeature)(name, src));
|
|
35
|
-
});
|
|
13
|
+
export async function compileFile(fn) {
|
|
14
|
+
const src = readFileSync(fn, 'utf8').toString();
|
|
15
|
+
const name = basename(fn).replace(/\.[a-z]+$/i, '');
|
|
16
|
+
const outFn = `${fn.replace(/\.[a-z]+$/i, '')}.mjs`;
|
|
17
|
+
writeFileSync(outFn, compileFeature(fn, src));
|
|
36
18
|
}
|
|
37
|
-
exports.compileFile = compileFile;
|
package/config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/definitions.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CucumberExpression, ParameterTypeRegistry, } from '@cucumber/cucumber-expressions';
|
|
2
|
+
const registry = new ParameterTypeRegistry();
|
|
3
|
+
export function definitions({ marker, code, feature, }) {
|
|
4
|
+
var _a, _b;
|
|
5
|
+
const re = new RegExp(`^\s*${q(marker)}(.*?)$`, 'gm');
|
|
6
|
+
let match = re.exec(code);
|
|
7
|
+
const start = (_a = match === null || match === void 0 ? void 0 : match.index) !== null && _a !== void 0 ? _a : code.length;
|
|
8
|
+
feature.prelude += code.slice(0, start);
|
|
9
|
+
while (match) {
|
|
10
|
+
const bodyStart = match.index + match[0].length;
|
|
11
|
+
const head = match[1].trim();
|
|
12
|
+
match = re.exec(code);
|
|
13
|
+
const end = (_b = match === null || match === void 0 ? void 0 : match.index) !== null && _b !== void 0 ? _b : code.length;
|
|
14
|
+
const body = code.slice(bodyStart, end).trim();
|
|
15
|
+
if (body) {
|
|
16
|
+
feature.definitions.set(new CucumberExpression(head, registry), body);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function q(pattern) {
|
|
21
|
+
return pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Gherkin = void 0;
|
|
4
|
+
class Gherkin {
|
|
5
|
+
constructor(outFile) {
|
|
6
|
+
this.outFile = outFile;
|
|
7
|
+
}
|
|
8
|
+
feature(name, tests) {
|
|
9
|
+
this.outFile.print(`Feature: ${name}`);
|
|
10
|
+
this.outFile.print('');
|
|
11
|
+
this.outFile.indent(() => {
|
|
12
|
+
for (const test of tests) {
|
|
13
|
+
test.toCode(this);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
test(t) {
|
|
18
|
+
this.outFile.print(`Scenario: ${t.name}`);
|
|
19
|
+
this.outFile.indent(() => {
|
|
20
|
+
for (const step of t.steps) {
|
|
21
|
+
step.toCode(this);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
this.outFile.print('');
|
|
25
|
+
}
|
|
26
|
+
phrase(p) {
|
|
27
|
+
this.outFile.print(`${p.keyword} ${p.text} || ${p.feature}`);
|
|
28
|
+
if (p.docstring !== undefined) {
|
|
29
|
+
this.outFile.indent(() => {
|
|
30
|
+
this.outFile.print(`"""`);
|
|
31
|
+
this.outFile.print(...p.docstring.split('\n'));
|
|
32
|
+
this.outFile.print(`"""`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.Gherkin = Gherkin;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeTest = void 0;
|
|
4
|
+
class NodeTest {
|
|
5
|
+
constructor(outFile) {
|
|
6
|
+
this.outFile = outFile;
|
|
7
|
+
this.framework = 'vitest';
|
|
8
|
+
}
|
|
9
|
+
feature(feature) {
|
|
10
|
+
if (this.framework === 'vitest') {
|
|
11
|
+
this.outFile.print(`import { test, expect } from 'vitest';`);
|
|
12
|
+
}
|
|
13
|
+
this.outFile.print(feature.prelude);
|
|
14
|
+
for (const test of feature.tests) {
|
|
15
|
+
test.toCode(this);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
test(t) {
|
|
19
|
+
this.outFile.print(`test('${t.name}', async () => {`);
|
|
20
|
+
this.outFile.indent(() => {
|
|
21
|
+
for (const step of t.steps) {
|
|
22
|
+
step.toCode(this);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
this.outFile.print('})');
|
|
26
|
+
this.outFile.print('');
|
|
27
|
+
}
|
|
28
|
+
phrase(p) {
|
|
29
|
+
var _a;
|
|
30
|
+
this.outFile.loc(p).print('/// ' + p.text);
|
|
31
|
+
const code = (_a = p.definition()) !== null && _a !== void 0 ? _a : `throw 'Not defined: ' + ${JSON.stringify(p.text)};`;
|
|
32
|
+
this.outFile.print(...code.split('\n'));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.NodeTest = NodeTest;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class Gherkin {
|
|
2
|
+
constructor(outFile) {
|
|
3
|
+
this.outFile = outFile;
|
|
4
|
+
}
|
|
5
|
+
feature(feature) {
|
|
6
|
+
this.outFile.print(`Feature: ${feature.name}`);
|
|
7
|
+
this.outFile.print('');
|
|
8
|
+
this.outFile.indent(() => {
|
|
9
|
+
for (const test of feature.tests) {
|
|
10
|
+
test.toCode(this);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
test(t) {
|
|
15
|
+
this.outFile.print(`Scenario: ${t.name}`);
|
|
16
|
+
this.outFile.indent(() => {
|
|
17
|
+
for (const step of t.steps) {
|
|
18
|
+
step.toCode(this);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
this.outFile.print('');
|
|
22
|
+
}
|
|
23
|
+
phrase(p) {
|
|
24
|
+
this.outFile.print(`${p.keyword} ${p.text} || ${p.feature}`);
|
|
25
|
+
if (p.docstring !== undefined) {
|
|
26
|
+
this.outFile.indent(() => {
|
|
27
|
+
this.outFile.print(`"""`);
|
|
28
|
+
this.outFile.print(...p.docstring.split('\n'));
|
|
29
|
+
this.outFile.print(`"""`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class NodeTest {
|
|
2
|
+
constructor(outFile) {
|
|
3
|
+
this.outFile = outFile;
|
|
4
|
+
this.framework = 'vitest';
|
|
5
|
+
}
|
|
6
|
+
feature(feature) {
|
|
7
|
+
if (this.framework === 'vitest') {
|
|
8
|
+
this.outFile.print(`import { test, expect } from 'vitest';`);
|
|
9
|
+
}
|
|
10
|
+
this.outFile.print(feature.prelude);
|
|
11
|
+
for (const test of feature.tests) {
|
|
12
|
+
test.toCode(this);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
test(t) {
|
|
16
|
+
this.outFile.print(`test('${t.name}', async () => {`);
|
|
17
|
+
this.outFile.indent(() => {
|
|
18
|
+
for (const step of t.steps) {
|
|
19
|
+
step.toCode(this);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
this.outFile.print('})');
|
|
23
|
+
this.outFile.print('');
|
|
24
|
+
}
|
|
25
|
+
phrase(p) {
|
|
26
|
+
var _a;
|
|
27
|
+
this.outFile.loc(p).print('/// ' + p.text);
|
|
28
|
+
const code = (_a = p.definition()) !== null && _a !== void 0 ? _a : `throw 'Not defined: ' + ${JSON.stringify(p.text)};`;
|
|
29
|
+
this.outFile.print(...code.split('\n'));
|
|
30
|
+
}
|
|
31
|
+
}
|
package/model.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Routers } from './Router.js';
|
|
2
|
+
export class Feature {
|
|
3
|
+
constructor(name) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
this.root = new Section();
|
|
6
|
+
this.definitions = new Map();
|
|
7
|
+
this.prelude = '';
|
|
8
|
+
}
|
|
9
|
+
get tests() {
|
|
10
|
+
return makeTests(this.root);
|
|
11
|
+
}
|
|
12
|
+
toCode(cg) {
|
|
13
|
+
cg.feature(this);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class Branch {
|
|
7
17
|
constructor(children = []) {
|
|
8
18
|
this.isFork = false;
|
|
9
19
|
this.isEnd = false;
|
|
@@ -14,9 +24,9 @@ class Branch {
|
|
|
14
24
|
this.isFork = isFork;
|
|
15
25
|
return this;
|
|
16
26
|
}
|
|
17
|
-
|
|
27
|
+
setFeature(feature) {
|
|
18
28
|
for (const child of this.children)
|
|
19
|
-
child.
|
|
29
|
+
child.setFeature(feature);
|
|
20
30
|
return this;
|
|
21
31
|
}
|
|
22
32
|
addChild(child, index = this.children.length) {
|
|
@@ -57,85 +67,107 @@ class Branch {
|
|
|
57
67
|
return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
constructor(action = '', responses = [], children, isFork = false, featureName = '') {
|
|
70
|
+
export class Step extends Branch {
|
|
71
|
+
constructor(action = '', responses = [], children, isFork = false) {
|
|
63
72
|
super(children);
|
|
64
|
-
this.action = new Action(action
|
|
65
|
-
this.responses = responses.map((response) => new Response(response
|
|
73
|
+
this.action = new Action(action);
|
|
74
|
+
this.responses = responses.map((response) => new Response(response));
|
|
66
75
|
this.isFork = isFork;
|
|
67
76
|
}
|
|
68
77
|
get phrases() {
|
|
69
78
|
return [this.action, ...this.responses];
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
toCode(cg) {
|
|
81
|
+
for (const phrase of this.phrases) {
|
|
82
|
+
phrase.toCode(cg);
|
|
83
|
+
}
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
this.action.
|
|
85
|
+
setFeature(feature) {
|
|
86
|
+
this.action.setFeature(feature);
|
|
76
87
|
for (const response of this.responses)
|
|
77
|
-
response.
|
|
78
|
-
return super.
|
|
88
|
+
response.setFeature(feature);
|
|
89
|
+
return super.setFeature(feature);
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
|
-
|
|
82
|
-
class State {
|
|
92
|
+
export class State {
|
|
83
93
|
constructor(text = '') {
|
|
84
94
|
this.text = text;
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
|
-
|
|
88
|
-
class Label {
|
|
97
|
+
export class Label {
|
|
89
98
|
constructor(text = '') {
|
|
90
99
|
this.text = text;
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
|
-
|
|
94
|
-
class Section extends Branch {
|
|
102
|
+
export class Section extends Branch {
|
|
95
103
|
constructor(label = '', children, isFork = false) {
|
|
96
104
|
super(children);
|
|
97
105
|
this.label = new Label(label);
|
|
98
106
|
this.isFork = isFork;
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
constructor(text = '', featureName = '') {
|
|
109
|
+
export class Phrase {
|
|
110
|
+
constructor(text = '') {
|
|
104
111
|
this.text = text;
|
|
105
|
-
this.featureName = featureName;
|
|
106
|
-
}
|
|
107
|
-
setFeatureName(featureName) {
|
|
108
|
-
this.featureName = featureName;
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
setFeature(feature) {
|
|
114
|
+
this.feature = feature;
|
|
115
|
+
}
|
|
116
|
+
get keyword() {
|
|
117
|
+
return this.kind === 'action' ? 'When' : 'Then';
|
|
118
|
+
}
|
|
119
|
+
toCode(cg) {
|
|
120
|
+
return cg.phrase(this);
|
|
121
|
+
}
|
|
122
|
+
definition() {
|
|
123
|
+
const key = this.kind === 'action' ? this.text : `=> ${this.text}`;
|
|
124
|
+
let args;
|
|
125
|
+
let code;
|
|
126
|
+
for (const [ce, c] of this.feature.definitions.entries()) {
|
|
127
|
+
const m = ce.match(key);
|
|
128
|
+
if (!m)
|
|
129
|
+
continue;
|
|
130
|
+
if (args !== undefined)
|
|
131
|
+
throw new Error(`Ambiguous definition: ${key}`);
|
|
132
|
+
args = m;
|
|
133
|
+
code = c;
|
|
134
|
+
}
|
|
135
|
+
if (args === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
return code.replace(/\$([$_1-9])/g, (s, varName) => {
|
|
138
|
+
var _a;
|
|
139
|
+
if (varName.match(/[1-9]/))
|
|
140
|
+
return JSON.stringify(args[parseInt(varName) - 1].getValue(undefined));
|
|
141
|
+
else if (varName === '_')
|
|
142
|
+
return JSON.stringify((_a = this.docstring) !== null && _a !== void 0 ? _a : '');
|
|
143
|
+
else if (varName === '$')
|
|
144
|
+
return '$';
|
|
145
|
+
else
|
|
146
|
+
return s;
|
|
147
|
+
});
|
|
112
148
|
}
|
|
113
149
|
}
|
|
114
|
-
|
|
115
|
-
class Action extends Phrase {
|
|
150
|
+
export class Action extends Phrase {
|
|
116
151
|
constructor() {
|
|
117
152
|
super(...arguments);
|
|
118
153
|
this.kind = 'action';
|
|
119
154
|
}
|
|
120
155
|
}
|
|
121
|
-
|
|
122
|
-
class Response extends Phrase {
|
|
156
|
+
export class Response extends Phrase {
|
|
123
157
|
constructor() {
|
|
124
158
|
super(...arguments);
|
|
125
159
|
this.kind = 'response';
|
|
126
160
|
}
|
|
127
161
|
}
|
|
128
|
-
|
|
129
|
-
class Precondition extends Branch {
|
|
162
|
+
export class Precondition extends Branch {
|
|
130
163
|
constructor(state = '') {
|
|
131
164
|
super();
|
|
132
165
|
this.state = new State();
|
|
133
166
|
this.state.text = state;
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const routers = new Router_1.Routers(root);
|
|
169
|
+
export function makeTests(root) {
|
|
170
|
+
const routers = new Routers(root);
|
|
139
171
|
const tests = [];
|
|
140
172
|
let ic = routers.getIncompleteCount();
|
|
141
173
|
let newIc;
|
|
@@ -159,8 +191,7 @@ function makeTests(root) {
|
|
|
159
191
|
tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
|
|
160
192
|
return tests;
|
|
161
193
|
}
|
|
162
|
-
|
|
163
|
-
class Test {
|
|
194
|
+
export class Test {
|
|
164
195
|
constructor(root, branches) {
|
|
165
196
|
this.root = root;
|
|
166
197
|
this.branches = branches;
|
|
@@ -176,12 +207,10 @@ class Test {
|
|
|
176
207
|
.filter((b) => b instanceof Section)
|
|
177
208
|
.map((s) => s.label.text);
|
|
178
209
|
}
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
];
|
|
210
|
+
get name() {
|
|
211
|
+
return `${this.testNumber}${this.labels.length > 0 ? ' - ' : ''}${this.labels.join(' - ')}`;
|
|
212
|
+
}
|
|
213
|
+
toCode(cg) {
|
|
214
|
+
cg.test(this);
|
|
185
215
|
}
|
|
186
216
|
}
|
|
187
|
-
exports.Test = Test;
|
package/outFile.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SourceMapGenerator } from 'source-map-js';
|
|
2
|
+
export class OutFile {
|
|
3
|
+
constructor(indentSpaces = 2) {
|
|
4
|
+
this.indentSpaces = indentSpaces;
|
|
5
|
+
this.lines = [];
|
|
6
|
+
this.level = 0;
|
|
7
|
+
this.sm = new SourceMapGenerator();
|
|
8
|
+
}
|
|
9
|
+
indent(fn) {
|
|
10
|
+
this.level++;
|
|
11
|
+
try {
|
|
12
|
+
fn();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
this.level--;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
print(...lines) {
|
|
19
|
+
const l = this.lines.length;
|
|
20
|
+
this.lines.push(...lines.map((line) => ' '.repeat(this.level * this.indentSpaces) + line));
|
|
21
|
+
if (this.currentLoc)
|
|
22
|
+
for (const i of lines.keys()) {
|
|
23
|
+
this.sm.addMapping({
|
|
24
|
+
source: this.currentLoc.fileName,
|
|
25
|
+
original: {
|
|
26
|
+
line: this.currentLoc.line,
|
|
27
|
+
column: this.currentLoc.column,
|
|
28
|
+
},
|
|
29
|
+
generated: {
|
|
30
|
+
line: l + i,
|
|
31
|
+
column: this.level * this.indentSpaces,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
loc({ location }) {
|
|
38
|
+
this.currentLoc = location;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
get value() {
|
|
42
|
+
return (this.lines.join('\n') +
|
|
43
|
+
`\n\n//# sourceMappingURL=data:application/json,${encodeURIComponent(this.sm.toString())}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "harmonyc",
|
|
3
|
-
"description": "Harmony Code -
|
|
4
|
-
"version": "0.
|
|
5
|
-
"author": "Bernát Kalló",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"unit
|
|
25
|
-
"
|
|
26
|
-
"bdd",
|
|
27
|
-
"behavior-driven",
|
|
28
|
-
"test design",
|
|
29
|
-
"test design automation",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"test
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
]
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "harmonyc",
|
|
3
|
+
"description": "Harmony Code - model-driven BDD for Vitest",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"author": "Bernát Kalló",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"harmonyc": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/harmony-ac/code#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/harmony-ac/code.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/harmony-ac/code/issues"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"fast-glob": "^3.3.2",
|
|
20
|
+
"typescript-parsec": "0.3.4"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"unit test",
|
|
25
|
+
"unit testing",
|
|
26
|
+
"bdd",
|
|
27
|
+
"behavior-driven",
|
|
28
|
+
"test design",
|
|
29
|
+
"test design automation",
|
|
30
|
+
"harmony.ac",
|
|
31
|
+
"test framework",
|
|
32
|
+
"model-based test",
|
|
33
|
+
"model-based testing",
|
|
34
|
+
"test model",
|
|
35
|
+
"test modeling",
|
|
36
|
+
"test modelling",
|
|
37
|
+
"vitest"
|
|
38
|
+
]
|
|
39
|
+
}
|
package/syntax.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import remarkParse from 'remark-parse';
|
|
2
|
+
import { unified } from 'unified';
|
|
3
|
+
import { Feature, Section, Step } from './model';
|
|
4
|
+
import { definitions } from './definitions';
|
|
5
|
+
export function parse({ fileName, src, }) {
|
|
6
|
+
const tree = unified().use(remarkParse).parse(src);
|
|
7
|
+
const rootNodes = tree.children;
|
|
8
|
+
const feature = new Feature('');
|
|
9
|
+
const headings = [feature.root];
|
|
10
|
+
let name;
|
|
11
|
+
for (let i = 0; i < rootNodes.length; i++) {
|
|
12
|
+
const node = rootNodes[i];
|
|
13
|
+
if (node.type === 'heading') {
|
|
14
|
+
if (node.depth === 1 && name === undefined) {
|
|
15
|
+
name = textContent(node);
|
|
16
|
+
}
|
|
17
|
+
const level = node.depth;
|
|
18
|
+
const section = new Section(textContent(node), [], true);
|
|
19
|
+
setLocation(section, node, fileName);
|
|
20
|
+
const last = headings.slice(0, level).reverse().find(Boolean);
|
|
21
|
+
last.addChild(section);
|
|
22
|
+
headings[level] = section;
|
|
23
|
+
headings.length = level + 1;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const last = headings[headings.length - 1];
|
|
27
|
+
for (const branch of topLevel(node))
|
|
28
|
+
last.addChild(branch);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
feature.name = name !== null && name !== void 0 ? name : fileName;
|
|
32
|
+
feature.root.setFeature(feature);
|
|
33
|
+
return feature;
|
|
34
|
+
function topLevel(node) {
|
|
35
|
+
if (node.type === 'paragraph')
|
|
36
|
+
return [];
|
|
37
|
+
if (node.type == 'list')
|
|
38
|
+
return list(node);
|
|
39
|
+
if (node.type === 'code')
|
|
40
|
+
return code(node);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
function list(node) {
|
|
44
|
+
const isFork = node.ordered === false;
|
|
45
|
+
return node.children.map((item) => listItem(item, isFork));
|
|
46
|
+
}
|
|
47
|
+
function code(node) {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!((_a = node.meta) === null || _a === void 0 ? void 0 : _a.match(/harmony/)))
|
|
50
|
+
return [];
|
|
51
|
+
const code = node.value;
|
|
52
|
+
const marker = '///';
|
|
53
|
+
definitions({ marker, code, feature });
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
function listItem(node, isFork) {
|
|
57
|
+
const first = node.children[0];
|
|
58
|
+
const text = textContent(first);
|
|
59
|
+
let branch;
|
|
60
|
+
if (first.type === 'heading') {
|
|
61
|
+
branch = new Section(text, [], isFork);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const [action, ...responses] = text.split(/(?:^| )=>(?: |$)/);
|
|
65
|
+
const step = (branch = new Step(action, responses.filter(Boolean), [], isFork));
|
|
66
|
+
setLocation(step.action, first, fileName);
|
|
67
|
+
for (const response of step.responses) {
|
|
68
|
+
// todo separate locations along =>'s
|
|
69
|
+
setLocation(response, first, fileName);
|
|
70
|
+
}
|
|
71
|
+
let i = 0;
|
|
72
|
+
for (const child of node.children.slice(1)) {
|
|
73
|
+
if (child.type === 'list')
|
|
74
|
+
break;
|
|
75
|
+
if (child.type === 'code') {
|
|
76
|
+
if (step.phrases[i])
|
|
77
|
+
step.phrases[i].docstring = child.value;
|
|
78
|
+
else
|
|
79
|
+
break;
|
|
80
|
+
++i;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const child of node.children) {
|
|
85
|
+
if (child.type === 'list') {
|
|
86
|
+
for (const b of list(child))
|
|
87
|
+
branch.addChild(b);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
setLocation(branch, node, fileName);
|
|
91
|
+
return branch;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function textContent(node) {
|
|
95
|
+
if (!node)
|
|
96
|
+
return '';
|
|
97
|
+
if (node.type === 'text') {
|
|
98
|
+
return node.value.split(/\s+/).filter(Boolean).join(' ');
|
|
99
|
+
}
|
|
100
|
+
if (node.type === 'list')
|
|
101
|
+
return '';
|
|
102
|
+
if (!('children' in node))
|
|
103
|
+
return '';
|
|
104
|
+
return node.children.map(textContent).filter(Boolean).join(' ');
|
|
105
|
+
}
|
|
106
|
+
function setLocation(model, node, fileName) {
|
|
107
|
+
if (!node.position)
|
|
108
|
+
return;
|
|
109
|
+
model.location = {
|
|
110
|
+
line: node.position.start.line,
|
|
111
|
+
column: node.position.start.column,
|
|
112
|
+
fileName,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/util/indent.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
exports.indent = indent;
|
|
1
|
+
export const Indent = (n) => function* indent(lines) {
|
|
2
|
+
for (const line of lines) {
|
|
3
|
+
yield ' '.repeat(n) + line;
|
|
4
|
+
}
|
|
5
|
+
};
|
package/util/xmur3.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.xmur3 = void 0;
|
|
4
1
|
// https://stackoverflow.com/revisions/47593316/25 by bryc (github.com/bryc)
|
|
5
|
-
function xmur3(str) {
|
|
2
|
+
export function xmur3(str) {
|
|
6
3
|
let h = 1779033703 ^ str.length;
|
|
7
4
|
for (let i = 0; i < str.length; i++)
|
|
8
5
|
(h = Math.imul(h ^ str.charCodeAt(i), 3432918353)),
|
|
@@ -13,4 +10,3 @@ function xmur3(str) {
|
|
|
13
10
|
return (h ^= h >>> 16) >>> 0;
|
|
14
11
|
};
|
|
15
12
|
}
|
|
16
|
-
exports.xmur3 = xmur3;
|
package/watch.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { compileFile, compileFiles } from './compiler';
|
|
3
|
+
export async function watchFiles(patterns) {
|
|
4
|
+
const fns = await compileFiles(patterns);
|
|
5
|
+
for (const file of fns) {
|
|
6
|
+
watch(file, () => {
|
|
7
|
+
compileFile(file);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
package/lexer.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.lexer = exports.T = void 0;
|
|
4
|
-
const typescript_parsec_1 = require("typescript-parsec");
|
|
5
|
-
var T;
|
|
6
|
-
(function (T) {
|
|
7
|
-
T[T["Newline"] = 0] = "Newline";
|
|
8
|
-
T[T["EOF"] = 1] = "EOF";
|
|
9
|
-
T[T["Comment"] = 2] = "Comment";
|
|
10
|
-
T[T["Dent"] = 3] = "Dent";
|
|
11
|
-
T[T["Seq"] = 4] = "Seq";
|
|
12
|
-
T[T["Fork"] = 5] = "Fork";
|
|
13
|
-
T[T["State"] = 6] = "State";
|
|
14
|
-
T[T["Label"] = 7] = "Label";
|
|
15
|
-
T[T["ResponseMark"] = 8] = "ResponseMark";
|
|
16
|
-
T[T["Phrase"] = 9] = "Phrase";
|
|
17
|
-
T[T["Space"] = 10] = "Space";
|
|
18
|
-
})(T || (exports.T = T = {}));
|
|
19
|
-
exports.lexer = (0, typescript_parsec_1.buildLexer)([
|
|
20
|
-
[false, /^\s*\n/g, T.Newline],
|
|
21
|
-
[false, /^\s*$/g, T.EOF],
|
|
22
|
-
[false, /^#.*?(?=\n|$)/g, T.Comment],
|
|
23
|
-
[false, /^\/\/.*?(?=\n|$)/g, T.Comment],
|
|
24
|
-
[true, /^ {2}/g, T.Dent],
|
|
25
|
-
[true, /^- /g, T.Seq],
|
|
26
|
-
[true, /^\+ /g, T.Fork],
|
|
27
|
-
[true, /^\[\s*.*?\s*\]/g, T.State],
|
|
28
|
-
[true, /^[^-+\s[][^\n[]*?\s*:(?=\s*(?:\n|$))/g, T.Label],
|
|
29
|
-
[true, /^\s*=>\s*/g, T.ResponseMark],
|
|
30
|
-
[true, /^[^-+\s[][^\n[]*?(?=\s*(?:\n|$|\[|=>))/g, T.Phrase],
|
|
31
|
-
]);
|
package/parser.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parse = void 0;
|
|
4
|
-
const typescript_parsec_1 = require("typescript-parsec");
|
|
5
|
-
const lexer_1 = require("./lexer");
|
|
6
|
-
const model_1 = require("./model");
|
|
7
|
-
function parse(input) {
|
|
8
|
-
const tokens = lexer_1.lexer.parse(input);
|
|
9
|
-
return (0, typescript_parsec_1.expectSingleResult)((0, typescript_parsec_1.expectEOF)(TEST_DESIGN.parse(tokens)));
|
|
10
|
-
}
|
|
11
|
-
exports.parse = parse;
|
|
12
|
-
const PHRASE = (0, typescript_parsec_1.rule)();
|
|
13
|
-
PHRASE.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.tok)(lexer_1.T.Phrase), ({ text }) => text));
|
|
14
|
-
const STEP = (0, typescript_parsec_1.rule)();
|
|
15
|
-
STEP.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.seq)(PHRASE, (0, typescript_parsec_1.rep_sc)((0, typescript_parsec_1.kright)((0, typescript_parsec_1.tok)(lexer_1.T.ResponseMark), PHRASE))), ([action, responses]) => new model_1.Step(action, responses).setFork(true)));
|
|
16
|
-
const SECTION = (0, typescript_parsec_1.rule)();
|
|
17
|
-
SECTION.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.tok)(lexer_1.T.Label), ({ text }) => new model_1.Section(text.slice(0, -1))));
|
|
18
|
-
const BRANCH = (0, typescript_parsec_1.rule)();
|
|
19
|
-
BRANCH.setPattern((0, typescript_parsec_1.alt_sc)(STEP, SECTION));
|
|
20
|
-
const DENTS = (0, typescript_parsec_1.rule)();
|
|
21
|
-
DENTS.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.opt_sc)((0, typescript_parsec_1.seq)((0, typescript_parsec_1.rep_sc)((0, typescript_parsec_1.tok)(lexer_1.T.Dent)), (0, typescript_parsec_1.alt_sc)((0, typescript_parsec_1.tok)(lexer_1.T.Seq), (0, typescript_parsec_1.tok)(lexer_1.T.Fork)))), (lineHead) => {
|
|
22
|
-
if (!lineHead)
|
|
23
|
-
return { dent: 0, isFork: true };
|
|
24
|
-
const [dents, seqOrFork] = lineHead;
|
|
25
|
-
return { dent: dents.length + 1, isFork: seqOrFork.kind === lexer_1.T.Fork };
|
|
26
|
-
}));
|
|
27
|
-
const LINE = (0, typescript_parsec_1.rule)();
|
|
28
|
-
LINE.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.seq)(DENTS, BRANCH), ([{ dent, isFork }, branch], [start, end]) => ({
|
|
29
|
-
dent,
|
|
30
|
-
branch: branch.setFork(isFork),
|
|
31
|
-
})));
|
|
32
|
-
const TEST_DESIGN = (0, typescript_parsec_1.rule)();
|
|
33
|
-
TEST_DESIGN.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.rep_sc)(LINE), (lines) => {
|
|
34
|
-
const startDent = 0;
|
|
35
|
-
let dent = startDent;
|
|
36
|
-
const root = new model_1.Section();
|
|
37
|
-
let parent = root;
|
|
38
|
-
let lineNo = 0;
|
|
39
|
-
for (const { dent: d, branch } of lines) {
|
|
40
|
-
++lineNo;
|
|
41
|
-
if (d > dent + 1) {
|
|
42
|
-
throw new Error(`invalid indent ${d} at line ${lineNo}`);
|
|
43
|
-
}
|
|
44
|
-
else if (d === dent + 1) {
|
|
45
|
-
parent = parent.children[parent.children.length - 1];
|
|
46
|
-
++dent;
|
|
47
|
-
}
|
|
48
|
-
else if (d < startDent) {
|
|
49
|
-
throw new Error(`invalid indent at line ${lineNo}`);
|
|
50
|
-
}
|
|
51
|
-
else
|
|
52
|
-
while (d < dent) {
|
|
53
|
-
parent = parent.parent;
|
|
54
|
-
--dent;
|
|
55
|
-
}
|
|
56
|
-
parent.addChild(branch);
|
|
57
|
-
}
|
|
58
|
-
return root;
|
|
59
|
-
}));
|
|
60
|
-
function inputText(start, end) {
|
|
61
|
-
let text = '';
|
|
62
|
-
let t = start;
|
|
63
|
-
while (t && t !== end) {
|
|
64
|
-
text += t.text;
|
|
65
|
-
t = t.next;
|
|
66
|
-
}
|
|
67
|
-
return text;
|
|
68
|
-
}
|