tarsec 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ import { describe, expect, it, test } from "vitest";
2
+ import {
3
+ alphanum,
4
+ digit,
5
+ letter,
6
+ noneOf,
7
+ num,
8
+ oneOf,
9
+ space,
10
+ spaces,
11
+ str,
12
+ word,
13
+ char,
14
+ } from "./parsers";
15
+ import { success, failure } from "../vitest.globals.js";
16
+ import {
17
+ capture,
18
+ many,
19
+ many1,
20
+ many1WithJoin,
21
+ not,
22
+ optional,
23
+ or,
24
+ seq,
25
+ } from "./combinators";
26
+ test.skip("hello", () => {
27
+ describe("Parser Tests", () => {
28
+ describe("optional parser", () => {
29
+ const parser = optional(char("a"));
30
+
31
+ it("should parse the character if it exists", () => {
32
+ const result = parser("a");
33
+ expect(result).toEqual(success({ rest: "", match: "a" }));
34
+ });
35
+
36
+ it("should return an empty string if the character is missing", () => {
37
+ const result = parser("b");
38
+ expect(result).toEqual(success({ rest: "b", match: "" }));
39
+ });
40
+
41
+ it("should not consume any input if it fails", () => {
42
+ // @ts-ignore
43
+ const parser2 = optional(seq(many1WithJoin(letter), char("!")));
44
+ const result1 = parser2("hello!");
45
+ expect(result1).toEqual(
46
+ success({ rest: "", match: "hello!", captures: {} })
47
+ );
48
+
49
+ const result2 = parser2("hello");
50
+ expect(result2).toEqual(success({ rest: "hello", match: "" }));
51
+ });
52
+ });
53
+
54
+ describe("not parser", () => {
55
+ const parser = not(char("a"));
56
+
57
+ it("should fail if the character is present", () => {
58
+ const result = parser("a");
59
+ expect(result).toEqual(
60
+ failure({ rest: "a", message: "unexpected match" })
61
+ );
62
+ });
63
+
64
+ it("should return an empty string if the character is missing", () => {
65
+ const result = parser("b");
66
+ expect(result).toEqual(success({ rest: "b", match: "" }));
67
+ });
68
+ });
69
+
70
+ describe("space parser", () => {
71
+ it("should parse a space character", () => {
72
+ const result = space(" ");
73
+ expect(result).toEqual(success({ rest: "", match: " " }));
74
+ });
75
+
76
+ it("should fail if the character is not a space", () => {
77
+ const result = space("a");
78
+ expect(result).toEqual(
79
+ failure({ rest: "a", message: "expected , got a" })
80
+ );
81
+ });
82
+ });
83
+
84
+ describe("spaces parser", () => {
85
+ it("should parse multiple space characters", () => {
86
+ const result = spaces(" ");
87
+ expect(result).toEqual(success({ rest: "", match: " " }));
88
+ });
89
+
90
+ it("should fail if no space characters found", () => {
91
+ const result = spaces("abc");
92
+ expect(result).toEqual(
93
+ failure({ rest: "abc", message: "expected at least one match" })
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("digit parser", () => {
99
+ it("should parse a single digit", () => {
100
+ const result = digit("1");
101
+ expect(result).toEqual(success({ rest: "", match: "1" }));
102
+ });
103
+
104
+ it("should fail if the character is not a digit", () => {
105
+ const result = digit("a");
106
+ expect(result).toEqual(
107
+ failure({ rest: "a", message: "expected one of 0123456789" })
108
+ );
109
+ });
110
+ });
111
+
112
+ describe("letter parser", () => {
113
+ it("should parse a single letter", () => {
114
+ const result = letter("a");
115
+ expect(result).toEqual(success({ rest: "", match: "a" }));
116
+ });
117
+
118
+ it("should fail if the character is not a letter", () => {
119
+ const result = letter("1");
120
+ expect(result).toEqual(
121
+ failure({
122
+ rest: "1",
123
+ message: "expected one of abcdefghijklmnopqrstuvwxyz",
124
+ })
125
+ );
126
+ });
127
+ });
128
+
129
+ describe("alphanum parser", () => {
130
+ it("should parse a single alphanumeric character", () => {
131
+ const result = alphanum("1");
132
+ expect(result).toEqual(success({ rest: "", match: "1" }));
133
+ });
134
+
135
+ it("should fail if the character is not alphanumeric", () => {
136
+ const result = alphanum("_");
137
+ expect(result).toEqual(
138
+ failure({
139
+ rest: "_",
140
+ message: "expected one of abcdefghijklmnopqrstuvwxyz0123456789",
141
+ })
142
+ );
143
+ });
144
+ });
145
+
146
+ describe("word parser", () => {
147
+ const parser = word;
148
+
149
+ it("should parse a single word", () => {
150
+ const result = parser("hello");
151
+ expect(result).toEqual(success({ rest: "", match: "hello" }));
152
+ });
153
+
154
+ it("should fail if no word characters found", () => {
155
+ const result = parser("123");
156
+ expect(result).toEqual(
157
+ failure({ rest: "123", message: "expected at least one match" })
158
+ );
159
+ });
160
+ });
161
+
162
+ describe("number parser", () => {
163
+ const parser = num;
164
+
165
+ it("should parse a single number", () => {
166
+ const result = parser("123");
167
+ expect(result).toEqual(success({ rest: "", match: "123" }));
168
+ });
169
+
170
+ it("should fail if no number characters found", () => {
171
+ const result = parser("abc");
172
+ expect(result).toEqual(
173
+ failure({ rest: "abc", message: "expected at least one match" })
174
+ );
175
+ });
176
+ });
177
+ });
178
+
179
+ describe("seq parser", () => {
180
+ const parser = seq([char("a"), char("b")]);
181
+
182
+ it("should parse both characters in sequence", () => {
183
+ const result = parser("ab");
184
+ expect(result).toEqual(success({ rest: "", match: "ab", captures: {} }));
185
+ });
186
+
187
+ it("should fail if any of the parsers fail", () => {
188
+ const result = parser("ac");
189
+ expect(result).toEqual(
190
+ failure({ rest: "c", message: "expected b, got c" })
191
+ );
192
+ });
193
+ });
194
+
195
+ describe("seq parser - hello world", () => {
196
+ it("multiple char parsers", () => {
197
+ const parser = seq([
198
+ char("h"),
199
+ char("e"),
200
+ char("l"),
201
+ char("l"),
202
+ char("o"),
203
+ ]);
204
+ const result = parser("hello world");
205
+ expect(result).toEqual(
206
+ success({ match: "hello", rest: " world", captures: {} })
207
+ );
208
+ });
209
+
210
+ it("multiple str parsers", () => {
211
+ const parser = seq([str("hello"), space, str("world")]);
212
+ const result = parser("hello world");
213
+ expect(result).toEqual(
214
+ success({ match: "hello world", rest: "", captures: {} })
215
+ );
216
+ });
217
+
218
+ it("multiple str parsers + capture", () => {
219
+ const parser = seq([str("hello"), space, capture(str("world"), "name")]);
220
+ const result = parser("hello world");
221
+ expect(result).toEqual(
222
+ success({ match: "hello world", rest: "", captures: { name: "world" } })
223
+ );
224
+ });
225
+ });
226
+
227
+ /* test("quote parser - single quote", () => {
228
+ const input = "'";
229
+ const result = quote(input);
230
+ expect(result).toEqual(success({ rest: "", match: "'", captures: {} }));
231
+ });
232
+
233
+ test("quote parser - double quote", () => {
234
+ const input = '"';
235
+ const result = quote(input);
236
+ expect(result).toEqual(success({ rest: "", match: '"', captures: {} }));
237
+ });
238
+
239
+ test("quote parser - invalid quote", () => {
240
+ const input = "`";
241
+ const result = quote(input);
242
+ expect(result).toEqual(
243
+ failure({ rest: "`", message: "unexpected end of input" })
244
+ );
245
+ });
246
+
247
+ // Test for anyChar parser
248
+ test("anyChar parser - non-empty input", () => {
249
+ const input = "abc";
250
+ const result = anyChar(input);
251
+ expect(result).toEqual(success({ rest: "bc", match: "a", captures: {} }));
252
+ });
253
+
254
+ test("anyChar parser - empty input", () => {
255
+ const input = "";
256
+ const result = anyChar(input);
257
+ expect(result).toEqual(
258
+ failure({ rest: "", message: "unexpected end of input" })
259
+ );
260
+ });
261
+
262
+ // Test for between parser
263
+ test("between parser - valid input", () => {
264
+ const open = quote;
265
+ const close = quote;
266
+ const parser = anyChar;
267
+ const input = "'abc'";
268
+ const result = between(open, close, parser)(input);
269
+ expect(result).toEqual(success({ rest: "", match: "a", captures: {} }));
270
+ });
271
+
272
+ test("between parser - invalid input", () => {
273
+ const open = quote;
274
+ const close = quote;
275
+ const parser = anyChar;
276
+ const input = "\"abc'";
277
+ const result = between(open, close, parser)(input);
278
+ expect(result).toEqual(
279
+ failure({ rest: "abc'", message: "unexpected end of input" })
280
+ );
281
+ });
282
+
283
+ // Test for sepBy parser
284
+ test("sepBy parser - valid input", () => {
285
+ const separator = anyChar;
286
+ const parser = anyChar;
287
+ const input = "a,b,c";
288
+ const result = sepBy(separator, parser)(input);
289
+ expect(result).toEqual(success({ rest: "", match: "abc", captures: {} }));
290
+ });
291
+
292
+ test("sepBy parser - invalid input", () => {
293
+ const separator = quote;
294
+ const parser = anyChar;
295
+ const input = '"a"bc';
296
+ const result = sepBy(separator, parser)(input);
297
+ expect(result).toEqual(success({ rest: "bc", match: "a", captures: {} }));
298
+ });
299
+ */
300
+ });
package/lib/parsers.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { many1, many1WithJoin, seq, transform } from "./combinators";
2
+ import { trace } from "./trace";
3
+ import { Parser, ParserResult } from "./types";
4
+ import { escape } from "./utils";
5
+ export function char(c: string): Parser<string> {
6
+ return trace(`char(${escape(c)})`, (input: string) => {
7
+ if (input.length === 0) {
8
+ return {
9
+ success: false,
10
+ rest: input,
11
+ message: "unexpected end of input",
12
+ };
13
+ }
14
+ if (input[0] === c) {
15
+ return { success: true, match: c, rest: input.slice(1) };
16
+ }
17
+ return {
18
+ success: false,
19
+ rest: input,
20
+ message: `expected ${c}, got ${input[0]}`,
21
+ };
22
+ });
23
+ }
24
+
25
+ export function str(s: string): Parser<string> {
26
+ return trace(`str(${escape(s)})`, (input: string) => {
27
+ if (input.substring(0, s.length) === s) {
28
+ return { success: true, match: s, rest: input.slice(s.length) };
29
+ }
30
+ return {
31
+ success: false,
32
+ rest: input,
33
+ message: `expected ${s}, got ${input.substring(0, s.length)}`,
34
+ };
35
+ });
36
+ }
37
+
38
+ export function oneOf(chars: string): Parser<string> {
39
+ return trace(`oneOf(${escape(chars)})`, (input: string) => {
40
+ if (input.length === 0) {
41
+ return {
42
+ success: false,
43
+ rest: input,
44
+ message: "unexpected end of input",
45
+ };
46
+ }
47
+ const c = input[0];
48
+ if (chars.includes(c)) {
49
+ return char(c)(input);
50
+ }
51
+ return {
52
+ success: false,
53
+ rest: input,
54
+ message: `expected one of ${escape(chars)}, got ${c}`,
55
+ };
56
+ });
57
+ }
58
+
59
+ export function noneOf(chars: string): Parser<string> {
60
+ return trace(`noneOf(${escape(chars)})`, (input: string) => {
61
+ if (input.length === 0) {
62
+ return {
63
+ success: false,
64
+ rest: input,
65
+ message: "unexpected end of input",
66
+ };
67
+ }
68
+ if (chars.includes(input[0])) {
69
+ return {
70
+ success: false,
71
+ rest: input,
72
+ message: `expected none of ${chars}`,
73
+ };
74
+ }
75
+ return char(input[0])(input);
76
+ });
77
+ }
78
+
79
+ export function anyChar(input: string): Parser<string> {
80
+ return trace("anyChar", (input: string) => {
81
+ if (input.length === 0) {
82
+ return {
83
+ success: false,
84
+ rest: input,
85
+ message: "unexpected end of input",
86
+ };
87
+ }
88
+ return { success: true, match: input[0], rest: input.slice(1) };
89
+ });
90
+ }
91
+
92
+ export const space: Parser<string> = oneOf(" \t\n\r");
93
+ export const spaces: Parser<string> = many1WithJoin(space);
94
+ export const digit: Parser<string> = oneOf("0123456789");
95
+ export const letter: Parser<string> = oneOf("abcdefghijklmnopqrstuvwxyz");
96
+ export const alphanum: Parser<string> = oneOf(
97
+ "abcdefghijklmnopqrstuvwxyz0123456789"
98
+ );
99
+ export const word: Parser<string> = many1WithJoin(letter);
100
+ export const num: Parser<string> = many1WithJoin(digit);
101
+ export const quote: Parser<string> = oneOf(`'"`);
102
+ export const tab: Parser<string> = char("\t");
103
+ export const newline: Parser<string> = char("\n");
104
+
105
+ export const quotedString = transform(
106
+ seq<any, string>([quote, word, quote], "quotedString"),
107
+ (x) => x.join("")
108
+ );
package/lib/trace.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { ParserResult, Parser } from "./types";
2
+ import { escape } from "./utils";
3
+ const STEP = 2;
4
+
5
+ export function resultToString<T>(
6
+ name: string,
7
+ result: ParserResult<T, string>
8
+ ): string {
9
+ if (result.success) {
10
+ return `✅ ${name} -- match: ${escape(result.match)}, rest: ${escape(
11
+ result.rest
12
+ )}`;
13
+ }
14
+ return `❌ ${name} -- message: ${escape(result.message)}, rest: ${escape(
15
+ result.rest
16
+ )}`;
17
+ }
18
+
19
+ let level = 0;
20
+
21
+ export function trace<T>(name: string, parser: Parser<T>): Parser<T> {
22
+ return (input: string) => {
23
+ if (process.env.DEBUG) {
24
+ console.log(" ".repeat(level) + `🔍 ${name} -- input: ${escape(input)}`);
25
+ }
26
+ level += STEP;
27
+ const result = parser(input);
28
+ level -= STEP;
29
+ if (process.env.DEBUG) {
30
+ console.log(" ".repeat(level) + resultToString(name, result));
31
+ if (result.success && result.captures) {
32
+ console.log(
33
+ " ".repeat(level) +
34
+ `⭐ ${name} -- captures: ${JSON.stringify(result.captures)}`
35
+ );
36
+ }
37
+ }
38
+ return result;
39
+ };
40
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ export type Object = Record<string, string>;
2
+ export type ParserSuccess<M, C extends string> = {
3
+ success: true;
4
+ match: M;
5
+ captures?: Record<C, any>;
6
+ rest: string;
7
+ };
8
+
9
+ export type ParserFailure = {
10
+ success: false;
11
+ rest: string;
12
+ message: string;
13
+ };
14
+
15
+ export type ParserOptions = {
16
+ capture: string;
17
+ };
18
+
19
+ export type ParserResult<M, C extends string> =
20
+ | ParserSuccess<M, C>
21
+ | ParserFailure;
22
+ export type Parser<M, C extends string = string> = (
23
+ input: string
24
+ ) => ParserResult<M, C>;
package/lib/utils.ts ADDED
@@ -0,0 +1,33 @@
1
+ export function escape(str: any) {
2
+ return JSON.stringify(str);
3
+ }
4
+
5
+ export function merge(a: any | any[], b: any | any[]): any[] {
6
+ if (Array.isArray(a) && Array.isArray(b)) {
7
+ return [...a, ...b];
8
+ } else if (Array.isArray(a)) {
9
+ return [...a, b];
10
+ } else if (Array.isArray(b)) {
11
+ return [a, ...b];
12
+ } else {
13
+ return [a, b];
14
+ }
15
+ }
16
+
17
+ export function mergeCaptures(
18
+ a: Record<string, any>,
19
+ b: Record<string, any>
20
+ ): Record<string, any> {
21
+ const result: Record<string, any> = {};
22
+ Object.keys(a).forEach((key) => {
23
+ result[key] = a[key];
24
+ });
25
+ Object.keys(b).forEach((key) => {
26
+ if (result[key]) {
27
+ result[key] = merge(result[key], b[key]);
28
+ } else {
29
+ result[key] = b[key];
30
+ }
31
+ });
32
+ return result;
33
+ }
package/makefile ADDED
@@ -0,0 +1,5 @@
1
+ all:
2
+ npm run build && npm run start
3
+
4
+ test:
5
+ npm run test
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "tarsec",
3
+ "version": "0.0.1",
4
+ "description": "A parser combinator library for TypeScript, inspired by Parsec.",
5
+ "scripts": {
6
+ "test": "vitest",
7
+ "build": "tsc",
8
+ "start": "cd dist && node index.js"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "devDependencies": {
14
+ "@types/node": "^20.11.28",
15
+ "vitest": "^1.4.0"
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ import { many } from "@/lib/combinators";
2
+ import { digit } from "@/lib/parsers";
3
+ import { describe, it, expect } from "vitest";
4
+ import { success } from "vitest.globals";
5
+
6
+ describe("many combinator", () => {
7
+ const parser = many(digit);
8
+
9
+ it("should parse multiple digits", () => {
10
+ const result = parser("1234");
11
+ expect(result).toEqual(success({ rest: "", match: ["1", "2", "3", "4"] }));
12
+ });
13
+
14
+ it("should return an empty string if no matches found", () => {
15
+ const result = parser("abc");
16
+ expect(result).toEqual(success({ rest: "abc", match: [] }));
17
+ });
18
+ });
@@ -0,0 +1,20 @@
1
+ import { many1 } from "@/lib/combinators";
2
+ import { digit } from "@/lib/parsers";
3
+ import { describe, it, expect } from "vitest";
4
+ import { success, failure } from "vitest.globals";
5
+
6
+ describe("many1 combinator", () => {
7
+ const parser = many1(digit);
8
+
9
+ it("should parse multiple digits", () => {
10
+ const result = parser("1234");
11
+ expect(result).toEqual(success({ rest: "", match: ["1", "2", "3", "4"] }));
12
+ });
13
+
14
+ it("should fail if no matches found", () => {
15
+ const result = parser("abc");
16
+ expect(result).toEqual(
17
+ failure({ rest: "abc", message: "expected at least one match" })
18
+ );
19
+ });
20
+ });
@@ -0,0 +1,25 @@
1
+ import { or } from "@/lib/combinators";
2
+ import { char } from "@/lib/parsers";
3
+ import { describe, it, expect } from "vitest";
4
+ import { success, failure } from "vitest.globals";
5
+
6
+ describe("or parser", () => {
7
+ const parser = or<string>([char("a"), char("b")]);
8
+
9
+ it("should parse the first parser if it succeeds", () => {
10
+ const result = parser("a");
11
+ expect(result).toEqual(success({ rest: "", match: "a" }));
12
+ });
13
+
14
+ it("should parse the second parser if the first one fails", () => {
15
+ const result = parser("b");
16
+ expect(result).toEqual(success({ rest: "", match: "b" }));
17
+ });
18
+
19
+ it("should fail if all parsers fail", () => {
20
+ const result = parser("c");
21
+ expect(result).toEqual(
22
+ failure({ rest: "c", message: "all parsers failed" })
23
+ );
24
+ });
25
+ });
@@ -0,0 +1,38 @@
1
+ import { seq, capture } from "@/lib/combinators";
2
+ import { char, str, space } from "@/lib/parsers";
3
+ import { describe, it, expect } from "vitest";
4
+ import { success } from "vitest.globals";
5
+
6
+ describe("seq parser - hello world", () => {
7
+ it("multiple char parsers", () => {
8
+ const parser = seq([char("h"), char("e"), char("l"), char("l"), char("o")]);
9
+ const result = parser("hello world");
10
+ expect(result).toEqual(
11
+ success({
12
+ match: ["h", "e", "l", "l", "o"],
13
+ rest: " world",
14
+ captures: {},
15
+ })
16
+ );
17
+ });
18
+
19
+ it("multiple str parsers", () => {
20
+ const parser = seq([str("hello"), space, str("world")]);
21
+ const result = parser("hello world");
22
+ expect(result).toEqual(
23
+ success({ match: ["hello", " ", "world"], rest: "", captures: {} })
24
+ );
25
+ });
26
+
27
+ it("multiple str parsers + capture", () => {
28
+ const parser = seq([str("hello"), space, capture(str("world"), "name")]);
29
+ const result = parser("hello world");
30
+ expect(result).toEqual(
31
+ success({
32
+ match: ["hello", " ", "world"],
33
+ rest: "",
34
+ captures: { name: "world" },
35
+ })
36
+ );
37
+ });
38
+ });
@@ -0,0 +1,21 @@
1
+ import { seq, capture, many1, many1WithJoin } from "@/lib/combinators";
2
+ import { char, str, space, noneOf } from "@/lib/parsers";
3
+ import { ParserSuccess } from "@/lib/types";
4
+ import { describe, it, expect } from "vitest";
5
+ import { success } from "vitest.globals";
6
+
7
+ describe("hello world", () => {
8
+ it("parses + captures name", () => {
9
+ const parser = seq([
10
+ str("hello"),
11
+ space,
12
+ capture(many1WithJoin(noneOf("!")), "name"),
13
+ char("!"),
14
+ ]);
15
+ const result = parser("hello adit!");
16
+ expect(result.success).toEqual(true);
17
+ expect((result as ParserSuccess<string[], string>).captures).toEqual({
18
+ name: "adit",
19
+ });
20
+ });
21
+ });
@@ -0,0 +1,14 @@
1
+ import { seq, capture } from "@/lib/combinators";
2
+ import { char, str, space } from "@/lib/parsers";
3
+ import { describe, it, expect } from "vitest";
4
+ import { success } from "vitest.globals";
5
+
6
+ describe("hello world", () => {
7
+ it("parses", () => {
8
+ const parser = seq([str("hello"), space, str("world")]);
9
+ const result = parser("hello world");
10
+ expect(result).toEqual(
11
+ success({ match: ["hello", " ", "world"], rest: "", captures: {} })
12
+ );
13
+ });
14
+ });