@cornjs/parser 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 +19 -0
- package/dist/evaluator.js +179 -0
- package/dist/evaluator.test.js +96 -0
- package/dist/index.js +72 -0
- package/dist/lexer.js +634 -0
- package/dist/parser.js +236 -0
- package/dist/utils.js +71 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Corn.JS
|
|
2
|
+
|
|
3
|
+
Native Typescript implementation of the Corn parser.
|
|
4
|
+
|
|
5
|
+
This is compliant with the (currently unfinished) v0.11 spec.
|
|
6
|
+
|
|
7
|
+
>![NOTE]
|
|
8
|
+
> Not yet published to NPM.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { parse } from '<TBD>';
|
|
14
|
+
|
|
15
|
+
const corn = "{ let $foo = 42 } in { value = $foo }";
|
|
16
|
+
const res = parse(corn);
|
|
17
|
+
|
|
18
|
+
if(res.ok) console.log(res.value); // { value: 42 }
|
|
19
|
+
```
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Evaluator = void 0;
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const dedent_1 = __importDefault(require("dedent"));
|
|
9
|
+
class Evaluator {
|
|
10
|
+
inputs = {};
|
|
11
|
+
evaluate(rule) {
|
|
12
|
+
if (rule.assignBlock) {
|
|
13
|
+
const res = this.evaluateAssignBlock(rule.assignBlock);
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
return res;
|
|
16
|
+
}
|
|
17
|
+
const obj = this.evaluateObject(rule.object);
|
|
18
|
+
if (!obj.ok)
|
|
19
|
+
return obj;
|
|
20
|
+
return (0, utils_1.ok)(obj.value);
|
|
21
|
+
}
|
|
22
|
+
evaluateAssignBlock(rule) {
|
|
23
|
+
for (const pair of rule.assignments) {
|
|
24
|
+
const value = this.evaluateValue(pair.value);
|
|
25
|
+
if (!value.ok)
|
|
26
|
+
return value;
|
|
27
|
+
this.inputs[pair.key] = value.value;
|
|
28
|
+
}
|
|
29
|
+
return (0, utils_1.ok)(undefined);
|
|
30
|
+
}
|
|
31
|
+
evaluateObject(rule) {
|
|
32
|
+
const obj = {};
|
|
33
|
+
for (const pair of rule.pairs) {
|
|
34
|
+
switch (pair.type) {
|
|
35
|
+
case "pair": {
|
|
36
|
+
const value = this.evaluateValue(pair.value);
|
|
37
|
+
if (!value.ok)
|
|
38
|
+
return value;
|
|
39
|
+
const res = this.addAtPath(obj, pair.path.value, value.value);
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
return res;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "spread": {
|
|
45
|
+
const value = this.evaluateInput(pair);
|
|
46
|
+
if (!value.ok)
|
|
47
|
+
return value;
|
|
48
|
+
if (!(0, utils_1.isObject)(value.value))
|
|
49
|
+
return (0, utils_1.err)(`input is not an object: ${pair.value}`);
|
|
50
|
+
for (const key in value.value) {
|
|
51
|
+
obj[key] = value.value[key];
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
(0, utils_1.assertNever)(pair);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return (0, utils_1.ok)(obj);
|
|
60
|
+
}
|
|
61
|
+
addAtPath(obj, path, value) {
|
|
62
|
+
for (let i = 0; i < path.length; i++) {
|
|
63
|
+
const key = path[i];
|
|
64
|
+
if (i === path.length - 1) {
|
|
65
|
+
obj[key] = value;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const existingObject = obj[key];
|
|
69
|
+
if (existingObject === undefined) {
|
|
70
|
+
obj[key] = {};
|
|
71
|
+
}
|
|
72
|
+
else if (!(0, utils_1.isObject)(existingObject)) {
|
|
73
|
+
return (0, utils_1.err)(`expected object at path ${path.slice(0, i + 1).join(".")}`);
|
|
74
|
+
}
|
|
75
|
+
obj = obj[key];
|
|
76
|
+
}
|
|
77
|
+
return (0, utils_1.ok)(undefined);
|
|
78
|
+
}
|
|
79
|
+
evaluateArray(rule) {
|
|
80
|
+
const array = [];
|
|
81
|
+
for (const value of rule.values) {
|
|
82
|
+
switch (value.type) {
|
|
83
|
+
case "spread": {
|
|
84
|
+
const inputVal = this.evaluateInput(value);
|
|
85
|
+
if (!inputVal.ok)
|
|
86
|
+
return inputVal;
|
|
87
|
+
if (!Array.isArray(inputVal.value))
|
|
88
|
+
return (0, utils_1.err)(`input is not an array: ${value.value}`);
|
|
89
|
+
array.push(...inputVal.value);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
default: {
|
|
93
|
+
const val = this.evaluateValue(value);
|
|
94
|
+
if (!val.ok)
|
|
95
|
+
return val;
|
|
96
|
+
array.push(val.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return (0, utils_1.ok)(array);
|
|
101
|
+
}
|
|
102
|
+
evaluateString(rule) {
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const part of rule.value) {
|
|
105
|
+
switch (part.type) {
|
|
106
|
+
case "charSequence":
|
|
107
|
+
case "charEscape":
|
|
108
|
+
result.push(part.value);
|
|
109
|
+
break;
|
|
110
|
+
case "unicodeEscape": {
|
|
111
|
+
const MAX = 0x10ffff;
|
|
112
|
+
if (part.value > MAX) {
|
|
113
|
+
return (0, utils_1.err)("invalid code point");
|
|
114
|
+
}
|
|
115
|
+
result.push(String.fromCodePoint(part.value));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "input": {
|
|
119
|
+
const value = this.evaluateInput(part);
|
|
120
|
+
if (!value.ok)
|
|
121
|
+
return value;
|
|
122
|
+
if (typeof value.value !== "string")
|
|
123
|
+
return (0, utils_1.err)(`input is not a string: ${part.value}`);
|
|
124
|
+
result.push(...value.value);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
(0, utils_1.assertNever)(part);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let str = result.join("");
|
|
132
|
+
if (str.includes("\n")) {
|
|
133
|
+
str = this.trimMultilineString(str);
|
|
134
|
+
}
|
|
135
|
+
return (0, utils_1.ok)(str);
|
|
136
|
+
}
|
|
137
|
+
trimMultilineString(str) {
|
|
138
|
+
if (str.startsWith("\r")) {
|
|
139
|
+
str = str.slice(1);
|
|
140
|
+
}
|
|
141
|
+
if (str.startsWith("\n")) {
|
|
142
|
+
str = str.slice(1);
|
|
143
|
+
}
|
|
144
|
+
return dedent_1.default.withOptions({ trimWhitespace: false })(str);
|
|
145
|
+
}
|
|
146
|
+
evaluateValue(rule) {
|
|
147
|
+
switch (rule.type) {
|
|
148
|
+
case "boolean":
|
|
149
|
+
case "integer":
|
|
150
|
+
case "float":
|
|
151
|
+
return (0, utils_1.ok)(rule.value);
|
|
152
|
+
case "input":
|
|
153
|
+
return this.evaluateInput(rule);
|
|
154
|
+
case "string":
|
|
155
|
+
return this.evaluateString(rule);
|
|
156
|
+
case "array":
|
|
157
|
+
return this.evaluateArray(rule);
|
|
158
|
+
case "object":
|
|
159
|
+
return this.evaluateObject(rule);
|
|
160
|
+
case "null":
|
|
161
|
+
return (0, utils_1.ok)(null);
|
|
162
|
+
default:
|
|
163
|
+
(0, utils_1.assertNever)(rule);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
evaluateInput(rule) {
|
|
167
|
+
let value;
|
|
168
|
+
if (rule.value.startsWith("$env_")) {
|
|
169
|
+
value = process.env[rule.value.slice("$env_".length)];
|
|
170
|
+
}
|
|
171
|
+
if (!value) {
|
|
172
|
+
value = this.inputs[rule.value];
|
|
173
|
+
}
|
|
174
|
+
if (!value)
|
|
175
|
+
return (0, utils_1.err)(`input not found: ${rule.value}`);
|
|
176
|
+
return (0, utils_1.ok)(value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.Evaluator = Evaluator;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const glob_1 = require("glob");
|
|
8
|
+
const evaluator_1 = require("./evaluator");
|
|
9
|
+
const lexer_1 = require("./lexer");
|
|
10
|
+
const parser_1 = require("./parser");
|
|
11
|
+
const index_1 = require("./index");
|
|
12
|
+
const positiveCases = glob_1.glob
|
|
13
|
+
.sync("test-suite/corn/**/*.pos.corn")
|
|
14
|
+
.map((p) => p.replaceAll("\\", "/"));
|
|
15
|
+
const negativeCases = glob_1.glob
|
|
16
|
+
.sync("test-suite/corn/**/*.neg.corn")
|
|
17
|
+
.map((p) => p.replaceAll("\\", "/"));
|
|
18
|
+
function makeTree(cases) {
|
|
19
|
+
const testGroups = {};
|
|
20
|
+
cases.forEach(function (path) {
|
|
21
|
+
path
|
|
22
|
+
.replace("test-suite/corn/", "")
|
|
23
|
+
.split("/")
|
|
24
|
+
.reduce((group, section, i, arr) => {
|
|
25
|
+
if (i === arr.length - 1) {
|
|
26
|
+
group[section.replace(".pos.corn", "").replace(".neg.corn", "")] =
|
|
27
|
+
path;
|
|
28
|
+
return group;
|
|
29
|
+
}
|
|
30
|
+
if (!group[section]) {
|
|
31
|
+
group[section] = {};
|
|
32
|
+
}
|
|
33
|
+
return group[section];
|
|
34
|
+
}, testGroups);
|
|
35
|
+
});
|
|
36
|
+
return testGroups;
|
|
37
|
+
}
|
|
38
|
+
function positiveTest(key, value) {
|
|
39
|
+
const jsonPath = value
|
|
40
|
+
.replace("/corn/", "/json/")
|
|
41
|
+
.replace(".pos.corn", ".json");
|
|
42
|
+
const corn = fs_1.default.readFileSync(value, "utf-8").replaceAll("\r\n", "\n"); // CRLF --> LF, thanks Windows
|
|
43
|
+
const jsonFile = fs_1.default.readFileSync(jsonPath, "utf-8");
|
|
44
|
+
const json = JSON.parse(jsonFile);
|
|
45
|
+
test(key, () => {
|
|
46
|
+
const result = (0, index_1.parse)(corn);
|
|
47
|
+
expect(result.ok).toBe(true);
|
|
48
|
+
if (!result.ok)
|
|
49
|
+
return;
|
|
50
|
+
expect(result.value).toEqual(json);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function negativeTest(key, value) {
|
|
54
|
+
const corn = fs_1.default.readFileSync(value, "utf-8").replaceAll("\r\n", "\n"); // CRLF --> LF, thanks Windows
|
|
55
|
+
test(key, () => {
|
|
56
|
+
let hasErr = false;
|
|
57
|
+
let err;
|
|
58
|
+
const tokens = new lexer_1.Lexer().tokenizeInput(corn);
|
|
59
|
+
if (!tokens.ok) {
|
|
60
|
+
hasErr = true;
|
|
61
|
+
err = tokens.error;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const ast = (0, parser_1.parseTokens)(tokens.value.map((t) => t.token));
|
|
65
|
+
if (!ast.ok) {
|
|
66
|
+
hasErr = true;
|
|
67
|
+
err = ast.error;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const evaluator = new evaluator_1.Evaluator();
|
|
71
|
+
const result = evaluator.evaluate(ast.value);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
hasErr = true;
|
|
74
|
+
err = result.error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log(err);
|
|
79
|
+
expect(hasErr).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function evaluate(group, tester) {
|
|
83
|
+
for (const key in group) {
|
|
84
|
+
const value = group[key];
|
|
85
|
+
if (typeof value === "object") {
|
|
86
|
+
describe(key, () => {
|
|
87
|
+
evaluate(value, tester);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
tester(key, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
describe("positive", () => evaluate(makeTree(positiveCases), positiveTest));
|
|
96
|
+
describe("negative", () => evaluate(makeTree(negativeCases), negativeTest));
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parse = parse;
|
|
4
|
+
const lexer_1 = require("./lexer");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const evaluator_1 = require("./evaluator");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
/**
|
|
9
|
+
* Parses the provided corn string
|
|
10
|
+
* into an object.
|
|
11
|
+
*
|
|
12
|
+
* @typeParam T output object type
|
|
13
|
+
* @param corn input string
|
|
14
|
+
* @returns result containing output object on success,
|
|
15
|
+
* or error variant on any failure.
|
|
16
|
+
*/
|
|
17
|
+
function parse(corn) {
|
|
18
|
+
// strip bom
|
|
19
|
+
if (corn.charCodeAt(0) === 0xfeff) {
|
|
20
|
+
corn = corn.slice(1);
|
|
21
|
+
}
|
|
22
|
+
const lexer = new lexer_1.Lexer();
|
|
23
|
+
const tokensRes = lexer.tokenizeInput(corn);
|
|
24
|
+
if (!tokensRes.ok)
|
|
25
|
+
return (0, utils_1.err)(tokensRes.error);
|
|
26
|
+
const tokens = tokensRes.value;
|
|
27
|
+
const astRes = (0, parser_1.parseTokens)(tokens.map((t) => t.token));
|
|
28
|
+
if (!astRes.ok)
|
|
29
|
+
return (0, utils_1.err)(astRes.error);
|
|
30
|
+
const ast = astRes.value;
|
|
31
|
+
const evaluator = new evaluator_1.Evaluator();
|
|
32
|
+
return evaluator.evaluate(ast);
|
|
33
|
+
}
|
|
34
|
+
/* ---- */
|
|
35
|
+
// import fs from "fs";
|
|
36
|
+
// const input = fs
|
|
37
|
+
// .readFileSync("test-suite/corn/input/basic.pos.corn", "utf-8");
|
|
38
|
+
//
|
|
39
|
+
// console.log(input);
|
|
40
|
+
//
|
|
41
|
+
// const tokens = new Lexer().tokenizeInput(input);
|
|
42
|
+
//
|
|
43
|
+
// if(!tokens.ok) {
|
|
44
|
+
// console.error(tokens.error);
|
|
45
|
+
// process.exit(1);
|
|
46
|
+
// }
|
|
47
|
+
//
|
|
48
|
+
// console.log('TOKENS', tokens.value);
|
|
49
|
+
//
|
|
50
|
+
// console.log("\n---------------\n");
|
|
51
|
+
//
|
|
52
|
+
// const ast = parseTokens(tokens.value.map(t => t.token));
|
|
53
|
+
//
|
|
54
|
+
// if(!ast.ok) {
|
|
55
|
+
// console.error(ast.error);
|
|
56
|
+
// process.exit(1);
|
|
57
|
+
// }
|
|
58
|
+
//
|
|
59
|
+
// console.log("\nAST:");
|
|
60
|
+
// console.dir(ast.value, { depth: null });
|
|
61
|
+
//
|
|
62
|
+
// console.log("\n---------------\n");
|
|
63
|
+
//
|
|
64
|
+
// const obj = new Evaluator().evaluate(ast.value);
|
|
65
|
+
//
|
|
66
|
+
// if(!obj.ok) {
|
|
67
|
+
// console.error(obj.error);
|
|
68
|
+
// process.exit(1);
|
|
69
|
+
// }
|
|
70
|
+
//
|
|
71
|
+
// console.log("\nOBJ:");
|
|
72
|
+
// console.dir(obj.value, { depth: null });
|
package/dist/lexer.js
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Lexer = void 0;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const WHITESPACE = "\n\r\t ";
|
|
6
|
+
class Lexer {
|
|
7
|
+
row = 0;
|
|
8
|
+
col = 0;
|
|
9
|
+
MATCHERS = {
|
|
10
|
+
[0 /* State.TopLevel */]: [
|
|
11
|
+
createMatcher(this.matchLiteral("{"), {
|
|
12
|
+
action: "push",
|
|
13
|
+
state: 3 /* State.Object */,
|
|
14
|
+
}),
|
|
15
|
+
createMatcher(this.matchLiteral("let"), {
|
|
16
|
+
action: "push",
|
|
17
|
+
state: 1 /* State.AssignBlock */,
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
[1 /* State.AssignBlock */]: [
|
|
21
|
+
createMatcher(this.matchLiteral("in"), { action: "pop" }),
|
|
22
|
+
createMatcher(this.matchLiteral("{")),
|
|
23
|
+
createMatcher(this.matchLiteral("}")),
|
|
24
|
+
createMatcher(this.matchInput.bind(this)),
|
|
25
|
+
createMatcher(this.matchLiteral("="), {
|
|
26
|
+
action: "push",
|
|
27
|
+
state: 2 /* State.Value */,
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
[2 /* State.Value */]: [
|
|
31
|
+
createMatcher(this.matchLiteral("{"), {
|
|
32
|
+
action: "replace",
|
|
33
|
+
state: 3 /* State.Object */,
|
|
34
|
+
}),
|
|
35
|
+
createMatcher(this.matchLiteral("["), {
|
|
36
|
+
action: "replace",
|
|
37
|
+
state: 4 /* State.Array */,
|
|
38
|
+
}),
|
|
39
|
+
createMatcher(this.matchLiteral("true"), { action: "pop" }, [
|
|
40
|
+
...WHITESPACE,
|
|
41
|
+
"}",
|
|
42
|
+
]),
|
|
43
|
+
createMatcher(this.matchLiteral("false"), { action: "pop" }, [
|
|
44
|
+
...WHITESPACE,
|
|
45
|
+
"}",
|
|
46
|
+
]),
|
|
47
|
+
createMatcher(this.matchLiteral("null"), { action: "pop" }, [
|
|
48
|
+
...WHITESPACE,
|
|
49
|
+
"}",
|
|
50
|
+
]),
|
|
51
|
+
createMatcher(this.matchLiteral('"'), {
|
|
52
|
+
action: "replace",
|
|
53
|
+
state: 5 /* State.String */,
|
|
54
|
+
}),
|
|
55
|
+
createMatcher(this.matchInput.bind(this), { action: "pop" }, [
|
|
56
|
+
...WHITESPACE,
|
|
57
|
+
"}",
|
|
58
|
+
]),
|
|
59
|
+
createMatcher(this.matchFloat.bind(this), { action: "pop" }, [
|
|
60
|
+
...WHITESPACE,
|
|
61
|
+
"}",
|
|
62
|
+
]),
|
|
63
|
+
createMatcher(this.matchInteger.bind(this), { action: "pop" }, [
|
|
64
|
+
...WHITESPACE,
|
|
65
|
+
"}",
|
|
66
|
+
]),
|
|
67
|
+
createMatcher(this.matchLiteral("]"), { action: "pop" }),
|
|
68
|
+
],
|
|
69
|
+
[3 /* State.Object */]: [
|
|
70
|
+
createMatcher(this.matchLiteral("}"), { action: "pop" }),
|
|
71
|
+
createMatcher(this.matchLiteral("="), {
|
|
72
|
+
action: "push",
|
|
73
|
+
state: 2 /* State.Value */,
|
|
74
|
+
}),
|
|
75
|
+
createMatcher(this.matchLiteral("..")),
|
|
76
|
+
createMatcher(this.matchLiteral(".")),
|
|
77
|
+
createMatcher(this.matchInput.bind(this)),
|
|
78
|
+
createMatcher(this.matchQuotedPathSegment.bind(this)),
|
|
79
|
+
createMatcher(this.matchPathSegment.bind(this)),
|
|
80
|
+
],
|
|
81
|
+
[4 /* State.Array */]: [
|
|
82
|
+
createMatcher(this.matchLiteral("]"), { action: "pop" }),
|
|
83
|
+
createMatcher(this.matchLiteral("{"), {
|
|
84
|
+
action: "push",
|
|
85
|
+
state: 3 /* State.Object */,
|
|
86
|
+
}),
|
|
87
|
+
createMatcher(this.matchLiteral("["), {
|
|
88
|
+
action: "push",
|
|
89
|
+
state: 4 /* State.Array */,
|
|
90
|
+
}),
|
|
91
|
+
createMatcher(this.matchLiteral('"'), {
|
|
92
|
+
action: "push",
|
|
93
|
+
state: 5 /* State.String */,
|
|
94
|
+
}),
|
|
95
|
+
createMatcher(this.matchLiteral("..")),
|
|
96
|
+
createMatcher(this.matchLiteral("true"), undefined, [...WHITESPACE, "]"]),
|
|
97
|
+
createMatcher(this.matchLiteral("false"), undefined, [
|
|
98
|
+
...WHITESPACE,
|
|
99
|
+
"]",
|
|
100
|
+
]),
|
|
101
|
+
createMatcher(this.matchLiteral("null"), undefined, [...WHITESPACE, "]"]),
|
|
102
|
+
createMatcher(this.matchInput.bind(this), undefined, [
|
|
103
|
+
...WHITESPACE,
|
|
104
|
+
"]",
|
|
105
|
+
]),
|
|
106
|
+
createMatcher(this.matchFloat.bind(this), undefined, [
|
|
107
|
+
...WHITESPACE,
|
|
108
|
+
"]",
|
|
109
|
+
]),
|
|
110
|
+
createMatcher(this.matchInteger.bind(this), undefined, [
|
|
111
|
+
...WHITESPACE,
|
|
112
|
+
"]",
|
|
113
|
+
]),
|
|
114
|
+
],
|
|
115
|
+
[5 /* State.String */]: [
|
|
116
|
+
createMatcher(this.matchLiteral('"'), { action: "pop" }),
|
|
117
|
+
createMatcher(this.matchInterpolatedInput.bind(this)),
|
|
118
|
+
createMatcher(this.matchCharEscape.bind(this)),
|
|
119
|
+
createMatcher(this.matchCharSequence.bind(this)),
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
tokenizeInput(inputString) {
|
|
123
|
+
const input = inputString.split("");
|
|
124
|
+
const tokens = [];
|
|
125
|
+
const state = [0 /* State.TopLevel */];
|
|
126
|
+
while (input.length > 0) {
|
|
127
|
+
const currLength = input.length;
|
|
128
|
+
const currentState = state[state.length - 1];
|
|
129
|
+
const char = input[0];
|
|
130
|
+
if (!char)
|
|
131
|
+
return (0, utils_1.err)({
|
|
132
|
+
message: "lexer error - expected char",
|
|
133
|
+
span: this.getAbsoluteSpan({ startCol: 0, endCol: 0, endRow: 0 }),
|
|
134
|
+
});
|
|
135
|
+
if (currentState !== 5 /* State.String */ && WHITESPACE.includes(char)) {
|
|
136
|
+
if (char === "\n") {
|
|
137
|
+
this.row++;
|
|
138
|
+
this.col = 0;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.col++;
|
|
142
|
+
}
|
|
143
|
+
input.shift();
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (currentState !== 5 /* State.String */ && take(input, "/", "/")) {
|
|
147
|
+
takeWhile(input, (char) => char !== "\n");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const matchers = this.MATCHERS[currentState];
|
|
151
|
+
let hasMatch = false;
|
|
152
|
+
for (const matcher of matchers) {
|
|
153
|
+
const match = matcher.func(input);
|
|
154
|
+
if (!match.ok)
|
|
155
|
+
return match;
|
|
156
|
+
if (!match.value)
|
|
157
|
+
continue;
|
|
158
|
+
hasMatch = true;
|
|
159
|
+
tokens.push(match.value);
|
|
160
|
+
this.setSpan(match.value.span);
|
|
161
|
+
switch (matcher.stateChange.action) {
|
|
162
|
+
case "push":
|
|
163
|
+
state.push(matcher.stateChange.state);
|
|
164
|
+
break;
|
|
165
|
+
case "replace":
|
|
166
|
+
state[state.length - 1] = matcher.stateChange.state;
|
|
167
|
+
break;
|
|
168
|
+
case "pop":
|
|
169
|
+
state.pop();
|
|
170
|
+
break;
|
|
171
|
+
case "none":
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
(0, utils_1.assertNever)(matcher.stateChange);
|
|
175
|
+
}
|
|
176
|
+
// some tokens cannot be next to other tokens
|
|
177
|
+
// (for example, to prevent `[truefalsenull]` in an array).
|
|
178
|
+
if (matcher.requiredChars.length) {
|
|
179
|
+
const nextChar = input[0];
|
|
180
|
+
if (!matcher.requiredChars.includes(nextChar)) {
|
|
181
|
+
return (0, utils_1.err)({
|
|
182
|
+
message: `expected whitespace after ${match.value.token.type}`,
|
|
183
|
+
span: match.value.span,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (!hasMatch) {
|
|
190
|
+
return (0, utils_1.err)({
|
|
191
|
+
message: `Unexpected token: ${char}`,
|
|
192
|
+
span: this.getAbsoluteSpan({
|
|
193
|
+
startCol: 0,
|
|
194
|
+
endCol: 1,
|
|
195
|
+
endRow: 0,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (input.length === currLength) {
|
|
200
|
+
throw new Error(`no match for char ${char} in state ${currentState} - input length is unchanged. This is a lexer bug!`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return (0, utils_1.ok)(tokens);
|
|
204
|
+
}
|
|
205
|
+
matchPathSegment(input) {
|
|
206
|
+
const value = takeWhile(input, (char) => !WHITESPACE.includes(char) &&
|
|
207
|
+
char !== "." &&
|
|
208
|
+
char !== "=" &&
|
|
209
|
+
char !== "'" &&
|
|
210
|
+
char !== '"');
|
|
211
|
+
if (value.length) {
|
|
212
|
+
const token = { type: "pathSegment", value: value.join("") };
|
|
213
|
+
return (0, utils_1.ok)({
|
|
214
|
+
token,
|
|
215
|
+
span: this.getAbsoluteSpan({
|
|
216
|
+
startCol: 0,
|
|
217
|
+
endCol: value.length,
|
|
218
|
+
endRow: 0,
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return (0, utils_1.ok)(undefined);
|
|
223
|
+
}
|
|
224
|
+
matchQuotedPathSegment(input) {
|
|
225
|
+
if (!take(input, "'"))
|
|
226
|
+
return (0, utils_1.ok)(undefined);
|
|
227
|
+
let escaping = false;
|
|
228
|
+
const value = takeWhile(input, (char) => {
|
|
229
|
+
if (char === "\\")
|
|
230
|
+
escaping = true;
|
|
231
|
+
if (char === "'" && !escaping)
|
|
232
|
+
return false;
|
|
233
|
+
if (char === "'" && escaping) {
|
|
234
|
+
escaping = false;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return char !== "'";
|
|
238
|
+
});
|
|
239
|
+
if (!take(input, "'")) {
|
|
240
|
+
return (0, utils_1.err)({
|
|
241
|
+
message: "expected closing quote",
|
|
242
|
+
span: this.getAbsoluteSpan({
|
|
243
|
+
startCol: 0,
|
|
244
|
+
endCol: value.length,
|
|
245
|
+
endRow: 0,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const token = {
|
|
250
|
+
type: "pathSegment",
|
|
251
|
+
value: value.filter((c) => c !== "\\").join(""),
|
|
252
|
+
};
|
|
253
|
+
return (0, utils_1.ok)({
|
|
254
|
+
token,
|
|
255
|
+
span: this.getAbsoluteSpan({
|
|
256
|
+
startCol: 0,
|
|
257
|
+
endCol: value.length + 1,
|
|
258
|
+
endRow: 0,
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
matchInteger(input) {
|
|
263
|
+
if (input.length < 2)
|
|
264
|
+
return (0, utils_1.err)({
|
|
265
|
+
message: "unexpected end of input",
|
|
266
|
+
span: this.getAbsoluteSpan({
|
|
267
|
+
startCol: 0,
|
|
268
|
+
endCol: 1,
|
|
269
|
+
endRow: 0,
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
const isNegative = take(input, "-");
|
|
273
|
+
let base = 10;
|
|
274
|
+
let matcher = utils_1.isDigit;
|
|
275
|
+
switch (true) {
|
|
276
|
+
case !!take(input, "0", "b"):
|
|
277
|
+
base = 2;
|
|
278
|
+
matcher = (char) => char === "0" || char === "1";
|
|
279
|
+
break;
|
|
280
|
+
case !!take(input, "0", "x"):
|
|
281
|
+
base = 16;
|
|
282
|
+
matcher = utils_1.isHexDigit;
|
|
283
|
+
break;
|
|
284
|
+
case !!take(input, "0", "o"):
|
|
285
|
+
base = 8;
|
|
286
|
+
matcher = (char) => char >= "0" && char <= "7";
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
const value = [];
|
|
290
|
+
let char = input[0];
|
|
291
|
+
while (char && (matcher(char) || char === "_")) {
|
|
292
|
+
const match = takeWhile(input, matcher);
|
|
293
|
+
if (!match.length)
|
|
294
|
+
return (0, utils_1.ok)(undefined);
|
|
295
|
+
value.push(...match);
|
|
296
|
+
char = input[0];
|
|
297
|
+
take(input, "_");
|
|
298
|
+
}
|
|
299
|
+
if (value.length) {
|
|
300
|
+
let num = Number.parseInt(value.join(""), base);
|
|
301
|
+
if (!Number.isSafeInteger(num)) {
|
|
302
|
+
return (0, utils_1.err)({
|
|
303
|
+
message: "integer out of range",
|
|
304
|
+
span: this.getAbsoluteSpan({
|
|
305
|
+
startCol: 0,
|
|
306
|
+
endCol: value.length,
|
|
307
|
+
endRow: 0,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (Number.isNaN(num))
|
|
312
|
+
return (0, utils_1.err)({
|
|
313
|
+
message: "invalid integer",
|
|
314
|
+
span: this.getAbsoluteSpan({
|
|
315
|
+
startCol: 0,
|
|
316
|
+
endCol: value.length,
|
|
317
|
+
endRow: 0,
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
if (isNegative)
|
|
321
|
+
num *= -1;
|
|
322
|
+
const token = { type: "integer", value: num };
|
|
323
|
+
return (0, utils_1.ok)({
|
|
324
|
+
token,
|
|
325
|
+
span: this.getAbsoluteSpan({
|
|
326
|
+
startCol: 0,
|
|
327
|
+
endCol: value.length,
|
|
328
|
+
endRow: 0,
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return (0, utils_1.ok)(undefined);
|
|
333
|
+
}
|
|
334
|
+
matchFloat(input) {
|
|
335
|
+
if (input.length < 2) {
|
|
336
|
+
return (0, utils_1.err)({
|
|
337
|
+
message: "unexpected end of input",
|
|
338
|
+
span: this.getAbsoluteSpan({
|
|
339
|
+
startCol: 0,
|
|
340
|
+
endCol: 1,
|
|
341
|
+
endRow: 0,
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const isNegative = peek(input, "-");
|
|
346
|
+
const negOffset = isNegative ? 1 : 0;
|
|
347
|
+
const intPartChars = peekWhile(input.slice(negOffset), utils_1.isDigit);
|
|
348
|
+
if (input[intPartChars.length + negOffset] !== ".")
|
|
349
|
+
return (0, utils_1.ok)(undefined);
|
|
350
|
+
input.splice(0, intPartChars.length + negOffset + 1);
|
|
351
|
+
const floatPartChars = takeWhile(input, utils_1.isDigit);
|
|
352
|
+
let exponent = "";
|
|
353
|
+
if (take(input, "e")) {
|
|
354
|
+
exponent += "e";
|
|
355
|
+
const expSign = take(input, "+") || take(input, "-");
|
|
356
|
+
if (!expSign) {
|
|
357
|
+
return (0, utils_1.err)({
|
|
358
|
+
message: "expected one of `+` or `-`",
|
|
359
|
+
span: this.getAbsoluteSpan({
|
|
360
|
+
startCol: intPartChars.length + floatPartChars.length + 1,
|
|
361
|
+
endCol: intPartChars.length + floatPartChars.length + 2,
|
|
362
|
+
endRow: 0,
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
exponent += expSign;
|
|
367
|
+
const expPartChars = takeWhile(input, utils_1.isDigit);
|
|
368
|
+
exponent += expPartChars.join("");
|
|
369
|
+
}
|
|
370
|
+
const fullString = `${intPartChars.join("")}.${floatPartChars.join("")}${exponent}`;
|
|
371
|
+
let num = parseFloat(fullString);
|
|
372
|
+
if (Number.isNaN(num)) {
|
|
373
|
+
return (0, utils_1.err)({
|
|
374
|
+
message: "invalid float",
|
|
375
|
+
span: this.getAbsoluteSpan({
|
|
376
|
+
startCol: 0,
|
|
377
|
+
endCol: fullString.length,
|
|
378
|
+
endRow: 0,
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (isNegative)
|
|
383
|
+
num *= -1;
|
|
384
|
+
const token = { type: "float", value: num };
|
|
385
|
+
return (0, utils_1.ok)({
|
|
386
|
+
token,
|
|
387
|
+
span: this.getAbsoluteSpan({
|
|
388
|
+
startCol: 0,
|
|
389
|
+
endCol: fullString.length,
|
|
390
|
+
endRow: 0,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
matchInterpolatedInput(input) {
|
|
395
|
+
if (!take(input, "$", "{"))
|
|
396
|
+
return (0, utils_1.ok)(undefined);
|
|
397
|
+
const name = "$" +
|
|
398
|
+
takeWhile(input, (char) => (0, utils_1.isAlphanumeric)(char) || char === "_").join("");
|
|
399
|
+
if (!take(input, "}"))
|
|
400
|
+
return (0, utils_1.ok)(undefined);
|
|
401
|
+
const token = { type: "input", value: name };
|
|
402
|
+
return (0, utils_1.ok)({
|
|
403
|
+
token,
|
|
404
|
+
span: this.getAbsoluteSpan({
|
|
405
|
+
startCol: 0,
|
|
406
|
+
endCol: name.length + 2,
|
|
407
|
+
endRow: 0,
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
matchCharEscape(input) {
|
|
412
|
+
if (!take(input, "\\"))
|
|
413
|
+
return (0, utils_1.ok)(undefined);
|
|
414
|
+
const span = this.getAbsoluteSpan({ startCol: 0, endCol: 2, endRow: 0 });
|
|
415
|
+
switch (input.shift()) {
|
|
416
|
+
case "n":
|
|
417
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: "\n" }, span });
|
|
418
|
+
case "r":
|
|
419
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: "\r" }, span });
|
|
420
|
+
case "t":
|
|
421
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: "\t" }, span });
|
|
422
|
+
case "\\":
|
|
423
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: "\\" }, span });
|
|
424
|
+
case '"':
|
|
425
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: '"' }, span });
|
|
426
|
+
case "$":
|
|
427
|
+
return (0, utils_1.ok)({ token: { type: "charEscape", value: "$" }, span });
|
|
428
|
+
case "u":
|
|
429
|
+
return this.matchUnicodeEscape(input);
|
|
430
|
+
default:
|
|
431
|
+
return (0, utils_1.err)({ message: "invalid char escape", span });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
matchUnicodeEscape(input) {
|
|
435
|
+
// since this runs effectively after charEscape,
|
|
436
|
+
// we only need to start matching from the brace.
|
|
437
|
+
const SPAN_ROW_OFFSET = 2;
|
|
438
|
+
if (!take(input, "{")) {
|
|
439
|
+
return (0, utils_1.err)({
|
|
440
|
+
message: "expected {",
|
|
441
|
+
span: this.getAbsoluteSpan({
|
|
442
|
+
startCol: SPAN_ROW_OFFSET,
|
|
443
|
+
endCol: SPAN_ROW_OFFSET + 1,
|
|
444
|
+
endRow: 0,
|
|
445
|
+
}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const hex = takeWhile(input, utils_1.isHexDigit);
|
|
449
|
+
if (!take(input, "}")) {
|
|
450
|
+
return (0, utils_1.err)({
|
|
451
|
+
message: "expected }",
|
|
452
|
+
span: this.getAbsoluteSpan({
|
|
453
|
+
startCol: SPAN_ROW_OFFSET + hex.length,
|
|
454
|
+
endCol: SPAN_ROW_OFFSET + hex.length + 1,
|
|
455
|
+
endRow: 0,
|
|
456
|
+
}),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (!hex.length) {
|
|
460
|
+
return (0, utils_1.err)({
|
|
461
|
+
message: "unicode escape cannot be empty",
|
|
462
|
+
span: this.getAbsoluteSpan({
|
|
463
|
+
startCol: SPAN_ROW_OFFSET,
|
|
464
|
+
endCol: SPAN_ROW_OFFSET + 2,
|
|
465
|
+
endRow: 0,
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const value = parseInt(hex.join(""), 16);
|
|
470
|
+
return (0, utils_1.ok)({
|
|
471
|
+
token: { type: "unicodeEscape", value },
|
|
472
|
+
span: this.getAbsoluteSpan({
|
|
473
|
+
startCol: 0,
|
|
474
|
+
endCol: hex.length + 4,
|
|
475
|
+
endRow: 0,
|
|
476
|
+
}),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
matchCharSequence(input) {
|
|
480
|
+
const value = takeWhile(input, (char, next) => char !== '"' && char !== "\\" && (char !== "$" || next !== "{"));
|
|
481
|
+
if (value.length) {
|
|
482
|
+
const rows = value.filter((c) => c === "\n").length;
|
|
483
|
+
const lastColumnIndex = value.lastIndexOf("\n") + 1;
|
|
484
|
+
const endColumn = value.length - lastColumnIndex;
|
|
485
|
+
const token = { type: "charSequence", value: value.join("") };
|
|
486
|
+
return (0, utils_1.ok)({
|
|
487
|
+
token: token,
|
|
488
|
+
span: this.getAbsoluteSpan({
|
|
489
|
+
startCol: 0,
|
|
490
|
+
endCol: endColumn,
|
|
491
|
+
endRow: rows,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return (0, utils_1.ok)(undefined);
|
|
496
|
+
}
|
|
497
|
+
matchInput(input) {
|
|
498
|
+
if (!take(input, "$"))
|
|
499
|
+
return (0, utils_1.ok)(undefined);
|
|
500
|
+
if (!(0, utils_1.isLetter)(input[0]) && input[0] !== "_")
|
|
501
|
+
return (0, utils_1.ok)(undefined);
|
|
502
|
+
const name = "$" +
|
|
503
|
+
takeWhile(input, (char) => (0, utils_1.isAlphanumeric)(char) || char === "_").join("");
|
|
504
|
+
const token = { type: "input", value: name };
|
|
505
|
+
return (0, utils_1.ok)({
|
|
506
|
+
token,
|
|
507
|
+
span: this.getAbsoluteSpan({
|
|
508
|
+
startCol: 0,
|
|
509
|
+
endCol: name.length,
|
|
510
|
+
endRow: 0,
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Returns a matcher function for a literal token.
|
|
516
|
+
|
|
517
|
+
* @param str Literal token value.
|
|
518
|
+
* @private
|
|
519
|
+
*/
|
|
520
|
+
matchLiteral(str) {
|
|
521
|
+
return (input) => {
|
|
522
|
+
if (take(input, ...str)) {
|
|
523
|
+
return (0, utils_1.ok)({
|
|
524
|
+
token: { type: str },
|
|
525
|
+
span: this.getAbsoluteSpan({
|
|
526
|
+
startCol: 0,
|
|
527
|
+
endCol: str.length,
|
|
528
|
+
endRow: 0,
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return (0, utils_1.ok)(undefined);
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Applies a relative span to the current row/col
|
|
537
|
+
* to get an absolute span.
|
|
538
|
+
*
|
|
539
|
+
* @param relativeSpan
|
|
540
|
+
* @private
|
|
541
|
+
*/
|
|
542
|
+
getAbsoluteSpan(relativeSpan) {
|
|
543
|
+
const endCol = relativeSpan.endRow > 1 ? 0 : this.col;
|
|
544
|
+
return {
|
|
545
|
+
startCol: this.col + relativeSpan.startCol,
|
|
546
|
+
startRow: this.row,
|
|
547
|
+
endCol: endCol + relativeSpan.endCol,
|
|
548
|
+
endRow: this.row + relativeSpan.endRow,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
setSpan(span) {
|
|
552
|
+
this.row = span.endRow;
|
|
553
|
+
this.col = span.endCol;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
exports.Lexer = Lexer;
|
|
557
|
+
/**
|
|
558
|
+
* Creates a new matcher object.
|
|
559
|
+
* @param func
|
|
560
|
+
* @param stateChange
|
|
561
|
+
* @param requiredChars
|
|
562
|
+
*/
|
|
563
|
+
function createMatcher(func, stateChange, requiredChars = []) {
|
|
564
|
+
return {
|
|
565
|
+
func,
|
|
566
|
+
stateChange: stateChange ?? { action: "none" },
|
|
567
|
+
requiredChars,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Checks if the provided chars are present at the start of the input.
|
|
572
|
+
* Characters are not consumed.
|
|
573
|
+
*
|
|
574
|
+
* @param input Input chars
|
|
575
|
+
* @param chars Check chars, in order.
|
|
576
|
+
* @returns Whether chars exist at start of input.
|
|
577
|
+
*/
|
|
578
|
+
function peek(input, ...chars) {
|
|
579
|
+
for (let i = 0; i < chars.length; i++) {
|
|
580
|
+
if (input[i] !== chars[i])
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Like {@link takeWhile},
|
|
587
|
+
* but does not take the characters from `input`.
|
|
588
|
+
*
|
|
589
|
+
* @param input Input chars
|
|
590
|
+
* @param predicate Check function.
|
|
591
|
+
*/
|
|
592
|
+
function peekWhile(input, predicate) {
|
|
593
|
+
const result = [];
|
|
594
|
+
let char = input[0];
|
|
595
|
+
let i = 0;
|
|
596
|
+
while (predicate(input[i], input[i + 1]) && i < input.length) {
|
|
597
|
+
result.push(char);
|
|
598
|
+
char = input[++i];
|
|
599
|
+
}
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Attempts to take `chars` from the start of `input`.
|
|
604
|
+
* If any of `chars` is not present, returns `false` and does nothing.
|
|
605
|
+
*
|
|
606
|
+
* If all `chars` are present, they are removed from the front of `input`
|
|
607
|
+
* and returned.
|
|
608
|
+
*
|
|
609
|
+
* @param input Input chars
|
|
610
|
+
* @param chars chars to take, in order
|
|
611
|
+
* @returns taken chars, or `false`.
|
|
612
|
+
*/
|
|
613
|
+
function take(input, ...chars) {
|
|
614
|
+
const res = peek(input, ...chars);
|
|
615
|
+
if (res)
|
|
616
|
+
input.splice(0, chars.length);
|
|
617
|
+
return res ? chars : false;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Takes chars from the start of `input`
|
|
621
|
+
* for as long as `predicate` is true.
|
|
622
|
+
*
|
|
623
|
+
* The predicate function takes the current and next char,
|
|
624
|
+
* and returns a boolean.
|
|
625
|
+
*
|
|
626
|
+
* @param input Input chars.
|
|
627
|
+
* @param predicate Check function.
|
|
628
|
+
*/
|
|
629
|
+
function takeWhile(input, predicate) {
|
|
630
|
+
const res = peekWhile(input, predicate);
|
|
631
|
+
if (res.length)
|
|
632
|
+
input.splice(0, res.length);
|
|
633
|
+
return res;
|
|
634
|
+
}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTokens = parseTokens;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
/**
|
|
6
|
+
* Attempts to parse the provided token array
|
|
7
|
+
* into an AST of rules.
|
|
8
|
+
*
|
|
9
|
+
* @param tokens The token array.
|
|
10
|
+
* @returns A result containing the AST if successfully parsed,
|
|
11
|
+
* or an error message if not.
|
|
12
|
+
*/
|
|
13
|
+
function parseTokens(tokens) {
|
|
14
|
+
if (tokens.length === 0)
|
|
15
|
+
return (0, utils_1.err)("no tokens");
|
|
16
|
+
const token = tokens[0];
|
|
17
|
+
let assignBlock;
|
|
18
|
+
if (token.type === "let") {
|
|
19
|
+
assignBlock = parseAssignBlock(tokens);
|
|
20
|
+
if (!assignBlock.ok)
|
|
21
|
+
return assignBlock;
|
|
22
|
+
}
|
|
23
|
+
const object = parseObject(tokens);
|
|
24
|
+
if (!object.ok)
|
|
25
|
+
return object;
|
|
26
|
+
return (0, utils_1.ok)({
|
|
27
|
+
type: "config",
|
|
28
|
+
assignBlock: assignBlock?.value,
|
|
29
|
+
object: object.value,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function parseAssignBlock(tokens) {
|
|
33
|
+
let lit = consumeLiteral(tokens, "let");
|
|
34
|
+
if (!lit.ok)
|
|
35
|
+
return lit;
|
|
36
|
+
lit = consumeLiteral(tokens, "{");
|
|
37
|
+
if (!lit.ok)
|
|
38
|
+
return lit;
|
|
39
|
+
const rule = { type: "assignBlock", assignments: [] };
|
|
40
|
+
while (tokens[0].type !== "}") {
|
|
41
|
+
const name = tokens.shift();
|
|
42
|
+
if (name?.type !== "input")
|
|
43
|
+
return (0, utils_1.err)("expected input declaration");
|
|
44
|
+
lit = consumeLiteral(tokens, "=");
|
|
45
|
+
if (!lit.ok)
|
|
46
|
+
return lit;
|
|
47
|
+
const value = parseValue(tokens);
|
|
48
|
+
if (!value.ok)
|
|
49
|
+
return value;
|
|
50
|
+
rule.assignments.push({
|
|
51
|
+
type: "assignment",
|
|
52
|
+
key: name.value,
|
|
53
|
+
value: value.value,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
lit = consumeLiteral(tokens, "}");
|
|
57
|
+
if (!lit.ok)
|
|
58
|
+
return lit;
|
|
59
|
+
lit = consumeLiteral(tokens, "in");
|
|
60
|
+
if (!lit.ok)
|
|
61
|
+
return lit;
|
|
62
|
+
return (0, utils_1.ok)(rule);
|
|
63
|
+
}
|
|
64
|
+
function parseObject(tokens) {
|
|
65
|
+
let lit = consumeLiteral(tokens, "{");
|
|
66
|
+
if (!lit.ok)
|
|
67
|
+
return lit;
|
|
68
|
+
const object = { type: "object", pairs: [] };
|
|
69
|
+
let token = tokens[0];
|
|
70
|
+
while (token && token.type !== "}") {
|
|
71
|
+
switch (token.type) {
|
|
72
|
+
case "..": {
|
|
73
|
+
const spread = parseSpread(tokens);
|
|
74
|
+
if (!spread.ok)
|
|
75
|
+
return spread;
|
|
76
|
+
object.pairs.push(spread.value);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "pathSegment": {
|
|
80
|
+
const pair = parsePair(tokens);
|
|
81
|
+
if (!pair.ok)
|
|
82
|
+
return pair;
|
|
83
|
+
object.pairs.push(pair.value);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return (0, utils_1.err)("expected `..` or path segment, got: " + token.type);
|
|
88
|
+
}
|
|
89
|
+
token = tokens[0];
|
|
90
|
+
}
|
|
91
|
+
lit = consumeLiteral(tokens, "}");
|
|
92
|
+
if (!lit.ok)
|
|
93
|
+
return lit;
|
|
94
|
+
return (0, utils_1.ok)(object);
|
|
95
|
+
}
|
|
96
|
+
function parsePair(tokens) {
|
|
97
|
+
if (tokens.length < 3)
|
|
98
|
+
return (0, utils_1.err)("not enough tokens");
|
|
99
|
+
const path = parsePath(tokens);
|
|
100
|
+
if (!path.ok)
|
|
101
|
+
return path;
|
|
102
|
+
const lit = consumeLiteral(tokens, "=");
|
|
103
|
+
if (!lit.ok)
|
|
104
|
+
return lit;
|
|
105
|
+
const value = parseValue(tokens);
|
|
106
|
+
if (!value.ok)
|
|
107
|
+
return value;
|
|
108
|
+
return (0, utils_1.ok)({ type: "pair", path: path.value, value: value.value });
|
|
109
|
+
}
|
|
110
|
+
function parsePath(tokens) {
|
|
111
|
+
const rule = { type: "path", value: [] };
|
|
112
|
+
const token = tokens.shift();
|
|
113
|
+
if (token?.type !== "pathSegment")
|
|
114
|
+
return (0, utils_1.err)("expected path segment");
|
|
115
|
+
rule.value.push(token.value);
|
|
116
|
+
while (tokens[0].type !== "=") {
|
|
117
|
+
const lit = consumeLiteral(tokens, ".");
|
|
118
|
+
if (!lit.ok)
|
|
119
|
+
return lit;
|
|
120
|
+
const token = tokens.shift();
|
|
121
|
+
if (token?.type !== "pathSegment")
|
|
122
|
+
return (0, utils_1.err)("expected path segment");
|
|
123
|
+
rule.value.push(token.value);
|
|
124
|
+
}
|
|
125
|
+
return (0, utils_1.ok)(rule);
|
|
126
|
+
}
|
|
127
|
+
function parseValue(tokens) {
|
|
128
|
+
const token = tokens[0];
|
|
129
|
+
if (!token)
|
|
130
|
+
return (0, utils_1.err)("expected value");
|
|
131
|
+
switch (token.type) {
|
|
132
|
+
case "input":
|
|
133
|
+
case "integer":
|
|
134
|
+
case "float":
|
|
135
|
+
tokens.shift();
|
|
136
|
+
return (0, utils_1.ok)(token);
|
|
137
|
+
case "true":
|
|
138
|
+
tokens.shift();
|
|
139
|
+
return (0, utils_1.ok)({ type: "boolean", value: true });
|
|
140
|
+
case "false":
|
|
141
|
+
tokens.shift();
|
|
142
|
+
return (0, utils_1.ok)({ type: "boolean", value: false });
|
|
143
|
+
case "null":
|
|
144
|
+
tokens.shift();
|
|
145
|
+
return (0, utils_1.ok)({ type: "null" });
|
|
146
|
+
case '"':
|
|
147
|
+
return parseString(tokens);
|
|
148
|
+
case "{":
|
|
149
|
+
return parseObject(tokens);
|
|
150
|
+
case "[":
|
|
151
|
+
return parseArray(tokens);
|
|
152
|
+
default:
|
|
153
|
+
return (0, utils_1.err)("expected value token, got: " + token.type);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function parseString(tokens) {
|
|
157
|
+
let lit = consumeLiteral(tokens, '"');
|
|
158
|
+
if (!lit.ok)
|
|
159
|
+
return lit;
|
|
160
|
+
const rule = { type: "string", value: [] };
|
|
161
|
+
let token = tokens[0];
|
|
162
|
+
while (token && token.type !== '"') {
|
|
163
|
+
switch (token.type) {
|
|
164
|
+
case "charSequence":
|
|
165
|
+
case "charEscape":
|
|
166
|
+
case "unicodeEscape":
|
|
167
|
+
case "input":
|
|
168
|
+
tokens.shift();
|
|
169
|
+
rule.value.push(token);
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
return (0, utils_1.err)("expected one of {char sequence, char escape, unicode escape, input}, got: " +
|
|
173
|
+
token.type);
|
|
174
|
+
}
|
|
175
|
+
token = tokens[0];
|
|
176
|
+
}
|
|
177
|
+
lit = consumeLiteral(tokens, '"');
|
|
178
|
+
if (!lit.ok)
|
|
179
|
+
return lit;
|
|
180
|
+
return (0, utils_1.ok)(rule);
|
|
181
|
+
}
|
|
182
|
+
function parseArray(tokens) {
|
|
183
|
+
let lit = consumeLiteral(tokens, "[");
|
|
184
|
+
if (!lit.ok)
|
|
185
|
+
return lit;
|
|
186
|
+
const rule = { type: "array", values: [] };
|
|
187
|
+
let token = tokens[0];
|
|
188
|
+
while (token.type !== "]") {
|
|
189
|
+
switch (token.type) {
|
|
190
|
+
case "..": {
|
|
191
|
+
const spread = parseSpread(tokens);
|
|
192
|
+
if (!spread.ok)
|
|
193
|
+
return spread;
|
|
194
|
+
rule.values.push(spread.value);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
default: {
|
|
198
|
+
const value = parseValue(tokens);
|
|
199
|
+
if (!value.ok)
|
|
200
|
+
return value;
|
|
201
|
+
rule.values.push(value.value);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
token = tokens[0];
|
|
206
|
+
}
|
|
207
|
+
lit = consumeLiteral(tokens, "]");
|
|
208
|
+
if (!lit.ok)
|
|
209
|
+
return lit;
|
|
210
|
+
return (0, utils_1.ok)(rule);
|
|
211
|
+
}
|
|
212
|
+
function parseSpread(tokens) {
|
|
213
|
+
const lit = consumeLiteral(tokens, "..");
|
|
214
|
+
if (!lit.ok)
|
|
215
|
+
return lit;
|
|
216
|
+
const input = tokens.shift();
|
|
217
|
+
if (input?.type !== "input")
|
|
218
|
+
return (0, utils_1.err)("expected input");
|
|
219
|
+
return (0, utils_1.ok)({ type: "spread", value: input.value });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Consumes a literal token,
|
|
223
|
+
* returning an error if is not present.
|
|
224
|
+
*
|
|
225
|
+
* @param tokens Token array
|
|
226
|
+
* @param tokenType Token to consume
|
|
227
|
+
*/
|
|
228
|
+
function consumeLiteral(tokens, tokenType) {
|
|
229
|
+
if (tokens.length === 0)
|
|
230
|
+
return (0, utils_1.err)("no tokens");
|
|
231
|
+
const nextToken = tokens.shift();
|
|
232
|
+
if (nextToken?.type !== tokenType) {
|
|
233
|
+
return (0, utils_1.err)(`expected '${tokenType}', got '${nextToken?.type}'`);
|
|
234
|
+
}
|
|
235
|
+
return (0, utils_1.ok)(undefined);
|
|
236
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ok = ok;
|
|
4
|
+
exports.err = err;
|
|
5
|
+
exports.assertNever = assertNever;
|
|
6
|
+
exports.isDigit = isDigit;
|
|
7
|
+
exports.isHexDigit = isHexDigit;
|
|
8
|
+
exports.isLetter = isLetter;
|
|
9
|
+
exports.isAlphanumeric = isAlphanumeric;
|
|
10
|
+
exports.isObject = isObject;
|
|
11
|
+
/**
|
|
12
|
+
* Creates an ok (success) variant of `Result`.
|
|
13
|
+
* @param value
|
|
14
|
+
*/
|
|
15
|
+
function ok(value) {
|
|
16
|
+
return { ok: true, value };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates an error variant of `Result`.
|
|
20
|
+
* @param error
|
|
21
|
+
*/
|
|
22
|
+
function err(error) {
|
|
23
|
+
return { ok: false, error };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Throws an "unreachable" error if it ever runs.
|
|
27
|
+
*
|
|
28
|
+
* This is used as an exhaustive typeguard check on switch blocks,
|
|
29
|
+
* causing the type checker to error if a non-never type is passed in.
|
|
30
|
+
* @param _
|
|
31
|
+
*/
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
33
|
+
function assertNever(_) {
|
|
34
|
+
throw new Error("unreachable");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Determines if the given character is a numeric digit (0-9).
|
|
38
|
+
*
|
|
39
|
+
* @param {string} char - A single character to evaluate.
|
|
40
|
+
* @return {boolean} True if the character is a digit, otherwise false.
|
|
41
|
+
*/
|
|
42
|
+
function isDigit(char) {
|
|
43
|
+
return char.charCodeAt(0) >= 48 && char.charCodeAt(0) <= 57;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Determines if the given character is a hex digit (0-F), case-insensitive.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} char - A single character to evaluate.
|
|
49
|
+
* @return {boolean} True if the character is a digit, otherwise false.
|
|
50
|
+
*/
|
|
51
|
+
function isHexDigit(char) {
|
|
52
|
+
return (isDigit(char) ||
|
|
53
|
+
(char >= "a" && char <= "f") ||
|
|
54
|
+
(char >= "A" && char <= "F"));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Determines if a given character is a letter (uppercase or lowercase) `[a-zA-Z]`.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} char - The character to be checked.
|
|
60
|
+
* @return {boolean} Returns true if the character is a letter, otherwise false.
|
|
61
|
+
*/
|
|
62
|
+
function isLetter(char) {
|
|
63
|
+
return ((char.charCodeAt(0) >= 65 && char.charCodeAt(0) <= 90) ||
|
|
64
|
+
(char.charCodeAt(0) >= 97 && char.charCodeAt(0) <= 122));
|
|
65
|
+
}
|
|
66
|
+
function isAlphanumeric(char) {
|
|
67
|
+
return isDigit(char) || isLetter(char);
|
|
68
|
+
}
|
|
69
|
+
function isObject(val) {
|
|
70
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cornjs/parser",
|
|
3
|
+
"author": {
|
|
4
|
+
"email": "mail@jstanger.dev",
|
|
5
|
+
"name": "Jake Stanger",
|
|
6
|
+
"url": "https://github.com/jakestanger"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"version": "0.11.0",
|
|
12
|
+
"description": " Native Typescript Corn parser",
|
|
13
|
+
"homepage": "https://cornlang.dev",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/jakestanger/corn.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/jakestanger/corn/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"corn",
|
|
24
|
+
"config",
|
|
25
|
+
"parser",
|
|
26
|
+
"language",
|
|
27
|
+
"typescript"
|
|
28
|
+
],
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"dedent": "^1.6.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@babel/core": "^7.28.0",
|
|
35
|
+
"@babel/preset-env": "^7.28.0",
|
|
36
|
+
"@babel/preset-typescript": "^7.27.1",
|
|
37
|
+
"@eslint/js": "^9.32.0",
|
|
38
|
+
"@types/jest": "^30.0.0",
|
|
39
|
+
"@types/node": "22.13.14",
|
|
40
|
+
"babel-jest": "^30.0.5",
|
|
41
|
+
"eslint": "^9.32.0",
|
|
42
|
+
"glob": "^13.0.0",
|
|
43
|
+
"jest": "^30.0.5",
|
|
44
|
+
"prettier": "^3.6.2",
|
|
45
|
+
"ts-node": "^10.9.2",
|
|
46
|
+
"typescript": "^5.5.3",
|
|
47
|
+
"typescript-eslint": "^8.38.0"
|
|
48
|
+
},
|
|
49
|
+
"volta": {
|
|
50
|
+
"node": "22.17.1"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"dev": "ts-node src",
|
|
55
|
+
"test": "jest"
|
|
56
|
+
}
|
|
57
|
+
}
|