pulse-js-framework 1.10.0 → 1.10.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.
- package/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +2 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- package/runtime/router.js.original +1605 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Parser - Block Parsing
|
|
3
|
+
*
|
|
4
|
+
* Actions, router, store blocks, and function/guard parsing
|
|
5
|
+
*
|
|
6
|
+
* @module compiler/parser/blocks
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TokenType } from '../lexer.js';
|
|
10
|
+
import { NodeType, ASTNode, Parser } from './core.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// Actions Block Parsing
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse actions block
|
|
18
|
+
*/
|
|
19
|
+
Parser.prototype.parseActionsBlock = function() {
|
|
20
|
+
this.expect(TokenType.ACTIONS);
|
|
21
|
+
this.expect(TokenType.LBRACE);
|
|
22
|
+
|
|
23
|
+
const functions = [];
|
|
24
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
25
|
+
functions.push(this.parseFunctionDeclaration());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.expect(TokenType.RBRACE);
|
|
29
|
+
return new ASTNode(NodeType.ActionsBlock, { functions });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse function declaration
|
|
34
|
+
*/
|
|
35
|
+
Parser.prototype.parseFunctionDeclaration = function() {
|
|
36
|
+
let async = false;
|
|
37
|
+
if (this.is(TokenType.IDENT) && this.current().value === 'async') {
|
|
38
|
+
this.advance();
|
|
39
|
+
async = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
43
|
+
this.expect(TokenType.LPAREN);
|
|
44
|
+
|
|
45
|
+
const params = [];
|
|
46
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
47
|
+
// Accept IDENT or keyword tokens that can be used as parameter names
|
|
48
|
+
const paramToken = this.current();
|
|
49
|
+
if (this.is(TokenType.IDENT) || this.is(TokenType.PAGE) ||
|
|
50
|
+
this.is(TokenType.ROUTE) || this.is(TokenType.FROM) ||
|
|
51
|
+
this.is(TokenType.STATE) || this.is(TokenType.VIEW) ||
|
|
52
|
+
this.is(TokenType.STORE) || this.is(TokenType.ROUTER)) {
|
|
53
|
+
params.push(this.advance().value);
|
|
54
|
+
} else {
|
|
55
|
+
throw this.createError(`Expected parameter name but got ${paramToken?.type}`);
|
|
56
|
+
}
|
|
57
|
+
if (this.is(TokenType.COMMA)) {
|
|
58
|
+
this.advance();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.expect(TokenType.RPAREN);
|
|
62
|
+
|
|
63
|
+
// Parse function body as raw JS
|
|
64
|
+
this.expect(TokenType.LBRACE);
|
|
65
|
+
const body = this.parseFunctionBody();
|
|
66
|
+
this.expect(TokenType.RBRACE);
|
|
67
|
+
|
|
68
|
+
return new ASTNode(NodeType.FunctionDeclaration, { name, params, body, async });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse function body (raw content between braces)
|
|
73
|
+
*/
|
|
74
|
+
Parser.prototype.parseFunctionBody = function() {
|
|
75
|
+
// Simplified: collect all tokens until matching }
|
|
76
|
+
const statements = [];
|
|
77
|
+
let braceCount = 1;
|
|
78
|
+
|
|
79
|
+
while (!this.is(TokenType.EOF)) {
|
|
80
|
+
if (this.is(TokenType.LBRACE)) {
|
|
81
|
+
braceCount++;
|
|
82
|
+
} else if (this.is(TokenType.RBRACE)) {
|
|
83
|
+
braceCount--;
|
|
84
|
+
if (braceCount === 0) break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Collect raw token for reconstruction
|
|
88
|
+
statements.push(this.current());
|
|
89
|
+
this.advance();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return statements;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// Router Block Parsing
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse router block
|
|
101
|
+
*/
|
|
102
|
+
Parser.prototype.parseRouterBlock = function() {
|
|
103
|
+
this.expect(TokenType.ROUTER);
|
|
104
|
+
this.expect(TokenType.LBRACE);
|
|
105
|
+
|
|
106
|
+
const config = {
|
|
107
|
+
mode: 'history',
|
|
108
|
+
base: '',
|
|
109
|
+
routes: [],
|
|
110
|
+
beforeEach: null,
|
|
111
|
+
afterEach: null
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
115
|
+
// mode: "hash"
|
|
116
|
+
if (this.is(TokenType.MODE)) {
|
|
117
|
+
this.advance();
|
|
118
|
+
this.expect(TokenType.COLON);
|
|
119
|
+
config.mode = this.expect(TokenType.STRING).value;
|
|
120
|
+
}
|
|
121
|
+
// base: "/app"
|
|
122
|
+
else if (this.is(TokenType.BASE)) {
|
|
123
|
+
this.advance();
|
|
124
|
+
this.expect(TokenType.COLON);
|
|
125
|
+
config.base = this.expect(TokenType.STRING).value;
|
|
126
|
+
}
|
|
127
|
+
// routes { ... }
|
|
128
|
+
else if (this.is(TokenType.ROUTES)) {
|
|
129
|
+
config.routes = this.parseRoutesBlock();
|
|
130
|
+
}
|
|
131
|
+
// beforeEach(to, from) { ... }
|
|
132
|
+
else if (this.is(TokenType.BEFORE_EACH)) {
|
|
133
|
+
config.beforeEach = this.parseGuardHook('beforeEach');
|
|
134
|
+
}
|
|
135
|
+
// afterEach(to) { ... }
|
|
136
|
+
else if (this.is(TokenType.AFTER_EACH)) {
|
|
137
|
+
config.afterEach = this.parseGuardHook('afterEach');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw this.createError(
|
|
141
|
+
`Unexpected token '${this.current()?.value}' in router block. ` +
|
|
142
|
+
`Expected: mode, base, routes, beforeEach, or afterEach`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.expect(TokenType.RBRACE);
|
|
148
|
+
return new ASTNode(NodeType.RouterBlock, config);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse routes block
|
|
153
|
+
*/
|
|
154
|
+
Parser.prototype.parseRoutesBlock = function() {
|
|
155
|
+
this.expect(TokenType.ROUTES);
|
|
156
|
+
this.expect(TokenType.LBRACE);
|
|
157
|
+
|
|
158
|
+
const routes = [];
|
|
159
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
160
|
+
const path = this.expect(TokenType.STRING).value;
|
|
161
|
+
this.expect(TokenType.COLON);
|
|
162
|
+
const handler = this.expect(TokenType.IDENT).value;
|
|
163
|
+
routes.push(new ASTNode(NodeType.RouteDefinition, { path, handler }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.expect(TokenType.RBRACE);
|
|
167
|
+
return routes;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse guard hook: beforeEach(to, from) { ... }
|
|
172
|
+
*/
|
|
173
|
+
Parser.prototype.parseGuardHook = function(name) {
|
|
174
|
+
this.advance(); // skip keyword
|
|
175
|
+
this.expect(TokenType.LPAREN);
|
|
176
|
+
const params = [];
|
|
177
|
+
while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
|
|
178
|
+
// Accept IDENT or FROM (since 'from' is a keyword but valid as parameter name)
|
|
179
|
+
if (this.is(TokenType.IDENT)) {
|
|
180
|
+
params.push(this.advance().value);
|
|
181
|
+
} else if (this.is(TokenType.FROM)) {
|
|
182
|
+
params.push(this.advance().value);
|
|
183
|
+
} else {
|
|
184
|
+
throw this.createError(`Expected parameter name but got ${this.current()?.type}`);
|
|
185
|
+
}
|
|
186
|
+
if (this.is(TokenType.COMMA)) this.advance();
|
|
187
|
+
}
|
|
188
|
+
this.expect(TokenType.RPAREN);
|
|
189
|
+
this.expect(TokenType.LBRACE);
|
|
190
|
+
const body = this.parseFunctionBody();
|
|
191
|
+
this.expect(TokenType.RBRACE);
|
|
192
|
+
|
|
193
|
+
return new ASTNode(NodeType.GuardHook, { name, params, body });
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Store Block Parsing
|
|
198
|
+
// ============================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parse store block
|
|
202
|
+
*/
|
|
203
|
+
Parser.prototype.parseStoreBlock = function() {
|
|
204
|
+
this.expect(TokenType.STORE);
|
|
205
|
+
this.expect(TokenType.LBRACE);
|
|
206
|
+
|
|
207
|
+
const config = {
|
|
208
|
+
state: null,
|
|
209
|
+
getters: null,
|
|
210
|
+
actions: null,
|
|
211
|
+
persist: false,
|
|
212
|
+
storageKey: 'pulse-store',
|
|
213
|
+
plugins: []
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
217
|
+
// state { ... }
|
|
218
|
+
if (this.is(TokenType.STATE)) {
|
|
219
|
+
config.state = this.parseStateBlock();
|
|
220
|
+
}
|
|
221
|
+
// getters { ... }
|
|
222
|
+
else if (this.is(TokenType.GETTERS)) {
|
|
223
|
+
config.getters = this.parseGettersBlock();
|
|
224
|
+
}
|
|
225
|
+
// actions { ... }
|
|
226
|
+
else if (this.is(TokenType.ACTIONS)) {
|
|
227
|
+
config.actions = this.parseActionsBlock();
|
|
228
|
+
}
|
|
229
|
+
// persist: true
|
|
230
|
+
else if (this.is(TokenType.PERSIST)) {
|
|
231
|
+
this.advance();
|
|
232
|
+
this.expect(TokenType.COLON);
|
|
233
|
+
if (this.is(TokenType.TRUE)) {
|
|
234
|
+
this.advance();
|
|
235
|
+
config.persist = true;
|
|
236
|
+
} else if (this.is(TokenType.FALSE)) {
|
|
237
|
+
this.advance();
|
|
238
|
+
config.persist = false;
|
|
239
|
+
} else {
|
|
240
|
+
throw this.createError('Expected true or false for persist');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// storageKey: "my-store"
|
|
244
|
+
else if (this.is(TokenType.STORAGE_KEY)) {
|
|
245
|
+
this.advance();
|
|
246
|
+
this.expect(TokenType.COLON);
|
|
247
|
+
config.storageKey = this.expect(TokenType.STRING).value;
|
|
248
|
+
}
|
|
249
|
+
// plugins: [historyPlugin, loggerPlugin]
|
|
250
|
+
else if (this.is(TokenType.PLUGINS)) {
|
|
251
|
+
this.advance();
|
|
252
|
+
this.expect(TokenType.COLON);
|
|
253
|
+
config.plugins = this.parseArrayLiteral();
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
throw this.createError(
|
|
257
|
+
`Unexpected token '${this.current()?.value}' in store block. ` +
|
|
258
|
+
`Expected: state, getters, actions, persist, storageKey, or plugins`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.expect(TokenType.RBRACE);
|
|
264
|
+
return new ASTNode(NodeType.StoreBlock, config);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse getters block
|
|
269
|
+
*/
|
|
270
|
+
Parser.prototype.parseGettersBlock = function() {
|
|
271
|
+
this.expect(TokenType.GETTERS);
|
|
272
|
+
this.expect(TokenType.LBRACE);
|
|
273
|
+
|
|
274
|
+
const getters = [];
|
|
275
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
276
|
+
getters.push(this.parseGetterDeclaration());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.expect(TokenType.RBRACE);
|
|
280
|
+
return new ASTNode(NodeType.GettersBlock, { getters });
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Parse getter declaration: name() { return ... }
|
|
285
|
+
*/
|
|
286
|
+
Parser.prototype.parseGetterDeclaration = function() {
|
|
287
|
+
const name = this.expect(TokenType.IDENT).value;
|
|
288
|
+
this.expect(TokenType.LPAREN);
|
|
289
|
+
this.expect(TokenType.RPAREN);
|
|
290
|
+
this.expect(TokenType.LBRACE);
|
|
291
|
+
const body = this.parseFunctionBody();
|
|
292
|
+
this.expect(TokenType.RBRACE);
|
|
293
|
+
|
|
294
|
+
return new ASTNode(NodeType.GetterDeclaration, { name, body });
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ============================================================
|
|
298
|
+
// Router View Directives
|
|
299
|
+
// ============================================================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Parse @link directive: @link("/path") "text"
|
|
303
|
+
*/
|
|
304
|
+
Parser.prototype.parseLinkDirective = function() {
|
|
305
|
+
this.expect(TokenType.LPAREN);
|
|
306
|
+
const path = this.parseExpression();
|
|
307
|
+
|
|
308
|
+
let options = null;
|
|
309
|
+
if (this.is(TokenType.COMMA)) {
|
|
310
|
+
this.advance();
|
|
311
|
+
options = this.parseObjectLiteralExpr();
|
|
312
|
+
}
|
|
313
|
+
this.expect(TokenType.RPAREN);
|
|
314
|
+
|
|
315
|
+
// Parse link content (text or children)
|
|
316
|
+
let content = null;
|
|
317
|
+
if (this.is(TokenType.STRING)) {
|
|
318
|
+
content = this.parseTextNode();
|
|
319
|
+
} else if (this.is(TokenType.LBRACE)) {
|
|
320
|
+
this.advance();
|
|
321
|
+
content = [];
|
|
322
|
+
while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
323
|
+
content.push(this.parseViewChild());
|
|
324
|
+
}
|
|
325
|
+
this.expect(TokenType.RBRACE);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return new ASTNode(NodeType.LinkDirective, { path, options, content });
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Parse @outlet directive
|
|
333
|
+
*/
|
|
334
|
+
Parser.prototype.parseOutletDirective = function() {
|
|
335
|
+
let container = null;
|
|
336
|
+
if (this.is(TokenType.LPAREN)) {
|
|
337
|
+
this.advance();
|
|
338
|
+
if (this.is(TokenType.STRING)) {
|
|
339
|
+
container = this.expect(TokenType.STRING).value;
|
|
340
|
+
}
|
|
341
|
+
this.expect(TokenType.RPAREN);
|
|
342
|
+
}
|
|
343
|
+
return new ASTNode(NodeType.OutletDirective, { container });
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parse @navigate directive
|
|
348
|
+
*/
|
|
349
|
+
Parser.prototype.parseNavigateDirective = function() {
|
|
350
|
+
this.expect(TokenType.LPAREN);
|
|
351
|
+
const path = this.parseExpression();
|
|
352
|
+
|
|
353
|
+
let options = null;
|
|
354
|
+
if (this.is(TokenType.COMMA)) {
|
|
355
|
+
this.advance();
|
|
356
|
+
options = this.parseObjectLiteralExpr();
|
|
357
|
+
}
|
|
358
|
+
this.expect(TokenType.RPAREN);
|
|
359
|
+
|
|
360
|
+
return new ASTNode(NodeType.NavigateDirective, { path, options });
|
|
361
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Parser Core - Base parser infrastructure
|
|
3
|
+
*
|
|
4
|
+
* Contains NodeType constants, ASTNode class, and base Parser class with utility methods
|
|
5
|
+
*
|
|
6
|
+
* @module compiler/parser/core
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TokenType } from '../lexer.js';
|
|
10
|
+
import { ParserError, SUGGESTIONS, getDocsUrl } from '../../runtime/errors.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// AST Node Types
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
export const NodeType = {
|
|
17
|
+
Program: 'Program',
|
|
18
|
+
ImportDeclaration: 'ImportDeclaration',
|
|
19
|
+
ImportSpecifier: 'ImportSpecifier',
|
|
20
|
+
PageDeclaration: 'PageDeclaration',
|
|
21
|
+
RouteDeclaration: 'RouteDeclaration',
|
|
22
|
+
PropsBlock: 'PropsBlock',
|
|
23
|
+
StateBlock: 'StateBlock',
|
|
24
|
+
ViewBlock: 'ViewBlock',
|
|
25
|
+
ActionsBlock: 'ActionsBlock',
|
|
26
|
+
StyleBlock: 'StyleBlock',
|
|
27
|
+
SlotElement: 'SlotElement',
|
|
28
|
+
Element: 'Element',
|
|
29
|
+
TextNode: 'TextNode',
|
|
30
|
+
Interpolation: 'Interpolation',
|
|
31
|
+
Directive: 'Directive',
|
|
32
|
+
IfDirective: 'IfDirective',
|
|
33
|
+
EachDirective: 'EachDirective',
|
|
34
|
+
EventDirective: 'EventDirective',
|
|
35
|
+
ModelDirective: 'ModelDirective',
|
|
36
|
+
|
|
37
|
+
// Accessibility directives
|
|
38
|
+
A11yDirective: 'A11yDirective',
|
|
39
|
+
LiveDirective: 'LiveDirective',
|
|
40
|
+
FocusTrapDirective: 'FocusTrapDirective',
|
|
41
|
+
|
|
42
|
+
// SSR directives
|
|
43
|
+
ClientDirective: 'ClientDirective',
|
|
44
|
+
ServerDirective: 'ServerDirective',
|
|
45
|
+
|
|
46
|
+
Property: 'Property',
|
|
47
|
+
ObjectLiteral: 'ObjectLiteral',
|
|
48
|
+
ArrayLiteral: 'ArrayLiteral',
|
|
49
|
+
Identifier: 'Identifier',
|
|
50
|
+
MemberExpression: 'MemberExpression',
|
|
51
|
+
CallExpression: 'CallExpression',
|
|
52
|
+
BinaryExpression: 'BinaryExpression',
|
|
53
|
+
UnaryExpression: 'UnaryExpression',
|
|
54
|
+
UpdateExpression: 'UpdateExpression',
|
|
55
|
+
Literal: 'Literal',
|
|
56
|
+
TemplateLiteral: 'TemplateLiteral',
|
|
57
|
+
ConditionalExpression: 'ConditionalExpression',
|
|
58
|
+
ArrowFunction: 'ArrowFunction',
|
|
59
|
+
SpreadElement: 'SpreadElement',
|
|
60
|
+
AssignmentExpression: 'AssignmentExpression',
|
|
61
|
+
FunctionDeclaration: 'FunctionDeclaration',
|
|
62
|
+
StyleRule: 'StyleRule',
|
|
63
|
+
StyleProperty: 'StyleProperty',
|
|
64
|
+
|
|
65
|
+
// Router nodes
|
|
66
|
+
RouterBlock: 'RouterBlock',
|
|
67
|
+
RoutesBlock: 'RoutesBlock',
|
|
68
|
+
RouteDefinition: 'RouteDefinition',
|
|
69
|
+
GuardHook: 'GuardHook',
|
|
70
|
+
|
|
71
|
+
// Store nodes
|
|
72
|
+
StoreBlock: 'StoreBlock',
|
|
73
|
+
GettersBlock: 'GettersBlock',
|
|
74
|
+
GetterDeclaration: 'GetterDeclaration',
|
|
75
|
+
|
|
76
|
+
// View directives for router
|
|
77
|
+
LinkDirective: 'LinkDirective',
|
|
78
|
+
OutletDirective: 'OutletDirective',
|
|
79
|
+
NavigateDirective: 'NavigateDirective'
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ============================================================
|
|
83
|
+
// AST Node Class
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* AST Node class
|
|
88
|
+
*/
|
|
89
|
+
export class ASTNode {
|
|
90
|
+
constructor(type, props = {}) {
|
|
91
|
+
this.type = type;
|
|
92
|
+
Object.assign(this, props);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================
|
|
97
|
+
// Base Parser Class
|
|
98
|
+
// ============================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parser class with base utility methods
|
|
102
|
+
* Parse methods are added via modules (imports, state, view, style, expressions, blocks)
|
|
103
|
+
*/
|
|
104
|
+
export class Parser {
|
|
105
|
+
constructor(tokens) {
|
|
106
|
+
this.tokens = tokens;
|
|
107
|
+
this.pos = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get current token
|
|
112
|
+
*/
|
|
113
|
+
current() {
|
|
114
|
+
return this.tokens[this.pos];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Peek at token at offset
|
|
119
|
+
*/
|
|
120
|
+
peek(offset = 1) {
|
|
121
|
+
const index = this.pos + offset;
|
|
122
|
+
if (index < 0 || index >= this.tokens.length) return undefined;
|
|
123
|
+
return this.tokens[index];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if current token matches type
|
|
128
|
+
*/
|
|
129
|
+
is(type) {
|
|
130
|
+
return this.current()?.type === type;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if current token matches any of types
|
|
135
|
+
*/
|
|
136
|
+
isAny(...types) {
|
|
137
|
+
return types.includes(this.current()?.type);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Advance to next token and return current
|
|
142
|
+
*/
|
|
143
|
+
advance() {
|
|
144
|
+
return this.tokens[this.pos++];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Expect a specific token type
|
|
149
|
+
*/
|
|
150
|
+
expect(type, message = null) {
|
|
151
|
+
if (!this.is(type)) {
|
|
152
|
+
const token = this.current();
|
|
153
|
+
throw this.createError(
|
|
154
|
+
message || `Expected ${type} but got ${token?.type}`,
|
|
155
|
+
token,
|
|
156
|
+
{ suggestion: SUGGESTIONS['unexpected-token']?.(type, token?.type) }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return this.advance();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a parse error with detailed information
|
|
164
|
+
* @param {string} message - Error message
|
|
165
|
+
* @param {Object} [token] - Token where error occurred
|
|
166
|
+
* @param {Object} [options] - Additional options (suggestion, code)
|
|
167
|
+
* @returns {ParserError} The parser error
|
|
168
|
+
*/
|
|
169
|
+
createError(message, token = null, options = {}) {
|
|
170
|
+
const t = token || this.current();
|
|
171
|
+
const code = options.code || 'PARSER_ERROR';
|
|
172
|
+
|
|
173
|
+
// Build enhanced message with docs link
|
|
174
|
+
let enhancedMessage = message;
|
|
175
|
+
if (options.suggestion) {
|
|
176
|
+
enhancedMessage += `\n → ${options.suggestion}`;
|
|
177
|
+
}
|
|
178
|
+
enhancedMessage += `\n See: ${getDocsUrl(code)}`;
|
|
179
|
+
|
|
180
|
+
return new ParserError(enhancedMessage, {
|
|
181
|
+
line: t?.line || 1,
|
|
182
|
+
column: t?.column || 1,
|
|
183
|
+
token: t,
|
|
184
|
+
code,
|
|
185
|
+
...options
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse the entire program
|
|
191
|
+
* This method delegates to specialized parse methods from other modules
|
|
192
|
+
*/
|
|
193
|
+
parse() {
|
|
194
|
+
const program = new ASTNode(NodeType.Program, {
|
|
195
|
+
imports: [],
|
|
196
|
+
page: null,
|
|
197
|
+
route: null,
|
|
198
|
+
props: null,
|
|
199
|
+
state: null,
|
|
200
|
+
view: null,
|
|
201
|
+
actions: null,
|
|
202
|
+
style: null,
|
|
203
|
+
router: null,
|
|
204
|
+
store: null
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
while (!this.is(TokenType.EOF)) {
|
|
208
|
+
// Import declarations (must come first)
|
|
209
|
+
if (this.is(TokenType.IMPORT)) {
|
|
210
|
+
program.imports.push(this.parseImportDeclaration());
|
|
211
|
+
}
|
|
212
|
+
// Page/Route declarations
|
|
213
|
+
else if (this.is(TokenType.AT)) {
|
|
214
|
+
this.advance();
|
|
215
|
+
if (this.is(TokenType.PAGE)) {
|
|
216
|
+
program.page = this.parsePageDeclaration();
|
|
217
|
+
} else if (this.is(TokenType.ROUTE)) {
|
|
218
|
+
program.route = this.parseRouteDeclaration();
|
|
219
|
+
} else {
|
|
220
|
+
throw this.createError(
|
|
221
|
+
`Expected 'page' or 'route' after '@', got '${this.current()?.value}'`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Props block
|
|
226
|
+
else if (this.is(TokenType.PROPS)) {
|
|
227
|
+
if (program.props) {
|
|
228
|
+
throw this.createError('Duplicate props block - only one props block allowed per file', null, {
|
|
229
|
+
code: 'DUPLICATE_BLOCK',
|
|
230
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('props')
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
program.props = this.parsePropsBlock();
|
|
234
|
+
}
|
|
235
|
+
// State block
|
|
236
|
+
else if (this.is(TokenType.STATE)) {
|
|
237
|
+
if (program.state) {
|
|
238
|
+
throw this.createError('Duplicate state block - only one state block allowed per file', null, {
|
|
239
|
+
code: 'DUPLICATE_BLOCK',
|
|
240
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('state')
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
program.state = this.parseStateBlock();
|
|
244
|
+
}
|
|
245
|
+
// View block
|
|
246
|
+
else if (this.is(TokenType.VIEW)) {
|
|
247
|
+
if (program.view) {
|
|
248
|
+
throw this.createError('Duplicate view block - only one view block allowed per file', null, {
|
|
249
|
+
code: 'DUPLICATE_BLOCK',
|
|
250
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('view')
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
program.view = this.parseViewBlock();
|
|
254
|
+
}
|
|
255
|
+
// Actions block
|
|
256
|
+
else if (this.is(TokenType.ACTIONS)) {
|
|
257
|
+
if (program.actions) {
|
|
258
|
+
throw this.createError('Duplicate actions block - only one actions block allowed per file', null, {
|
|
259
|
+
code: 'DUPLICATE_BLOCK',
|
|
260
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('actions')
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
program.actions = this.parseActionsBlock();
|
|
264
|
+
}
|
|
265
|
+
// Style block
|
|
266
|
+
else if (this.is(TokenType.STYLE)) {
|
|
267
|
+
if (program.style) {
|
|
268
|
+
throw this.createError('Duplicate style block - only one style block allowed per file', null, {
|
|
269
|
+
code: 'DUPLICATE_BLOCK',
|
|
270
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('style')
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
program.style = this.parseStyleBlock();
|
|
274
|
+
}
|
|
275
|
+
// Router block
|
|
276
|
+
else if (this.is(TokenType.ROUTER)) {
|
|
277
|
+
if (program.router) {
|
|
278
|
+
throw this.createError('Duplicate router block - only one router block allowed per file', null, {
|
|
279
|
+
code: 'DUPLICATE_BLOCK',
|
|
280
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('router')
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
program.router = this.parseRouterBlock();
|
|
284
|
+
}
|
|
285
|
+
// Store block
|
|
286
|
+
else if (this.is(TokenType.STORE)) {
|
|
287
|
+
if (program.store) {
|
|
288
|
+
throw this.createError('Duplicate store block - only one store block allowed per file', null, {
|
|
289
|
+
code: 'DUPLICATE_BLOCK',
|
|
290
|
+
suggestion: SUGGESTIONS['duplicate-declaration']?.('store')
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
program.store = this.parseStoreBlock();
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const token = this.current();
|
|
297
|
+
throw this.createError(
|
|
298
|
+
`Unexpected token '${token?.value || token?.type}' at line ${token?.line}:${token?.column}. ` +
|
|
299
|
+
`Expected: import, @page, @route, props, state, view, actions, style, router, or store`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return program;
|
|
305
|
+
}
|
|
306
|
+
}
|