@flexiberry/berrycore 0.1.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/LICENSE +21 -0
- package/dist/adapter/cli-adapter.d.ts +37 -0
- package/dist/adapter/cli-adapter.js +119 -0
- package/dist/berry-core.d.ts +108 -0
- package/dist/berry-core.js +258 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +18 -0
- package/dist/interpreter/environment.d.ts +45 -0
- package/dist/interpreter/environment.js +96 -0
- package/dist/interpreter/errors.d.ts +16 -0
- package/dist/interpreter/errors.js +27 -0
- package/dist/interpreter/interpreter.d.ts +111 -0
- package/dist/interpreter/interpreter.js +682 -0
- package/dist/interpreter/interpreter.types.d.ts +182 -0
- package/dist/interpreter/interpreter.types.js +73 -0
- package/dist/parser/ast/ast.engine.d.ts +103 -0
- package/dist/parser/ast/ast.engine.js +526 -0
- package/dist/parser/ast/ast.types.d.ts +242 -0
- package/dist/parser/ast/ast.types.js +37 -0
- package/dist/parser/formatter/formatter.d.ts +44 -0
- package/dist/parser/formatter/formatter.js +214 -0
- package/dist/parser/tokenizer/reader/grammer/api.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/api.grammer.js +102 -0
- package/dist/parser/tokenizer/reader/grammer/capture.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/capture.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/check.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/check.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/comment.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/comment.grammer.js +13 -0
- package/dist/parser/tokenizer/reader/grammer/conditions.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/conditions.grammer.js +68 -0
- package/dist/parser/tokenizer/reader/grammer/input.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/input.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/keyvalue.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/keyvalue.grammer.js +240 -0
- package/dist/parser/tokenizer/reader/grammer/link.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/link.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/params.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/params.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/step.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/step.grammer.js +25 -0
- package/dist/parser/tokenizer/reader/grammer/task.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/task.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/var.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/var.grammer.js +47 -0
- package/dist/parser/tokenizer/reader/lexer.engine.d.ts +43 -0
- package/dist/parser/tokenizer/reader/lexer.engine.js +178 -0
- package/dist/parser/tokenizer/reader/lexer.types.d.ts +18 -0
- package/dist/parser/tokenizer/reader/lexer.types.js +1 -0
- package/dist/parser/tokenizer/token.d.ts +13 -0
- package/dist/parser/tokenizer/token.js +13 -0
- package/dist/parser/tokenizer/tokenType.d.ts +58 -0
- package/dist/parser/tokenizer/tokenType.js +64 -0
- package/dist/script/format-util.d.ts +33 -0
- package/dist/script/format-util.js +94 -0
- package/dist/script/postman.util.d.ts +88 -0
- package/dist/script/postman.util.js +176 -0
- package/dist/script/swagger.util.d.ts +80 -0
- package/dist/script/swagger.util.js +202 -0
- package/dist/util/store-util.d.ts +5 -0
- package/dist/util/store-util.js +22 -0
- package/package.json +25 -0
- package/readme.md +107 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Engine — Recursive Descent Parser
|
|
3
|
+
*
|
|
4
|
+
* Consumes Token[] from the LexerEngine and produces a ProgramNode AST.
|
|
5
|
+
* Each parse* method corresponds to one grammar rule from reader_v2.
|
|
6
|
+
*
|
|
7
|
+
* Architecture rules enforced:
|
|
8
|
+
* - Parser ONLY produces AST — no runtime execution
|
|
9
|
+
* - Deterministic recursive descent
|
|
10
|
+
* - Throws syntax errors with line/column info
|
|
11
|
+
*/
|
|
12
|
+
import { LexerEngine } from "../tokenizer/reader/lexer.engine.js";
|
|
13
|
+
import { TokenType } from "../tokenizer/tokenType.js";
|
|
14
|
+
import { NodeType, } from "./ast.types.js";
|
|
15
|
+
// ─── Parser Errors ──────────────────────────────────────────────────────────
|
|
16
|
+
export class ParserError extends Error {
|
|
17
|
+
line;
|
|
18
|
+
column;
|
|
19
|
+
constructor(message, line, column) {
|
|
20
|
+
super(`[ParserError at ${line}:${column}] ${message}`);
|
|
21
|
+
this.line = line;
|
|
22
|
+
this.column = column;
|
|
23
|
+
this.name = "ParserError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class Ast {
|
|
27
|
+
static parse(code) {
|
|
28
|
+
const lexer = new LexerEngine(code);
|
|
29
|
+
const tokens = lexer.tokenize();
|
|
30
|
+
const engine = new AstEngine(tokens);
|
|
31
|
+
return engine.build();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// ─── Parser Engine ──────────────────────────────────────────────────────────
|
|
35
|
+
export class AstEngine {
|
|
36
|
+
tokens;
|
|
37
|
+
cursor = 0;
|
|
38
|
+
constructor(tokens) {
|
|
39
|
+
this.tokens = tokens;
|
|
40
|
+
}
|
|
41
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
42
|
+
/** Entry point: parse entire token stream into ProgramNode */
|
|
43
|
+
build() {
|
|
44
|
+
const body = [];
|
|
45
|
+
while (!this.isEof()) {
|
|
46
|
+
// Skip comments at top-level but collect them as nodes
|
|
47
|
+
if (this.check(TokenType.Comment)) {
|
|
48
|
+
body.push(this.parseComment());
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const statement = this.parseStatement();
|
|
52
|
+
if (statement) {
|
|
53
|
+
body.push(statement);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
type: NodeType.Program,
|
|
58
|
+
position: { line: 0, column: 0 },
|
|
59
|
+
body,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ── Statement Dispatch ──────────────────────────────────────────────────
|
|
63
|
+
parseStatement() {
|
|
64
|
+
if (this.check(TokenType.Var))
|
|
65
|
+
return this.parseVarDeclaration();
|
|
66
|
+
if (this.check(TokenType.Link))
|
|
67
|
+
return this.parseLinkStatement();
|
|
68
|
+
if (this.check(TokenType.Input))
|
|
69
|
+
return this.parseInputStatement();
|
|
70
|
+
if (this.check(TokenType.Api))
|
|
71
|
+
return this.parseApiBlock();
|
|
72
|
+
if (this.check(TokenType.Task))
|
|
73
|
+
return this.parseTaskBlock();
|
|
74
|
+
if (this.check(TokenType.Step))
|
|
75
|
+
return this.parseStepBlock();
|
|
76
|
+
if (this.check(TokenType.Params))
|
|
77
|
+
return this.parseParamsBlock();
|
|
78
|
+
if (this.check(TokenType.Capture))
|
|
79
|
+
return this.parseCaptureBlock();
|
|
80
|
+
if (this.check(TokenType.Check))
|
|
81
|
+
return this.parseCheckBlock();
|
|
82
|
+
if (this.check(TokenType.Comment))
|
|
83
|
+
return this.parseComment();
|
|
84
|
+
// Skip unrecognized tokens to avoid infinite loop
|
|
85
|
+
this.advance();
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// ── Var Declaration ─────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Grammar: Var ((@)(pointed) (title))? keyValueLoop
|
|
91
|
+
* Tokens: Var, optional Pointer+Pointed+Title, then key-value pairs
|
|
92
|
+
*/
|
|
93
|
+
parseVarDeclaration() {
|
|
94
|
+
const varToken = this.expect(TokenType.Var, "Expected 'Var' keyword");
|
|
95
|
+
const position = this.positionOf(varToken);
|
|
96
|
+
let pointer = null;
|
|
97
|
+
let title = null;
|
|
98
|
+
// Check for pointer reference: @ followed by pointed name
|
|
99
|
+
if (this.check(TokenType.Pointer)) {
|
|
100
|
+
pointer = this.parsePointerReference();
|
|
101
|
+
}
|
|
102
|
+
// Check for title
|
|
103
|
+
if (this.check(TokenType.Title)) {
|
|
104
|
+
title = this.advance().value;
|
|
105
|
+
}
|
|
106
|
+
// Parse key-value entries
|
|
107
|
+
const entries = this.parseKeyValuePairs();
|
|
108
|
+
return {
|
|
109
|
+
type: NodeType.VarDeclaration,
|
|
110
|
+
position,
|
|
111
|
+
title,
|
|
112
|
+
pointer,
|
|
113
|
+
entries,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
parsePointerReference() {
|
|
117
|
+
const pointerToken = this.expect(TokenType.Pointer, "Expected '@' pointer");
|
|
118
|
+
const position = this.positionOf(pointerToken);
|
|
119
|
+
const pointedToken = this.expect(TokenType.Pointed, "Expected pointed name after '@'");
|
|
120
|
+
return {
|
|
121
|
+
type: NodeType.PointerReference,
|
|
122
|
+
position,
|
|
123
|
+
symbol: pointerToken.value,
|
|
124
|
+
target: pointedToken.value,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ── Link Statement ──────────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Grammar: Link <path>
|
|
130
|
+
* Tokens: Link, LinkPath
|
|
131
|
+
*/
|
|
132
|
+
parseLinkStatement() {
|
|
133
|
+
const linkToken = this.expect(TokenType.Link, "Expected 'Link' keyword");
|
|
134
|
+
const position = this.positionOf(linkToken);
|
|
135
|
+
const pathToken = this.expect(TokenType.LinkPath, "Expected path after 'Link'");
|
|
136
|
+
return {
|
|
137
|
+
type: NodeType.LinkStatement,
|
|
138
|
+
position,
|
|
139
|
+
path: pathToken.value,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ── Input Statement ──────────────────────────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* Grammar: Input <path>
|
|
145
|
+
* Tokens: Input, InputPath
|
|
146
|
+
*/
|
|
147
|
+
parseInputStatement() {
|
|
148
|
+
const inputToken = this.expect(TokenType.Input, "Expected 'Input' keyword");
|
|
149
|
+
const position = this.positionOf(inputToken);
|
|
150
|
+
const pathToken = this.expect(TokenType.InputPath, "Expected path after 'Input'");
|
|
151
|
+
return {
|
|
152
|
+
type: NodeType.InputStatement,
|
|
153
|
+
position,
|
|
154
|
+
path: pathToken.value,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ── API Block ───────────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Grammar: Api (GET|POST|..)? #identifier title?
|
|
160
|
+
* Url value
|
|
161
|
+
* Header keyValueLoop
|
|
162
|
+
* Body type `content`
|
|
163
|
+
*/
|
|
164
|
+
parseApiBlock() {
|
|
165
|
+
const apiToken = this.expect(TokenType.Api, "Expected 'Api' keyword");
|
|
166
|
+
const position = this.positionOf(apiToken);
|
|
167
|
+
// Optional method
|
|
168
|
+
let method = null;
|
|
169
|
+
if (this.check(TokenType.ApiMethod)) {
|
|
170
|
+
method = this.advance().value;
|
|
171
|
+
}
|
|
172
|
+
// Hash + Identifier (API name)
|
|
173
|
+
if (this.check(TokenType.Hash)) {
|
|
174
|
+
this.advance(); // consume '#'
|
|
175
|
+
}
|
|
176
|
+
const nameToken = this.expect(TokenType.Identifier, "Expected API identifier after '#'");
|
|
177
|
+
const name = nameToken.value;
|
|
178
|
+
// Optional title
|
|
179
|
+
let title = null;
|
|
180
|
+
if (this.check(TokenType.Title)) {
|
|
181
|
+
title = this.advance().value;
|
|
182
|
+
}
|
|
183
|
+
// Parse sub-blocks: Url, Header, Body (in any order)
|
|
184
|
+
let url = null;
|
|
185
|
+
let headers = null;
|
|
186
|
+
let body = null;
|
|
187
|
+
while (this.check(TokenType.Url) ||
|
|
188
|
+
this.check(TokenType.Header) ||
|
|
189
|
+
this.check(TokenType.Body)) {
|
|
190
|
+
if (this.check(TokenType.Url)) {
|
|
191
|
+
url = this.parseUrlStatement();
|
|
192
|
+
}
|
|
193
|
+
else if (this.check(TokenType.Header)) {
|
|
194
|
+
headers = this.parseHeaderBlock();
|
|
195
|
+
}
|
|
196
|
+
else if (this.check(TokenType.Body)) {
|
|
197
|
+
body = this.parseBodyBlock();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
type: NodeType.ApiBlock,
|
|
202
|
+
position,
|
|
203
|
+
method,
|
|
204
|
+
name,
|
|
205
|
+
title,
|
|
206
|
+
url,
|
|
207
|
+
headers,
|
|
208
|
+
body,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
parseUrlStatement() {
|
|
212
|
+
const urlToken = this.expect(TokenType.Url, "Expected 'Url' keyword");
|
|
213
|
+
const position = this.positionOf(urlToken);
|
|
214
|
+
const valueToken = this.expect(TokenType.Value, "Expected URL value after 'Url'");
|
|
215
|
+
return {
|
|
216
|
+
type: NodeType.UrlStatement,
|
|
217
|
+
position,
|
|
218
|
+
value: valueToken.value,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
parseHeaderBlock() {
|
|
222
|
+
const headerToken = this.expect(TokenType.Header, "Expected 'Header' keyword");
|
|
223
|
+
const position = this.positionOf(headerToken);
|
|
224
|
+
const entries = this.parseKeyValuePairs();
|
|
225
|
+
return {
|
|
226
|
+
type: NodeType.HeaderBlock,
|
|
227
|
+
position,
|
|
228
|
+
entries,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
parseBodyBlock() {
|
|
232
|
+
const bodyToken = this.expect(TokenType.Body, "Expected 'Body' keyword");
|
|
233
|
+
const position = this.positionOf(bodyToken);
|
|
234
|
+
const bodyTypeToken = this.expect(TokenType.BodyType, "Expected body type (e.g. 'json') after 'Body'");
|
|
235
|
+
// Expect backtick-wrapped content
|
|
236
|
+
let content = "";
|
|
237
|
+
if (this.check(TokenType.Backtick)) {
|
|
238
|
+
this.advance(); // opening backtick
|
|
239
|
+
if (this.check(TokenType.Scalar)) {
|
|
240
|
+
content = this.advance().value;
|
|
241
|
+
}
|
|
242
|
+
if (this.check(TokenType.Backtick)) {
|
|
243
|
+
this.advance(); // closing backtick
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
type: NodeType.BodyBlock,
|
|
248
|
+
position,
|
|
249
|
+
bodyType: bodyTypeToken.value,
|
|
250
|
+
content,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ── Task Block ──────────────────────────────────────────────────────────
|
|
254
|
+
/**
|
|
255
|
+
* Grammar: Task title?
|
|
256
|
+
* Tokens: Task, Title
|
|
257
|
+
*/
|
|
258
|
+
parseTaskBlock() {
|
|
259
|
+
const taskToken = this.expect(TokenType.Task, "Expected 'Task' keyword");
|
|
260
|
+
const position = this.positionOf(taskToken);
|
|
261
|
+
let title = null;
|
|
262
|
+
if (this.check(TokenType.Title)) {
|
|
263
|
+
title = this.advance().value;
|
|
264
|
+
}
|
|
265
|
+
// Collect Step blocks that follow this Task
|
|
266
|
+
const steps = [];
|
|
267
|
+
while (this.check(TokenType.Step)) {
|
|
268
|
+
steps.push(this.parseStepBlock());
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
type: NodeType.TaskBlock,
|
|
272
|
+
position,
|
|
273
|
+
title,
|
|
274
|
+
steps,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// ── Step Block ──────────────────────────────────────────────────────────
|
|
278
|
+
/**
|
|
279
|
+
* Grammar: Step Call Api identifier
|
|
280
|
+
* Tokens: Step, Call, Api, Identifier
|
|
281
|
+
*/
|
|
282
|
+
parseStepBlock() {
|
|
283
|
+
const stepToken = this.expect(TokenType.Step, "Expected 'Step' keyword");
|
|
284
|
+
const position = this.positionOf(stepToken);
|
|
285
|
+
const callToken = this.expect(TokenType.Call, "Expected 'Call' after 'Step'");
|
|
286
|
+
const apiToken = this.expect(TokenType.Api, "Expected 'Api' after 'Call'");
|
|
287
|
+
const nameToken = this.expect(TokenType.Identifier, "Expected API name after 'Api'");
|
|
288
|
+
// Parse optional sub-blocks: Params, Capture, Check
|
|
289
|
+
let params = null;
|
|
290
|
+
let capture = null;
|
|
291
|
+
let check = null;
|
|
292
|
+
while (this.check(TokenType.Params) ||
|
|
293
|
+
this.check(TokenType.Capture) ||
|
|
294
|
+
this.check(TokenType.Check)) {
|
|
295
|
+
if (this.check(TokenType.Params)) {
|
|
296
|
+
params = this.parseParamsBlock();
|
|
297
|
+
}
|
|
298
|
+
else if (this.check(TokenType.Capture)) {
|
|
299
|
+
capture = this.parseCaptureBlock();
|
|
300
|
+
}
|
|
301
|
+
else if (this.check(TokenType.Check)) {
|
|
302
|
+
check = this.parseCheckBlock();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
type: NodeType.StepBlock,
|
|
307
|
+
position,
|
|
308
|
+
callType: callToken.value,
|
|
309
|
+
targetType: apiToken.value,
|
|
310
|
+
targetName: nameToken.value,
|
|
311
|
+
params,
|
|
312
|
+
capture,
|
|
313
|
+
check,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// ── Params Block ────────────────────────────────────────────────────────
|
|
317
|
+
/**
|
|
318
|
+
* Grammar: Params keyValueLoop
|
|
319
|
+
* Tokens: Params, then key-value pairs
|
|
320
|
+
*/
|
|
321
|
+
parseParamsBlock() {
|
|
322
|
+
const paramsToken = this.expect(TokenType.Params, "Expected 'Params' keyword");
|
|
323
|
+
const position = this.positionOf(paramsToken);
|
|
324
|
+
const entries = this.parseKeyValuePairs();
|
|
325
|
+
return {
|
|
326
|
+
type: NodeType.ParamsBlock,
|
|
327
|
+
position,
|
|
328
|
+
entries,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// ── Capture Block ───────────────────────────────────────────────────────
|
|
332
|
+
/**
|
|
333
|
+
* Grammar: Capture keyValueLoop
|
|
334
|
+
* Tokens: Capture, then key-value pairs
|
|
335
|
+
*/
|
|
336
|
+
parseCaptureBlock() {
|
|
337
|
+
const captureToken = this.expect(TokenType.Capture, "Expected 'Capture' keyword");
|
|
338
|
+
const position = this.positionOf(captureToken);
|
|
339
|
+
const entries = this.parseKeyValuePairs();
|
|
340
|
+
return {
|
|
341
|
+
type: NodeType.CaptureBlock,
|
|
342
|
+
position,
|
|
343
|
+
entries,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ── Check Block ─────────────────────────────────────────────────────────
|
|
347
|
+
/**
|
|
348
|
+
* Grammar: Check conditionLoop
|
|
349
|
+
* Tokens: Check, then condition list
|
|
350
|
+
*/
|
|
351
|
+
parseCheckBlock() {
|
|
352
|
+
const checkToken = this.expect(TokenType.Check, "Expected 'Check' keyword");
|
|
353
|
+
const position = this.positionOf(checkToken);
|
|
354
|
+
const conditions = this.parseConditions();
|
|
355
|
+
return {
|
|
356
|
+
type: NodeType.CheckBlock,
|
|
357
|
+
position,
|
|
358
|
+
conditions,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// ── Key-Value Pairs ─────────────────────────────────────────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Grammar: (- key: value)*
|
|
364
|
+
* Loops while Hyphen tokens appear
|
|
365
|
+
*/
|
|
366
|
+
parseKeyValuePairs() {
|
|
367
|
+
const entries = [];
|
|
368
|
+
while (this.check(TokenType.Hyphen)) {
|
|
369
|
+
const hyphenToken = this.advance(); // consume '-'
|
|
370
|
+
const position = this.positionOf(hyphenToken);
|
|
371
|
+
let key = "";
|
|
372
|
+
let isKeyQuoted = false;
|
|
373
|
+
let value = "";
|
|
374
|
+
let isValueQuoted = false;
|
|
375
|
+
let isMultiline = false;
|
|
376
|
+
// Key: quoted or unquoted
|
|
377
|
+
if (this.check(TokenType.Quote)) {
|
|
378
|
+
isKeyQuoted = true;
|
|
379
|
+
this.advance(); // opening quote
|
|
380
|
+
if (this.check(TokenType.Identifier)) {
|
|
381
|
+
key = this.advance().value;
|
|
382
|
+
}
|
|
383
|
+
if (this.check(TokenType.Quote)) {
|
|
384
|
+
this.advance(); // closing quote
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (this.check(TokenType.Identifier)) {
|
|
388
|
+
key = this.advance().value;
|
|
389
|
+
}
|
|
390
|
+
// Colon separator
|
|
391
|
+
if (this.check(TokenType.Colon)) {
|
|
392
|
+
this.advance();
|
|
393
|
+
}
|
|
394
|
+
// Value: quoted string, multiline backtick, or unquoted identifier
|
|
395
|
+
if (this.check(TokenType.Quote)) {
|
|
396
|
+
isValueQuoted = true;
|
|
397
|
+
this.advance(); // opening quote
|
|
398
|
+
if (this.check(TokenType.Scalar)) {
|
|
399
|
+
value = this.advance().value;
|
|
400
|
+
}
|
|
401
|
+
if (this.check(TokenType.Quote)) {
|
|
402
|
+
this.advance(); // closing quote
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (this.check(TokenType.Backtick)) {
|
|
406
|
+
isMultiline = true;
|
|
407
|
+
this.advance(); // opening backtick
|
|
408
|
+
if (this.check(TokenType.Scalar)) {
|
|
409
|
+
value = this.advance().value;
|
|
410
|
+
}
|
|
411
|
+
if (this.check(TokenType.Backtick)) {
|
|
412
|
+
this.advance(); // closing backtick
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (this.check(TokenType.Identifier)) {
|
|
416
|
+
value = this.advance().value;
|
|
417
|
+
}
|
|
418
|
+
let isEncrypted = false;
|
|
419
|
+
if (this.check(TokenType.Decrypt)) {
|
|
420
|
+
this.advance(); // consume 'Decrypt'
|
|
421
|
+
isEncrypted = true;
|
|
422
|
+
}
|
|
423
|
+
entries.push({
|
|
424
|
+
type: NodeType.KeyValuePair,
|
|
425
|
+
position,
|
|
426
|
+
key,
|
|
427
|
+
value,
|
|
428
|
+
isKeyQuoted,
|
|
429
|
+
isValueQuoted,
|
|
430
|
+
isMultiline,
|
|
431
|
+
isEncrypted,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return entries;
|
|
435
|
+
}
|
|
436
|
+
// ── Conditions ──────────────────────────────────────────────────────────
|
|
437
|
+
/**
|
|
438
|
+
* Grammar: (- lhs operator rhs (OR lhs operator rhs)*)*
|
|
439
|
+
* Loops while Hyphen tokens appear
|
|
440
|
+
*/
|
|
441
|
+
parseConditions() {
|
|
442
|
+
const conditions = [];
|
|
443
|
+
while (this.check(TokenType.Hyphen)) {
|
|
444
|
+
const hyphenToken = this.advance(); // consume '-'
|
|
445
|
+
const position = this.positionOf(hyphenToken);
|
|
446
|
+
const lhs = this.expect(TokenType.Lhs, "Expected LHS in condition").value;
|
|
447
|
+
const operator = this.expect(TokenType.Operator, "Expected operator in condition").value;
|
|
448
|
+
const rhs = this.expect(TokenType.Rhs, "Expected RHS in condition").value;
|
|
449
|
+
// Parse OR chains
|
|
450
|
+
const orConditions = [];
|
|
451
|
+
while (this.check(TokenType.Or)) {
|
|
452
|
+
const orToken = this.advance(); // consume 'OR'
|
|
453
|
+
const orPos = this.positionOf(orToken);
|
|
454
|
+
const orLhs = this.expect(TokenType.Lhs, "Expected LHS after 'OR'").value;
|
|
455
|
+
const orOperator = this.expect(TokenType.Operator, "Expected operator after OR LHS").value;
|
|
456
|
+
const orRhs = this.expect(TokenType.Rhs, "Expected RHS after OR operator").value;
|
|
457
|
+
orConditions.push({
|
|
458
|
+
type: NodeType.BinaryExpression,
|
|
459
|
+
position: orPos,
|
|
460
|
+
lhs: orLhs,
|
|
461
|
+
operator: orOperator,
|
|
462
|
+
rhs: orRhs,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
conditions.push({
|
|
466
|
+
type: NodeType.Condition,
|
|
467
|
+
position,
|
|
468
|
+
lhs,
|
|
469
|
+
operator,
|
|
470
|
+
rhs,
|
|
471
|
+
orConditions,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return conditions;
|
|
475
|
+
}
|
|
476
|
+
// ── Comment ─────────────────────────────────────────────────────────────
|
|
477
|
+
parseComment() {
|
|
478
|
+
const commentToken = this.expect(TokenType.Comment, "Expected comment text");
|
|
479
|
+
return {
|
|
480
|
+
type: NodeType.Comment,
|
|
481
|
+
position: this.positionOf(commentToken),
|
|
482
|
+
text: commentToken.value,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// ── Token Helpers ─────────────────────────────────────────────────────
|
|
486
|
+
/** Check if we've reached EOF */
|
|
487
|
+
isEof() {
|
|
488
|
+
return (this.cursor >= this.tokens.length ||
|
|
489
|
+
this.tokens[this.cursor].type === TokenType.Eof);
|
|
490
|
+
}
|
|
491
|
+
/** Peek at current token without consuming */
|
|
492
|
+
peek() {
|
|
493
|
+
return this.tokens[this.cursor];
|
|
494
|
+
}
|
|
495
|
+
/** Check if current token matches expected type */
|
|
496
|
+
check(type) {
|
|
497
|
+
if (this.isEof())
|
|
498
|
+
return false;
|
|
499
|
+
return this.peek().type === type;
|
|
500
|
+
}
|
|
501
|
+
/** Consume current token and return it */
|
|
502
|
+
advance() {
|
|
503
|
+
const token = this.tokens[this.cursor];
|
|
504
|
+
this.cursor++;
|
|
505
|
+
return token;
|
|
506
|
+
}
|
|
507
|
+
/** Expect a specific token type or throw a ParserError */
|
|
508
|
+
expect(type, errorMessage) {
|
|
509
|
+
if (this.isEof()) {
|
|
510
|
+
const lastToken = this.tokens[this.tokens.length - 1];
|
|
511
|
+
throw new ParserError(`${errorMessage} — unexpected end of input`, lastToken?.position.line ?? 0, lastToken?.position.start ?? 0);
|
|
512
|
+
}
|
|
513
|
+
const token = this.peek();
|
|
514
|
+
if (token.type !== type) {
|
|
515
|
+
throw new ParserError(`${errorMessage} — got '${TokenType[token.type]}' (${token.value})`, token.position.line ?? 0, token.position.start);
|
|
516
|
+
}
|
|
517
|
+
return this.advance();
|
|
518
|
+
}
|
|
519
|
+
/** Extract NodePosition from a Token */
|
|
520
|
+
positionOf(token) {
|
|
521
|
+
return {
|
|
522
|
+
line: token.position.line ?? 0,
|
|
523
|
+
column: token.position.start,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|