@graffiticode/parser 0.4.0 → 1.0.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/package.json +1 -1
- package/src/parser.js +10 -52
- package/src/parser.spec.js +38 -153
- package/src/unparse-l0166.spec.js +12 -9
- package/src/unparse.spec.js +11 -8
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import vm from "vm";
|
|
2
|
-
import { getLangAsset } from "../../api/src/lang/index.js";
|
|
3
1
|
import { parse } from "./parse.js";
|
|
4
2
|
import { unparse } from "./unparse.js";
|
|
5
3
|
|
|
@@ -40,63 +38,20 @@ const main = {
|
|
|
40
38
|
}
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
export const buildParser = ({
|
|
44
|
-
log,
|
|
45
|
-
cache,
|
|
46
|
-
getLangAsset,
|
|
47
|
-
main,
|
|
48
|
-
vm
|
|
49
|
-
}) => {
|
|
41
|
+
export const buildParser = ({ main }) => {
|
|
50
42
|
return {
|
|
51
|
-
async parse(lang, src, lexicon
|
|
52
|
-
//
|
|
53
|
-
if (lexicon) {
|
|
54
|
-
|
|
43
|
+
async parse(lang, src, lexicon) {
|
|
44
|
+
// Lexicon is now required
|
|
45
|
+
if (!lexicon) {
|
|
46
|
+
throw new Error("Lexicon is required for parsing");
|
|
55
47
|
}
|
|
56
|
-
|
|
57
|
-
// Otherwise, load from cache or remote
|
|
58
|
-
if (!cache.has(lang)) {
|
|
59
|
-
let data = await getLangAsset(lang, "/lexicon.js");
|
|
60
|
-
// TODO Make lexicon JSON.
|
|
61
|
-
if (data instanceof Buffer) {
|
|
62
|
-
data = data.toString();
|
|
63
|
-
}
|
|
64
|
-
if (typeof (data) !== "string") {
|
|
65
|
-
log(`Failed to get usable lexicon for ${lang}`, typeof (data), data);
|
|
66
|
-
throw new Error("unable to use lexicon");
|
|
67
|
-
}
|
|
68
|
-
const lstr = data.substring(data.indexOf("{"));
|
|
69
|
-
let loadedLexicon;
|
|
70
|
-
try {
|
|
71
|
-
loadedLexicon = JSON.parse(lstr);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
if (err instanceof SyntaxError) {
|
|
74
|
-
log(`failed to parse ${lang} lexicon: ${err.message}`);
|
|
75
|
-
const context = { window: { gcexports: {} } };
|
|
76
|
-
vm.createContext(context);
|
|
77
|
-
vm.runInContext(data, context);
|
|
78
|
-
if (typeof (context.window.gcexports.globalLexicon) === "object") {
|
|
79
|
-
loadedLexicon = context.window.gcexports.globalLexicon;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (!loadedLexicon) {
|
|
83
|
-
throw new Error("Malformed lexicon");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
cache.set(lang, loadedLexicon);
|
|
87
|
-
};
|
|
88
|
-
const cachedLexicon = cache.get(lang);
|
|
89
|
-
return await main.parse(src, cachedLexicon);
|
|
48
|
+
return await main.parse(src, lexicon);
|
|
90
49
|
}
|
|
91
50
|
};
|
|
92
51
|
};
|
|
93
52
|
|
|
94
53
|
export const parser = buildParser({
|
|
95
|
-
|
|
96
|
-
cache: new Map(),
|
|
97
|
-
getLangAsset,
|
|
98
|
-
main,
|
|
99
|
-
vm
|
|
54
|
+
main
|
|
100
55
|
});
|
|
101
56
|
|
|
102
57
|
// Add unparse as a property of parser
|
|
@@ -104,6 +59,9 @@ parser.unparse = unparse;
|
|
|
104
59
|
|
|
105
60
|
// Add reformat function that parses and unparses code
|
|
106
61
|
parser.reformat = async function(lang, src, lexicon, options = {}) {
|
|
62
|
+
if (!lexicon) {
|
|
63
|
+
throw new Error("Lexicon is required for reformatting");
|
|
64
|
+
}
|
|
107
65
|
const ast = await this.parse(lang, src, lexicon);
|
|
108
66
|
return unparse(ast, lexicon, options);
|
|
109
67
|
};
|
package/src/parser.spec.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
2
|
import { buildParser, parser } from "./parser.js";
|
|
3
3
|
import { mockPromiseValue, mockPromiseError } from "./testing/index.js";
|
|
4
|
-
import
|
|
4
|
+
import { lexicon as basisLexicon } from "@graffiticode/basis";
|
|
5
5
|
|
|
6
6
|
describe("lang/parser", () => {
|
|
7
7
|
const log = jest.fn();
|
|
8
|
-
it("should use provided lexicon
|
|
8
|
+
it("should use provided lexicon", async () => {
|
|
9
9
|
// Arrange
|
|
10
|
-
const cache = new Map();
|
|
11
|
-
const getLangAsset = jest.fn(); // Should not be called
|
|
12
10
|
const main = {
|
|
13
11
|
parse: mockPromiseValue({ root: "0" })
|
|
14
12
|
};
|
|
15
|
-
const parser = buildParser({
|
|
13
|
+
const parser = buildParser({ main });
|
|
16
14
|
const lang = "0";
|
|
17
15
|
const src = "'foo'..";
|
|
18
16
|
const providedLexicon = { test: "lexicon" };
|
|
@@ -21,154 +19,78 @@ describe("lang/parser", () => {
|
|
|
21
19
|
await expect(parser.parse(lang, src, providedLexicon)).resolves.toStrictEqual({ root: "0" });
|
|
22
20
|
|
|
23
21
|
// Assert
|
|
24
|
-
expect(getLangAsset).not.toHaveBeenCalled(); // Should not fetch when lexicon is provided
|
|
25
22
|
expect(main.parse).toHaveBeenCalledWith(src, providedLexicon);
|
|
26
|
-
expect(cache.has(lang)).toBe(false); // Should not cache when lexicon is provided
|
|
27
23
|
});
|
|
28
24
|
|
|
29
|
-
it("should
|
|
25
|
+
it("should throw error when lexicon is missing", async () => {
|
|
30
26
|
// Arrange
|
|
31
|
-
const cache = new Map();
|
|
32
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
33
27
|
const main = {
|
|
34
28
|
parse: mockPromiseValue({ root: "0" })
|
|
35
29
|
};
|
|
36
|
-
const parser = buildParser({
|
|
30
|
+
const parser = buildParser({ main });
|
|
37
31
|
const lang = "0";
|
|
38
32
|
const src = "'foo'..";
|
|
39
33
|
|
|
40
|
-
// Act
|
|
41
|
-
await expect(parser.parse(lang, src)).
|
|
42
|
-
|
|
43
|
-
// Assert
|
|
44
|
-
expect(getLangAsset).toHaveBeenCalledWith(lang, "/lexicon.js");
|
|
45
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
46
|
-
expect(cache.has(lang)).toBe(true);
|
|
47
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
34
|
+
// Act & Assert
|
|
35
|
+
await expect(parser.parse(lang, src)).rejects.toThrow("Lexicon is required for parsing");
|
|
48
36
|
});
|
|
49
|
-
it("should
|
|
37
|
+
it("should pass lexicon to main parser", async () => {
|
|
50
38
|
// Arrange
|
|
51
|
-
const cache = new Map();
|
|
52
39
|
const main = {
|
|
53
40
|
parse: mockPromiseValue({ root: "0" })
|
|
54
41
|
};
|
|
55
|
-
const parser = buildParser({
|
|
56
|
-
cache,
|
|
57
|
-
main
|
|
58
|
-
});
|
|
42
|
+
const parser = buildParser({ main });
|
|
59
43
|
const lang = "0";
|
|
60
44
|
const src = "'foo'..";
|
|
61
|
-
|
|
45
|
+
const lexicon = { someFunc: { name: "SOMEFUNC" } };
|
|
62
46
|
|
|
63
47
|
// Act
|
|
64
|
-
await expect(parser.parse(lang, src)).resolves.toStrictEqual({ root: "0" });
|
|
48
|
+
await expect(parser.parse(lang, src, lexicon)).resolves.toStrictEqual({ root: "0" });
|
|
65
49
|
|
|
66
50
|
// Assert
|
|
67
|
-
expect(main.parse).toHaveBeenCalledWith(src,
|
|
51
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
68
52
|
});
|
|
69
|
-
it("should return error if
|
|
53
|
+
it("should return error if main parser fails with lexicon", async () => {
|
|
70
54
|
// Arrange
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const parser = buildParser({
|
|
75
|
-
cache,
|
|
76
|
-
getLangAsset
|
|
77
|
-
});
|
|
55
|
+
const err = new Error("parser failed");
|
|
56
|
+
const main = { parse: mockPromiseError(err) };
|
|
57
|
+
const parser = buildParser({ main });
|
|
78
58
|
const lang = "00";
|
|
79
59
|
const src = "'foo'..";
|
|
60
|
+
const lexicon = {};
|
|
80
61
|
|
|
81
62
|
// Act
|
|
82
|
-
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
63
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
83
64
|
|
|
84
65
|
// Assert
|
|
85
|
-
expect(
|
|
66
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
86
67
|
});
|
|
87
68
|
it("should return error if main parser fails", async () => {
|
|
88
69
|
// Arrange
|
|
89
|
-
const log = jest.fn();
|
|
90
|
-
const cache = new Map();
|
|
91
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
92
70
|
const err = new Error("main parser failed");
|
|
93
71
|
const main = { parse: mockPromiseError(err) };
|
|
94
|
-
const parser = buildParser({
|
|
95
|
-
const lang = "0";
|
|
96
|
-
const src = "'foo'..";
|
|
97
|
-
|
|
98
|
-
// Act
|
|
99
|
-
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
100
|
-
|
|
101
|
-
// Assert
|
|
102
|
-
expect(getLangAsset).toHaveBeenCalledWith(lang, "/lexicon.js");
|
|
103
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
104
|
-
expect(cache.has(lang)).toBe(true);
|
|
105
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
106
|
-
});
|
|
107
|
-
it("should return succeed if lexicon is a buffer", async () => {
|
|
108
|
-
// Arrange
|
|
109
|
-
const log = jest.fn();
|
|
110
|
-
const cache = new Map();
|
|
111
|
-
const getLangAsset = mockPromiseValue(Buffer.from("{}"));
|
|
112
|
-
const ast = { root: "0" };
|
|
113
|
-
const main = { parse: mockPromiseValue(ast) };
|
|
114
|
-
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
115
|
-
const lang = "0";
|
|
116
|
-
const src = "'foo'..";
|
|
117
|
-
|
|
118
|
-
// Act
|
|
119
|
-
await expect(parser.parse(lang, src)).resolves.toStrictEqual(ast);
|
|
120
|
-
|
|
121
|
-
// Assert
|
|
122
|
-
expect(getLangAsset).toHaveBeenCalledWith(lang, "/lexicon.js");
|
|
123
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
124
|
-
expect(cache.has(lang)).toBe(true);
|
|
125
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
126
|
-
});
|
|
127
|
-
it("should try vm if lexicon cannot parse JSON", async () => {
|
|
128
|
-
// Arrange
|
|
129
|
-
const log = jest.fn();
|
|
130
|
-
const cache = new Map();
|
|
131
|
-
const rawLexicon = `
|
|
132
|
-
(() => {
|
|
133
|
-
window.gcexports.globalLexicon = {};
|
|
134
|
-
})();
|
|
135
|
-
`;
|
|
136
|
-
const getLangAsset = mockPromiseValue(rawLexicon);
|
|
137
|
-
const ast = { root: "0" };
|
|
138
|
-
const main = { parse: mockPromiseValue(ast) };
|
|
139
|
-
const vm = {
|
|
140
|
-
createContext: jest.fn(),
|
|
141
|
-
runInContext: jest.fn().mockImplementation((data, context) => {
|
|
142
|
-
context.window.gcexports.globalLexicon = {};
|
|
143
|
-
})
|
|
144
|
-
};
|
|
145
|
-
const parser = buildParser({ log, cache, getLangAsset, main, vm });
|
|
72
|
+
const parser = buildParser({ main });
|
|
146
73
|
const lang = "0";
|
|
147
74
|
const src = "'foo'..";
|
|
75
|
+
const lexicon = {};
|
|
148
76
|
|
|
149
77
|
// Act
|
|
150
|
-
await expect(parser.parse(lang, src)).
|
|
78
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
151
79
|
|
|
152
80
|
// Assert
|
|
153
|
-
expect(
|
|
154
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
155
|
-
expect(cache.has(lang)).toBe(true);
|
|
156
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
157
|
-
expect(vm.createContext).toHaveBeenCalled();
|
|
158
|
-
expect(vm.runInContext).toHaveBeenCalledWith(rawLexicon, expect.anything());
|
|
81
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
159
82
|
});
|
|
160
83
|
it("should parse error", async () => {
|
|
161
84
|
// Arrange
|
|
162
|
-
const cache = new Map();
|
|
163
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
164
85
|
const err = new Error("End of program reached.");
|
|
165
86
|
const main = { parse: mockPromiseError(err) };
|
|
166
|
-
const parser = buildParser({
|
|
87
|
+
const parser = buildParser({ main });
|
|
167
88
|
const lang = "0";
|
|
168
89
|
const src = "'hello, world'";
|
|
90
|
+
const lexicon = {};
|
|
169
91
|
|
|
170
92
|
// Act & Assert
|
|
171
|
-
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
93
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
172
94
|
});
|
|
173
95
|
});
|
|
174
96
|
|
|
@@ -176,7 +98,7 @@ describe("parser integration tests", () => {
|
|
|
176
98
|
// Tests using the actual parser
|
|
177
99
|
it("should parse string literals", async () => {
|
|
178
100
|
// Arrange & Act
|
|
179
|
-
const result = await parser.parse(0, "'hello, world'..");
|
|
101
|
+
const result = await parser.parse(0, "'hello, world'..", basisLexicon);
|
|
180
102
|
|
|
181
103
|
// Assert
|
|
182
104
|
expect(result).toHaveProperty("root");
|
|
@@ -205,7 +127,7 @@ describe("parser integration tests", () => {
|
|
|
205
127
|
|
|
206
128
|
it("should parse numeric literals", async () => {
|
|
207
129
|
// Arrange & Act
|
|
208
|
-
const result = await parser.parse(0, "42..");
|
|
130
|
+
const result = await parser.parse(0, "42..", basisLexicon);
|
|
209
131
|
|
|
210
132
|
// Assert
|
|
211
133
|
expect(result).toHaveProperty("root");
|
|
@@ -229,7 +151,7 @@ describe("parser integration tests", () => {
|
|
|
229
151
|
|
|
230
152
|
it("should have a PROG node at the root", async () => {
|
|
231
153
|
// Let's test the most basic structure that should always work
|
|
232
|
-
const result = await parser.parse(0, "123..");
|
|
154
|
+
const result = await parser.parse(0, "123..", basisLexicon);
|
|
233
155
|
|
|
234
156
|
// Assert
|
|
235
157
|
expect(result).toHaveProperty("root");
|
|
@@ -257,9 +179,8 @@ describe("parser integration tests", () => {
|
|
|
257
179
|
});
|
|
258
180
|
|
|
259
181
|
it("should parse complex program: apply (<a b: add a b>) [10 20]..", async () => {
|
|
260
|
-
// Create
|
|
261
|
-
const
|
|
262
|
-
customLexiconCache.set(0, {
|
|
182
|
+
// Create custom lexicon
|
|
183
|
+
const customLexicon = {
|
|
263
184
|
add: {
|
|
264
185
|
tk: 2,
|
|
265
186
|
name: "add",
|
|
@@ -272,23 +193,10 @@ describe("parser integration tests", () => {
|
|
|
272
193
|
cls: "function",
|
|
273
194
|
length: 2
|
|
274
195
|
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Use the parser with our custom cache
|
|
278
|
-
const customParser = buildParser({
|
|
279
|
-
log: console.log,
|
|
280
|
-
cache: customLexiconCache,
|
|
281
|
-
getLangAsset: async () => ({}),
|
|
282
|
-
main: {
|
|
283
|
-
parse: (src, lexicon) => {
|
|
284
|
-
return Promise.resolve(parser.parse(0, src));
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
vm
|
|
288
|
-
});
|
|
196
|
+
};
|
|
289
197
|
|
|
290
198
|
// Act
|
|
291
|
-
const result = await
|
|
199
|
+
const result = await parser.parse(0, "apply (<a b: add a b>) [10 20]..", customLexicon);
|
|
292
200
|
|
|
293
201
|
// Assert
|
|
294
202
|
expect(result).toHaveProperty("root");
|
|
@@ -348,7 +256,7 @@ describe("parser integration tests", () => {
|
|
|
348
256
|
|
|
349
257
|
try {
|
|
350
258
|
// Unclosed string - missing closing quote
|
|
351
|
-
result = await parser.parse(0, "'unclosed string..");
|
|
259
|
+
result = await parser.parse(0, "'unclosed string..", basisLexicon);
|
|
352
260
|
} catch (e) {
|
|
353
261
|
// Check for expected error (we should now have a robust parser that doesn't throw)
|
|
354
262
|
console.error("Unexpected error:", e);
|
|
@@ -388,7 +296,7 @@ describe("parser integration tests", () => {
|
|
|
388
296
|
|
|
389
297
|
try {
|
|
390
298
|
// Missing closing bracket
|
|
391
|
-
result = await parser.parse(0, "[1, 2, 3..");
|
|
299
|
+
result = await parser.parse(0, "[1, 2, 3..", basisLexicon);
|
|
392
300
|
} catch (e) {
|
|
393
301
|
console.error("Unexpected error:", e);
|
|
394
302
|
throw e;
|
|
@@ -427,7 +335,7 @@ describe("parser integration tests", () => {
|
|
|
427
335
|
|
|
428
336
|
try {
|
|
429
337
|
// Invalid sequence of tokens
|
|
430
|
-
result = await parser.parse(0, "if then else..");
|
|
338
|
+
result = await parser.parse(0, "if then else..", basisLexicon);
|
|
431
339
|
} catch (e) {
|
|
432
340
|
console.error("Unexpected error:", e);
|
|
433
341
|
throw e;
|
|
@@ -462,32 +370,9 @@ describe("parser integration tests", () => {
|
|
|
462
370
|
});
|
|
463
371
|
|
|
464
372
|
it("should perform parse-time evaluation for adding two numbers", async () => {
|
|
465
|
-
//
|
|
466
|
-
const customLexiconCache = new Map();
|
|
467
|
-
customLexiconCache.set(0, {
|
|
468
|
-
add: {
|
|
469
|
-
tk: 2,
|
|
470
|
-
name: "add",
|
|
471
|
-
cls: "function",
|
|
472
|
-
length: 2
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Use the parser with our custom cache
|
|
477
|
-
const customParser = buildParser({
|
|
478
|
-
log: console.log,
|
|
479
|
-
cache: customLexiconCache,
|
|
480
|
-
getLangAsset: async () => ({}),
|
|
481
|
-
main: {
|
|
482
|
-
parse: (src, lexicon) => {
|
|
483
|
-
return Promise.resolve(parser.parse(0, src));
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
vm
|
|
487
|
-
});
|
|
488
|
-
|
|
373
|
+
// Use basis lexicon which includes add function
|
|
489
374
|
// Act - parse a simple addition expression
|
|
490
|
-
const result = await
|
|
375
|
+
const result = await parser.parse(0, "add 123 456..", basisLexicon);
|
|
491
376
|
console.log(
|
|
492
377
|
"TEST",
|
|
493
378
|
"result=" + JSON.stringify(result, null, 2),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parser } from "./parser.js";
|
|
2
2
|
import { unparse } from "./unparse.js";
|
|
3
|
+
import { lexicon as basisLexicon } from "@graffiticode/basis";
|
|
3
4
|
|
|
4
5
|
describe("unparse with L0166 lexicon", () => {
|
|
5
6
|
// L0166 lexicon for spreadsheet operations (from l0166/packages/api/src/lexicon.js)
|
|
@@ -132,6 +133,9 @@ describe("unparse with L0166 lexicon", () => {
|
|
|
132
133
|
}
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
// Merge basis and L0166 lexicons
|
|
137
|
+
const mergedLexicon = { ...basisLexicon, ...l0166Lexicon };
|
|
138
|
+
|
|
135
139
|
it("should unparse L0166 spreadsheet code", async () => {
|
|
136
140
|
const source = `columns [
|
|
137
141
|
column A width 100 align "center" protected true {}
|
|
@@ -150,9 +154,8 @@ cells [
|
|
|
150
154
|
// produces valid code that can be parsed again
|
|
151
155
|
// Pass the lexicon directly to avoid fetching
|
|
152
156
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
const ast = await parser.parse(0, source);
|
|
157
|
+
// Parse with merged lexicon
|
|
158
|
+
const ast = await parser.parse(0, source, mergedLexicon);
|
|
156
159
|
|
|
157
160
|
// Log the AST pool
|
|
158
161
|
console.log("AST Pool:", JSON.stringify(ast, null, 2));
|
|
@@ -198,7 +201,7 @@ cells [
|
|
|
198
201
|
];
|
|
199
202
|
|
|
200
203
|
for (const { source, description } of tests) {
|
|
201
|
-
const ast = await parser.parse(166, source,
|
|
204
|
+
const ast = await parser.parse(166, source, mergedLexicon);
|
|
202
205
|
const unparsed = unparse(ast, l0166Lexicon);
|
|
203
206
|
|
|
204
207
|
// Check that unparse produces output
|
|
@@ -222,7 +225,7 @@ cells [
|
|
|
222
225
|
];
|
|
223
226
|
|
|
224
227
|
for (const source of tests) {
|
|
225
|
-
const ast = await parser.parse(0, source);
|
|
228
|
+
const ast = await parser.parse(0, source, mergedLexicon);
|
|
226
229
|
const unparsed = unparse(ast, l0166Lexicon);
|
|
227
230
|
|
|
228
231
|
// Should produce valid output
|
|
@@ -311,8 +314,8 @@ cells [
|
|
|
311
314
|
v: "0.0.1"
|
|
312
315
|
}..`;
|
|
313
316
|
|
|
314
|
-
// Parse with
|
|
315
|
-
const ast = await parser.parse("0166", source,
|
|
317
|
+
// Parse with merged lexicon
|
|
318
|
+
const ast = await parser.parse("0166", source, mergedLexicon);
|
|
316
319
|
|
|
317
320
|
console.log("Complex L0166 AST nodes:", Object.keys(ast).length);
|
|
318
321
|
|
|
@@ -343,8 +346,8 @@ cells [
|
|
|
343
346
|
it("should reformat L0166 code using parser.reformat", async () => {
|
|
344
347
|
const source = `columns [column A width 100 {}] rows [row 1 {}] cells [cell A1 text "Hello" {}] {v: "0.0.1"}..`;
|
|
345
348
|
|
|
346
|
-
// Reformat with
|
|
347
|
-
const reformatted = await parser.reformat("0166", source,
|
|
349
|
+
// Reformat with merged lexicon
|
|
350
|
+
const reformatted = await parser.reformat("0166", source, mergedLexicon);
|
|
348
351
|
|
|
349
352
|
// Check that it produces valid output
|
|
350
353
|
expect(reformatted).toBeDefined();
|
package/src/unparse.spec.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { parser } from "./parser.js";
|
|
2
2
|
import { unparse } from "./unparse.js";
|
|
3
|
+
import { lexicon as basisLexicon } from "@graffiticode/basis";
|
|
3
4
|
|
|
4
5
|
describe("unparse", () => {
|
|
5
6
|
// Helper function to test round-trip parsing
|
|
6
|
-
async function testRoundTrip(source,
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
async function testRoundTrip(source, dialectLexicon = {}, options = { compact: true }) {
|
|
8
|
+
// Merge basis lexicon with dialect lexicon for parsing
|
|
9
|
+
const lexicon = { ...basisLexicon, ...dialectLexicon };
|
|
10
|
+
const ast = await parser.parse(0, source, lexicon);
|
|
11
|
+
const unparsed = unparse(ast, dialectLexicon, options);
|
|
9
12
|
return unparsed;
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -293,13 +296,13 @@ describe("unparse", () => {
|
|
|
293
296
|
describe("parser.reformat", () => {
|
|
294
297
|
it("should reformat simple expressions", async () => {
|
|
295
298
|
const source = "42..";
|
|
296
|
-
const reformatted = await parser.reformat(0, source,
|
|
299
|
+
const reformatted = await parser.reformat(0, source, basisLexicon);
|
|
297
300
|
expect(reformatted).toBe("42..");
|
|
298
301
|
});
|
|
299
302
|
|
|
300
303
|
it("should reformat and pretty print lists", async () => {
|
|
301
304
|
const source = "[1,2,3]..";
|
|
302
|
-
const reformatted = await parser.reformat(0, source,
|
|
305
|
+
const reformatted = await parser.reformat(0, source, basisLexicon);
|
|
303
306
|
expect(reformatted).toContain("[\n");
|
|
304
307
|
expect(reformatted).toContain(" 1");
|
|
305
308
|
expect(reformatted).toContain(" 2");
|
|
@@ -324,7 +327,7 @@ describe("unparse", () => {
|
|
|
324
327
|
|
|
325
328
|
it("should reformat multiple expressions", async () => {
|
|
326
329
|
const source = "'hello'.[1, 2].{x: 10}..";
|
|
327
|
-
const reformatted = await parser.reformat(0, source,
|
|
330
|
+
const reformatted = await parser.reformat(0, source, basisLexicon);
|
|
328
331
|
expect(reformatted).toContain("'hello'");
|
|
329
332
|
expect(reformatted).toContain("[\n 1");
|
|
330
333
|
expect(reformatted).toContain("{\n x: 10");
|
|
@@ -333,13 +336,13 @@ describe("unparse", () => {
|
|
|
333
336
|
|
|
334
337
|
it("should support compact option", async () => {
|
|
335
338
|
const source = "[1, 2, 3]..";
|
|
336
|
-
const reformatted = await parser.reformat(0, source,
|
|
339
|
+
const reformatted = await parser.reformat(0, source, basisLexicon, { compact: true });
|
|
337
340
|
expect(reformatted).toBe("[1, 2, 3]..");
|
|
338
341
|
});
|
|
339
342
|
|
|
340
343
|
it("should support custom indent size", async () => {
|
|
341
344
|
const source = "[1, 2]..";
|
|
342
|
-
const reformatted = await parser.reformat(0, source,
|
|
345
|
+
const reformatted = await parser.reformat(0, source, basisLexicon, { indentSize: 4 });
|
|
343
346
|
expect(reformatted).toContain(" 1"); // 4 spaces
|
|
344
347
|
expect(reformatted).toContain(" 2"); // 4 spaces
|
|
345
348
|
});
|