@ozsarman/clarityjs 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js Parser — AST Builder
|
|
3
|
+
*
|
|
4
|
+
* Converts a token stream into an Abstract Syntax Tree.
|
|
5
|
+
* Every node carries source location for LLM-friendly error messages.
|
|
6
|
+
*
|
|
7
|
+
* Grammar (simplified):
|
|
8
|
+
*
|
|
9
|
+
* Program → (Import | Component)*
|
|
10
|
+
* Import → 'import' IdentList 'from' String
|
|
11
|
+
* Component → 'component' Ident Params '{' Block '}'
|
|
12
|
+
* Block → (StateDecl | EffectDecl | ComputedDecl | RenderBlock | ServerBlock)*
|
|
13
|
+
* StateDecl → 'state' Ident '=' Expr
|
|
14
|
+
* ComputedDecl → 'computed' Ident '=' Expr
|
|
15
|
+
* EffectDecl → 'effect' 'on' '(' IdentList ')' '{' Stmts '}'
|
|
16
|
+
* RenderBlock → 'render' '{' JSXElement '}'
|
|
17
|
+
* ServerBlock → 'server' '{' Stmts '}'
|
|
18
|
+
* JSXElement → '<' TagName Attrs '>' Children '</' TagName '>'
|
|
19
|
+
* | '<' TagName Attrs '/>'
|
|
20
|
+
*
|
|
21
|
+
* Author: Claude (Anthropic)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { T, Lexer } from './lexer.js';
|
|
25
|
+
|
|
26
|
+
// ─── ParseError ───────────────────────────────────────────────────────────────
|
|
27
|
+
export class ParseError extends Error {
|
|
28
|
+
constructor(message, token, source = '') {
|
|
29
|
+
const line = token?.line ?? '?';
|
|
30
|
+
const col = token?.col ?? '?';
|
|
31
|
+
const lines = source.split('\n');
|
|
32
|
+
const snippet = lines[line - 1] ?? '';
|
|
33
|
+
const pointer = ' '.repeat(Math.max(0, col - 1)) + '^';
|
|
34
|
+
|
|
35
|
+
super(
|
|
36
|
+
`[Clarity Parser] ${message}\n` +
|
|
37
|
+
` → Line ${line}, Col ${col}\n` +
|
|
38
|
+
` ${snippet}\n` +
|
|
39
|
+
` ${pointer}\n` +
|
|
40
|
+
` Got: ${token ? `${token.type}(${JSON.stringify(token.value)})` : 'EOF'}\n` +
|
|
41
|
+
` LLM-hint: Check syntax near this position. Every block must be opened and closed.`
|
|
42
|
+
);
|
|
43
|
+
this.name = 'ParseError';
|
|
44
|
+
this.token = token;
|
|
45
|
+
this.line = line;
|
|
46
|
+
this.col = col;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── AST Node Factories ───────────────────────────────────────────────────────
|
|
51
|
+
// Each factory produces a plain object with a 'type' field.
|
|
52
|
+
// Plain objects = easy to inspect, serialize, and reason about.
|
|
53
|
+
|
|
54
|
+
const node = {
|
|
55
|
+
program: (imports, components, stores, loc) => ({ type: 'Program', imports, components, stores, loc }),
|
|
56
|
+
import: (names, source, loc) => ({ type: 'Import', names, source, loc }),
|
|
57
|
+
component: (name, params, body, loc, typeParams) => ({ type: 'Component', name, params, body, loc, typeParams: typeParams ?? [] }),
|
|
58
|
+
stateDecl: (name, init, loc) => ({ type: 'StateDecl', name, init, loc }),
|
|
59
|
+
computedDecl: (name, expr, loc) => ({ type: 'ComputedDecl', name, expr, loc }),
|
|
60
|
+
effectDecl: (deps, body, loc) => ({ type: 'EffectDecl', deps, body, loc }),
|
|
61
|
+
renderBlock: (root, loc) => ({ type: 'RenderBlock', root, loc }),
|
|
62
|
+
aiDecl: (capability, targets, loc) => ({ type: 'AIDecl', capability, targets, loc }),
|
|
63
|
+
beforeMountBlock: (body, loc) => ({ type: 'BeforeMountBlock', body, loc }),
|
|
64
|
+
onMountBlock: (body, loc) => ({ type: 'OnMountBlock', body, loc }),
|
|
65
|
+
onCleanupBlock: (body, loc) => ({ type: 'OnCleanupBlock', body, loc }),
|
|
66
|
+
actionDecl: (name, params, body, loc) => ({ type: 'ActionDecl', name, params, body, loc }),
|
|
67
|
+
serverBlock: (data, loc) => ({ type: 'ServerBlock', data, loc }),
|
|
68
|
+
serverDataDecl: (name, init, loc) => ({ type: 'ServerDataDecl', name, init, loc }),
|
|
69
|
+
awaitExpr: (expr, loc) => ({ type: 'AwaitExpr', expr, loc }),
|
|
70
|
+
|
|
71
|
+
// JSX nodes
|
|
72
|
+
jsxElement: (tag, attrs, children, selfClose, loc) => ({ type: 'JSXElement', tag, attrs, children, selfClose, loc }),
|
|
73
|
+
jsxText: (value, loc) => ({ type: 'JSXText', value, loc }),
|
|
74
|
+
jsxExpr: (expr, loc) => ({ type: 'JSXExpr', expr, loc }),
|
|
75
|
+
|
|
76
|
+
// Attribute nodes
|
|
77
|
+
attr: (name, value, loc) => ({ type: 'Attr', name, value, loc }),
|
|
78
|
+
eventAttr: (event, modifiers, handler, loc) => ({ type: 'EventAttr', event, modifiers, handler, loc }),
|
|
79
|
+
bindAttr: (prop, signal, loc) => ({ type: 'BindAttr', prop, signal, loc }),
|
|
80
|
+
|
|
81
|
+
// Expression nodes
|
|
82
|
+
literal: (value, loc) => ({ type: 'Literal', value, loc }),
|
|
83
|
+
ident: (name, loc) => ({ type: 'Ident', name, loc }),
|
|
84
|
+
binary: (op, left, right, loc) => ({ type: 'BinaryExpr', op, left, right, loc }),
|
|
85
|
+
unary: (op, expr, loc) => ({ type: 'UnaryExpr', op, expr, loc }),
|
|
86
|
+
update: (op, expr, prefix, loc) => ({ type: 'UpdateExpr', op, expr, prefix, loc }),
|
|
87
|
+
ternary: (cond, then, else_, loc) => ({ type: 'TernaryExpr', cond, then, else: else_, loc }),
|
|
88
|
+
assign: (target, op, value, loc) => ({ type: 'AssignExpr', target, op, value, loc }),
|
|
89
|
+
member: (obj, prop, computed, loc)=> ({ type: 'MemberExpr', obj, prop, computed, loc }),
|
|
90
|
+
call: (callee, args, loc) => ({ type: 'CallExpr', callee, args, loc }),
|
|
91
|
+
arrow: (params, body, loc) => ({ type: 'ArrowFn', params, body, loc }),
|
|
92
|
+
array: (elements, loc) => ({ type: 'ArrayExpr', elements, loc }),
|
|
93
|
+
object: (properties, loc) => ({ type: 'ObjectExpr', properties, loc }),
|
|
94
|
+
prop: (key, value, loc) => ({ type: 'ObjectProp', key, value, loc }),
|
|
95
|
+
template: (parts, loc) => ({ type: 'TemplateLiteral', parts, loc }),
|
|
96
|
+
|
|
97
|
+
// Statement nodes
|
|
98
|
+
exprStmt: (expr, loc) => ({ type: 'ExprStmt', expr, loc }),
|
|
99
|
+
returnStmt: (value, loc) => ({ type: 'ReturnStmt', value, loc }),
|
|
100
|
+
ifStmt: (cond, then, else_, loc) => ({ type: 'IfStmt', cond, then, else: else_, loc }),
|
|
101
|
+
block: (body, loc) => ({ type: 'Block', body, loc }),
|
|
102
|
+
varDecl: (kind, name, init, loc) => ({ type: 'VarDecl', kind, name, init, loc }),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
106
|
+
export class Parser {
|
|
107
|
+
constructor(tokens, source = '') {
|
|
108
|
+
this.tokens = tokens.filter(t => t.type !== T.NEWLINE); // skip newlines for now
|
|
109
|
+
this.source = source;
|
|
110
|
+
this.pos = 0;
|
|
111
|
+
this._inAttrValue = false; // prevents comma from being parsed as CommaExpr in lists
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Public ──
|
|
115
|
+
parse() {
|
|
116
|
+
const loc = this._loc();
|
|
117
|
+
const imports = [];
|
|
118
|
+
const components = [];
|
|
119
|
+
const stores = [];
|
|
120
|
+
|
|
121
|
+
while (!this._atEnd()) {
|
|
122
|
+
if (this._check(T.IMPORT)) {
|
|
123
|
+
imports.push(this._parseImport());
|
|
124
|
+
} else if (this._check(T.COMPONENT)) {
|
|
125
|
+
components.push(this._parseComponent());
|
|
126
|
+
} else if (this._check(T.IDENT) && this._current().value === 'store') {
|
|
127
|
+
const storeLoc = this._loc();
|
|
128
|
+
this._advance(); // consume 'store'
|
|
129
|
+
const name = this._eat(T.IDENT).value;
|
|
130
|
+
this._eat(T.ASSIGN);
|
|
131
|
+
const init = this._parseExpr();
|
|
132
|
+
stores.push({ type: 'StoreDecl', name, init, loc: storeLoc });
|
|
133
|
+
} else if (this._check(T.EOF)) {
|
|
134
|
+
break;
|
|
135
|
+
} else {
|
|
136
|
+
this._advance(); // skip unknown top-level tokens
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return node.program(imports, components, stores, loc);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Top-Level Parsers ──
|
|
144
|
+
_parseImport() {
|
|
145
|
+
const loc = this._loc();
|
|
146
|
+
this._eat(T.IMPORT);
|
|
147
|
+
|
|
148
|
+
const names = [];
|
|
149
|
+
if (this._check(T.LBRACE)) {
|
|
150
|
+
this._eat(T.LBRACE);
|
|
151
|
+
while (!this._check(T.RBRACE) && !this._atEnd()) {
|
|
152
|
+
names.push(this._eat(T.IDENT).value);
|
|
153
|
+
if (this._check(T.COMMA)) this._advance();
|
|
154
|
+
}
|
|
155
|
+
this._eat(T.RBRACE);
|
|
156
|
+
} else {
|
|
157
|
+
names.push(this._eat(T.IDENT).value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._eat(T.FROM);
|
|
161
|
+
const source = this._eat(T.STRING).value;
|
|
162
|
+
return node.import(names, source, loc);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_parseComponent() {
|
|
166
|
+
const loc = this._loc();
|
|
167
|
+
this._eat(T.COMPONENT);
|
|
168
|
+
const name = this._eat(T.IDENT).value;
|
|
169
|
+
|
|
170
|
+
// ── Generic type parameters: component List<T> or component Map<K, V extends string>
|
|
171
|
+
// The lexer converts <T into JSX_OPEN(T), so we detect that here.
|
|
172
|
+
// Type params are purely for type-checking (TypeScript/Volar); the JS codegen ignores them.
|
|
173
|
+
const typeParams = [];
|
|
174
|
+
if (this._check(T.JSX_OPEN)) {
|
|
175
|
+
// Grab the first type param from the JSX_OPEN token value (e.g. "T" from <T)
|
|
176
|
+
const firstParam = this._advance().value; // JSX_OPEN value = tag name = first type ident
|
|
177
|
+
if (firstParam) {
|
|
178
|
+
typeParams.push(this._parseTypeParam(firstParam));
|
|
179
|
+
}
|
|
180
|
+
// Parse additional params: , U, , V extends Base
|
|
181
|
+
while (this._check(T.COMMA)) {
|
|
182
|
+
this._advance();
|
|
183
|
+
if (this._check(T.IDENT)) {
|
|
184
|
+
const paramName = this._advance().value;
|
|
185
|
+
typeParams.push(this._parseTypeParam(paramName));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Consume the closing > (GT token)
|
|
189
|
+
if (this._check(T.GT)) this._advance();
|
|
190
|
+
// Also handle JSX_SELF_CLOSE (/>) which shouldn't appear here but guard anyway
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Parse params: (name: Type, ...)
|
|
194
|
+
const params = [];
|
|
195
|
+
this._eat(T.LPAREN);
|
|
196
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
197
|
+
const paramName = this._eat(T.IDENT).value;
|
|
198
|
+
let paramType = null;
|
|
199
|
+
if (this._check(T.COLON)) {
|
|
200
|
+
this._advance();
|
|
201
|
+
paramType = this._eat(T.IDENT).value;
|
|
202
|
+
}
|
|
203
|
+
params.push({ name: paramName, type: paramType });
|
|
204
|
+
if (this._check(T.COMMA)) this._advance();
|
|
205
|
+
}
|
|
206
|
+
this._eat(T.RPAREN);
|
|
207
|
+
|
|
208
|
+
const body = this._parseComponentBody();
|
|
209
|
+
return node.component(name, params, body, loc, typeParams);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse a single type parameter from the given name string.
|
|
214
|
+
* Handles:
|
|
215
|
+
* T → { name: 'T', constraint: null }
|
|
216
|
+
* T extends string → { name: 'T', constraint: 'string' }
|
|
217
|
+
*
|
|
218
|
+
* The `extends` keyword appears as T.IDENT with value 'extends' in this context.
|
|
219
|
+
*/
|
|
220
|
+
_parseTypeParam(paramName) {
|
|
221
|
+
let constraint = null;
|
|
222
|
+
// Check for "extends BaseType" — appears as IDENT('extends') IDENT(type)
|
|
223
|
+
if (this._check(T.IDENT) && this._current().value === 'extends') {
|
|
224
|
+
this._advance(); // consume 'extends'
|
|
225
|
+
const parts = [];
|
|
226
|
+
// Read the constraint type (may be dotted: string, SomeInterface, React.FC)
|
|
227
|
+
if (this._check(T.IDENT)) {
|
|
228
|
+
parts.push(this._advance().value);
|
|
229
|
+
while (this._check(T.DOT) && this._peekTypeIsIdent()) {
|
|
230
|
+
this._advance(); // .
|
|
231
|
+
parts.push(this._advance().value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
constraint = parts.join('.') || null;
|
|
235
|
+
}
|
|
236
|
+
return { name: paramName, constraint };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_peekTypeIsIdent() {
|
|
240
|
+
return this._pos + 1 < this._tokens.length && this._tokens[this._pos + 1]?.type === T.IDENT;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_parseComponentBody() {
|
|
244
|
+
this._eat(T.LBRACE);
|
|
245
|
+
const body = [];
|
|
246
|
+
|
|
247
|
+
while (!this._check(T.RBRACE) && !this._atEnd()) {
|
|
248
|
+
if (this._check(T.STATE)) {
|
|
249
|
+
body.push(this._parseStateDecl());
|
|
250
|
+
} else if (this._check(T.COMPUTED)) {
|
|
251
|
+
body.push(this._parseComputedDecl());
|
|
252
|
+
} else if (this._check(T.EFFECT)) {
|
|
253
|
+
body.push(this._parseEffectDecl());
|
|
254
|
+
} else if (this._check(T.RENDER)) {
|
|
255
|
+
body.push(this._parseRenderBlock());
|
|
256
|
+
} else if (this._check(T.SERVER)) {
|
|
257
|
+
body.push(this._parseServerBlock());
|
|
258
|
+
} else if (this._check(T.AI)) {
|
|
259
|
+
body.push(this._parseAIDecl());
|
|
260
|
+
} else if (this._check(T.BEFORE_MOUNT)) {
|
|
261
|
+
body.push(this._parseBeforeMountBlock());
|
|
262
|
+
} else if (this._check(T.ON_MOUNT)) {
|
|
263
|
+
body.push(this._parseOnMountBlock());
|
|
264
|
+
} else if (this._check(T.ON_CLEANUP)) {
|
|
265
|
+
body.push(this._parseOnCleanupBlock());
|
|
266
|
+
} else if (this._check(T.ACTION)) {
|
|
267
|
+
body.push(this._parseActionDecl());
|
|
268
|
+
} else if (this._check(T.IDENT) && this._current().value === 'ref') {
|
|
269
|
+
const loc = this._loc();
|
|
270
|
+
this._advance(); // consume 'ref'
|
|
271
|
+
const name = this._eat(T.IDENT).value;
|
|
272
|
+
body.push({ type: 'RefDecl', name, loc });
|
|
273
|
+
} else if (this._check(T.IDENT) && this._current().value === 'async') {
|
|
274
|
+
const loc = this._loc();
|
|
275
|
+
this._advance(); // consume 'async'
|
|
276
|
+
this._eat(T.STATE); // expect 'state'
|
|
277
|
+
const name = this._eat(T.IDENT).value;
|
|
278
|
+
this._eat(T.ASSIGN);
|
|
279
|
+
const init = this._parseExpr();
|
|
280
|
+
body.push({ type: 'AsyncStateDecl', name, init, loc });
|
|
281
|
+
} else if (this._check(T.STYLE_BLOCK)) {
|
|
282
|
+
// CSS scoping block: style { ... }
|
|
283
|
+
// The lexer already consumed the braces and extracted raw CSS as token value.
|
|
284
|
+
const loc = this._loc();
|
|
285
|
+
const tok = this._advance();
|
|
286
|
+
body.push({ type: 'StyleBlock', css: tok.value, loc });
|
|
287
|
+
} else {
|
|
288
|
+
// Skip unknown tokens inside component body
|
|
289
|
+
this._advance();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this._eat(T.RBRACE);
|
|
294
|
+
return body;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_parseStateDecl() {
|
|
298
|
+
const loc = this._loc();
|
|
299
|
+
this._eat(T.STATE);
|
|
300
|
+
const name = this._eat(T.IDENT).value;
|
|
301
|
+
this._eat(T.ASSIGN);
|
|
302
|
+
const init = this._parseExpr();
|
|
303
|
+
return node.stateDecl(name, init, loc);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_parseComputedDecl() {
|
|
307
|
+
const loc = this._loc();
|
|
308
|
+
this._eat(T.COMPUTED);
|
|
309
|
+
const name = this._eat(T.IDENT).value;
|
|
310
|
+
this._eat(T.ASSIGN);
|
|
311
|
+
const expr = this._parseExpr();
|
|
312
|
+
return node.computedDecl(name, expr, loc);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
_parseEffectDecl() {
|
|
316
|
+
const loc = this._loc();
|
|
317
|
+
this._eat(T.EFFECT);
|
|
318
|
+
this._eat(T.ON);
|
|
319
|
+
this._eat(T.LPAREN);
|
|
320
|
+
|
|
321
|
+
const deps = [];
|
|
322
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
323
|
+
deps.push(this._eat(T.IDENT).value);
|
|
324
|
+
if (this._check(T.COMMA)) this._advance();
|
|
325
|
+
}
|
|
326
|
+
this._eat(T.RPAREN);
|
|
327
|
+
|
|
328
|
+
const body = this._parseBlock();
|
|
329
|
+
return node.effectDecl(deps, body, loc);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_parseRenderBlock() {
|
|
333
|
+
const loc = this._loc();
|
|
334
|
+
this._eat(T.RENDER);
|
|
335
|
+
this._eat(T.LBRACE);
|
|
336
|
+
const root = this._parseJSXElement();
|
|
337
|
+
this._eat(T.RBRACE);
|
|
338
|
+
return node.renderBlock(root, loc);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
_parseServerBlock() {
|
|
342
|
+
const loc = this._loc();
|
|
343
|
+
this._eat(T.SERVER);
|
|
344
|
+
this._eat(T.LBRACE);
|
|
345
|
+
const data = [];
|
|
346
|
+
while (!this._check(T.RBRACE) && !this._atEnd()) {
|
|
347
|
+
// data name = expr (expr may contain `await fetch(...)`)
|
|
348
|
+
const declLoc = this._loc();
|
|
349
|
+
this._eat(T.DATA);
|
|
350
|
+
const name = this._eat(T.IDENT).value;
|
|
351
|
+
this._eat(T.ASSIGN);
|
|
352
|
+
const init = this._parseExpr();
|
|
353
|
+
data.push(node.serverDataDecl(name, init, declLoc));
|
|
354
|
+
// optional semicolon
|
|
355
|
+
if (this._check(T.SEMICOLON)) this._advance();
|
|
356
|
+
}
|
|
357
|
+
this._eat(T.RBRACE);
|
|
358
|
+
return node.serverBlock(data, loc);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_parseAIDecl() {
|
|
362
|
+
const loc = this._loc();
|
|
363
|
+
this._eat(T.AI);
|
|
364
|
+
this._eat(T.COLON);
|
|
365
|
+
const capability = this._eat(T.IDENT).value; // readable, actionable, forbidden
|
|
366
|
+
if (this._check(T.ASSIGN)) this._advance(); // `=` is optional: `ai:readable [x]` or `ai:readable = [x]`
|
|
367
|
+
this._eat(T.LBRACKET);
|
|
368
|
+
const targets = [];
|
|
369
|
+
// Support member expressions: [user.name, user.email]
|
|
370
|
+
while (!this._check(T.RBRACKET) && !this._atEnd()) {
|
|
371
|
+
let target = this._eat(T.IDENT).value;
|
|
372
|
+
while (this._check(T.DOT)) {
|
|
373
|
+
this._advance();
|
|
374
|
+
target += '.' + this._eat(T.IDENT).value;
|
|
375
|
+
}
|
|
376
|
+
targets.push(target);
|
|
377
|
+
if (this._check(T.COMMA)) this._advance();
|
|
378
|
+
}
|
|
379
|
+
this._eat(T.RBRACKET);
|
|
380
|
+
return node.aiDecl(capability, targets, loc);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_parseBeforeMountBlock() {
|
|
384
|
+
const loc = this._loc();
|
|
385
|
+
this._eat(T.BEFORE_MOUNT);
|
|
386
|
+
const body = this._parseBlock();
|
|
387
|
+
return node.beforeMountBlock(body, loc);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_parseOnMountBlock() {
|
|
391
|
+
const loc = this._loc();
|
|
392
|
+
this._eat(T.ON_MOUNT);
|
|
393
|
+
const body = this._parseBlock();
|
|
394
|
+
return node.onMountBlock(body, loc);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_parseOnCleanupBlock() {
|
|
398
|
+
const loc = this._loc();
|
|
399
|
+
this._eat(T.ON_CLEANUP);
|
|
400
|
+
const body = this._parseBlock();
|
|
401
|
+
return node.onCleanupBlock(body, loc);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* action funcName(param1, param2) { body }
|
|
406
|
+
* Declares a named function that AI agents are allowed to call via act().
|
|
407
|
+
* The body can contain any statements (assignments, signal updates, etc.).
|
|
408
|
+
*/
|
|
409
|
+
_parseActionDecl() {
|
|
410
|
+
const loc = this._loc();
|
|
411
|
+
this._eat(T.ACTION);
|
|
412
|
+
const name = this._eat(T.IDENT).value;
|
|
413
|
+
this._eat(T.LPAREN);
|
|
414
|
+
const params = [];
|
|
415
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
416
|
+
const pname = this._eat(T.IDENT).value;
|
|
417
|
+
params.push(pname);
|
|
418
|
+
if (this._check(T.COMMA)) this._advance();
|
|
419
|
+
}
|
|
420
|
+
this._eat(T.RPAREN);
|
|
421
|
+
const body = this._parseBlock();
|
|
422
|
+
return node.actionDecl(name, params, body, loc);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── JSX Parsing ──
|
|
426
|
+
_parseJSXElement() {
|
|
427
|
+
const loc = this._loc();
|
|
428
|
+
|
|
429
|
+
if (!this._check(T.JSX_OPEN)) {
|
|
430
|
+
// Could be a text node or expression
|
|
431
|
+
if (this._check(T.STRING)) {
|
|
432
|
+
const val = this._advance().value;
|
|
433
|
+
return node.jsxText(val, loc);
|
|
434
|
+
}
|
|
435
|
+
throw new ParseError('Expected JSX element', this._peek(), this.source);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const tagName = this._eat(T.JSX_OPEN).value;
|
|
439
|
+
const attrs = this._parseJSXAttrs();
|
|
440
|
+
|
|
441
|
+
// Self-closing: />
|
|
442
|
+
if (this._check(T.JSX_SELF_CLOSE) || (this._check(T.SLASH) && this._checkAt(1, T.GT))) {
|
|
443
|
+
if (this._check(T.SLASH)) this._advance();
|
|
444
|
+
if (this._check(T.GT)) this._advance();
|
|
445
|
+
return node.jsxElement(tagName, attrs, [], true, loc);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Opening tag closes: >
|
|
449
|
+
if (this._check(T.GT)) this._advance();
|
|
450
|
+
|
|
451
|
+
// Children
|
|
452
|
+
const children = this._parseJSXChildren(tagName);
|
|
453
|
+
|
|
454
|
+
return node.jsxElement(tagName, attrs, children, false, loc);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_parseJSXAttrs() {
|
|
458
|
+
const attrs = [];
|
|
459
|
+
|
|
460
|
+
while (!this._atEnd() && !this._check(T.GT) && !this._check(T.SLASH) && !this._check(T.JSX_SELF_CLOSE)) {
|
|
461
|
+
const loc = this._loc();
|
|
462
|
+
|
|
463
|
+
// Spread attribute: {...expr}
|
|
464
|
+
// Must be checked before regular LBRACE handling
|
|
465
|
+
if (this._check(T.LBRACE)) {
|
|
466
|
+
this._eat(T.LBRACE);
|
|
467
|
+
if (this._check(T.SPREAD)) {
|
|
468
|
+
this._advance(); // consume ...
|
|
469
|
+
const expr = this._parseExpr();
|
|
470
|
+
this._eat(T.RBRACE);
|
|
471
|
+
attrs.push({ type: 'SpreadAttr', expr, loc });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
// Unexpected { without spread — skip to avoid infinite loop
|
|
475
|
+
this._advance();
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// on:event[.modifier]* handler
|
|
480
|
+
if (this._check(T.ON) || (this._check(T.IDENT) && this._current().value === 'on')) {
|
|
481
|
+
this._advance();
|
|
482
|
+
if (this._check(T.COLON)) {
|
|
483
|
+
this._advance();
|
|
484
|
+
const eventName = this._eat(T.IDENT).value;
|
|
485
|
+
// Collect optional chained modifiers: .prevent .stop .enter .escape .space .tab
|
|
486
|
+
const modifiers = [];
|
|
487
|
+
while (this._check(T.DOT)) {
|
|
488
|
+
this._advance(); // consume '.'
|
|
489
|
+
if (this._check(T.IDENT)) modifiers.push(this._advance().value);
|
|
490
|
+
}
|
|
491
|
+
this._eat(T.ASSIGN);
|
|
492
|
+
this._eat(T.LBRACE);
|
|
493
|
+
const handler = this._parseExpr();
|
|
494
|
+
this._eat(T.RBRACE);
|
|
495
|
+
attrs.push(node.eventAttr(eventName, modifiers, handler, loc));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// bind:prop
|
|
501
|
+
if (this._check(T.IDENT) && this._current().value === 'bind') {
|
|
502
|
+
this._advance();
|
|
503
|
+
if (this._check(T.COLON)) {
|
|
504
|
+
this._advance();
|
|
505
|
+
const propName = this._eat(T.IDENT).value;
|
|
506
|
+
this._eat(T.ASSIGN);
|
|
507
|
+
this._eat(T.LBRACE);
|
|
508
|
+
const sig = this._parseExpr();
|
|
509
|
+
this._eat(T.RBRACE);
|
|
510
|
+
attrs.push(node.bindAttr(propName, sig, loc));
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Regular attribute: name="value" or name={expr} or just name
|
|
516
|
+
// Accept plain identifiers AND keyword tokens (e.g. `action`, `data`,
|
|
517
|
+
// `for`) as attribute names — in JSX they are ordinary HTML attributes,
|
|
518
|
+
// not language keywords.
|
|
519
|
+
if (this._isJSXAttrName()) {
|
|
520
|
+
const name = this._advance().value;
|
|
521
|
+
|
|
522
|
+
if (!this._check(T.ASSIGN)) {
|
|
523
|
+
// Boolean attribute
|
|
524
|
+
attrs.push(node.attr(name, node.literal(true, loc), loc));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
this._eat(T.ASSIGN);
|
|
529
|
+
|
|
530
|
+
if (this._check(T.STRING)) {
|
|
531
|
+
const val = this._advance().value;
|
|
532
|
+
attrs.push(node.attr(name, node.literal(val, loc), loc));
|
|
533
|
+
} else if (this._check(T.LBRACE)) {
|
|
534
|
+
this._eat(T.LBRACE);
|
|
535
|
+
const expr = this._parseExpr();
|
|
536
|
+
this._eat(T.RBRACE);
|
|
537
|
+
attrs.push(node.attr(name, expr, loc));
|
|
538
|
+
} else {
|
|
539
|
+
this._advance();
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Skip unknown
|
|
545
|
+
this._advance();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return attrs;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* True when the current token can serve as a JSX attribute name. This is any
|
|
553
|
+
* identifier OR any keyword token (action, data, route, when, …) whose value
|
|
554
|
+
* is a valid attribute-name string. Structural / operator tokens are excluded.
|
|
555
|
+
*/
|
|
556
|
+
_isJSXAttrName() {
|
|
557
|
+
if (this._check(T.IDENT)) return true;
|
|
558
|
+
const t = this._current();
|
|
559
|
+
if (!t || typeof t.value !== 'string') return false;
|
|
560
|
+
const EXCLUDED = new Set([
|
|
561
|
+
T.GT, T.LT, T.SLASH, T.JSX_SELF_CLOSE, T.JSX_OPEN, T.JSX_CLOSE,
|
|
562
|
+
T.LBRACE, T.RBRACE, T.ASSIGN, T.COLON, T.DOT, T.COMMA, T.STRING,
|
|
563
|
+
T.NUMBER, T.EOF,
|
|
564
|
+
]);
|
|
565
|
+
if (EXCLUDED.has(t.type)) return false;
|
|
566
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(t.value);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
_parseJSXChildren(parentTag) {
|
|
570
|
+
const children = [];
|
|
571
|
+
|
|
572
|
+
// Token types that are NOT valid inside JSX children as text
|
|
573
|
+
const NON_TEXT = new Set([T.JSX_OPEN, T.JSX_CLOSE, T.LBRACE, T.RBRACE, T.JSX_TEXT, T.EOF]);
|
|
574
|
+
|
|
575
|
+
while (!this._atEnd()) {
|
|
576
|
+
const loc = this._loc();
|
|
577
|
+
|
|
578
|
+
// Closing tag
|
|
579
|
+
if (this._check(T.JSX_CLOSE)) {
|
|
580
|
+
this._advance();
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Nested JSX element
|
|
585
|
+
if (this._check(T.JSX_OPEN)) {
|
|
586
|
+
children.push(this._parseJSXElement());
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Expression: { expr }
|
|
591
|
+
if (this._check(T.LBRACE)) {
|
|
592
|
+
this._eat(T.LBRACE);
|
|
593
|
+
const expr = this._parseExpr();
|
|
594
|
+
this._eat(T.RBRACE);
|
|
595
|
+
children.push(node.jsxExpr(expr, loc));
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Explicit JSX_TEXT token
|
|
600
|
+
if (this._check(T.JSX_TEXT)) {
|
|
601
|
+
const text = this._advance().value;
|
|
602
|
+
if (text.trim()) children.push(node.jsxText(text, loc));
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Collect consecutive non-structural tokens as plain text
|
|
607
|
+
// (identifiers, keywords, numbers, punctuation used as text content).
|
|
608
|
+
// Reconstruct verbatim from the original source so punctuation like
|
|
609
|
+
// "Clarity.js", "AI-native" or "v0.6" keeps its exact spacing; collapse
|
|
610
|
+
// runs of whitespace to a single space (HTML-style).
|
|
611
|
+
if (!NON_TEXT.has(this._peek().type)) {
|
|
612
|
+
const toks = [];
|
|
613
|
+
while (!this._atEnd() && !NON_TEXT.has(this._peek().type)) {
|
|
614
|
+
toks.push(this._advance());
|
|
615
|
+
}
|
|
616
|
+
const first = toks[0];
|
|
617
|
+
const last = toks[toks.length - 1];
|
|
618
|
+
let text;
|
|
619
|
+
if (this.source && first.start != null && last.end != null) {
|
|
620
|
+
text = this.source.slice(first.start, last.end).replace(/\s+/g, ' ');
|
|
621
|
+
// Preserve a boundary space if the source had whitespace adjacent to
|
|
622
|
+
// the run (e.g. between an inline element and following text).
|
|
623
|
+
if (/\s/.test(this.source[first.start - 1] || '')) text = ' ' + text;
|
|
624
|
+
if (/\s/.test(this.source[last.end] || '')) text = text + ' ';
|
|
625
|
+
} else {
|
|
626
|
+
// Fallback when no source offsets are available (bare parse()).
|
|
627
|
+
text = toks.map(t => t.value ?? t.type).join(' ');
|
|
628
|
+
}
|
|
629
|
+
if (text.trim()) children.push(node.jsxText(text, loc));
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return children;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ── Expression Parsing ──
|
|
640
|
+
// Pratt-style precedence climbing
|
|
641
|
+
|
|
642
|
+
_parseExpr() {
|
|
643
|
+
// Support comma expression: a = 1, b = 2
|
|
644
|
+
// Used in event handlers like on:click={ a++, b = 0 }
|
|
645
|
+
const loc = this._loc();
|
|
646
|
+
const first = this._parseTernary();
|
|
647
|
+
if (this._check(T.COMMA) && !this._inAttrValue) {
|
|
648
|
+
const parts = [first];
|
|
649
|
+
while (this._check(T.COMMA)) {
|
|
650
|
+
this._advance();
|
|
651
|
+
parts.push(this._parseTernary());
|
|
652
|
+
}
|
|
653
|
+
return { type: 'CommaExpr', parts, loc };
|
|
654
|
+
}
|
|
655
|
+
return first;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_parseTernary() {
|
|
659
|
+
const loc = this._loc();
|
|
660
|
+
let expr = this._parseOr();
|
|
661
|
+
|
|
662
|
+
if (this._check(T.QUESTION)) {
|
|
663
|
+
this._advance();
|
|
664
|
+
const then = this._parseExpr();
|
|
665
|
+
this._eat(T.COLON);
|
|
666
|
+
const else_ = this._parseTernary();
|
|
667
|
+
return node.ternary(expr, then, else_, loc);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return expr;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
_parseOr() {
|
|
674
|
+
const loc = this._loc();
|
|
675
|
+
let left = this._parseAnd();
|
|
676
|
+
while (this._check(T.OR)) {
|
|
677
|
+
const op = this._advance().value;
|
|
678
|
+
left = node.binary(op, left, this._parseAnd(), loc);
|
|
679
|
+
}
|
|
680
|
+
return left;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
_parseAnd() {
|
|
684
|
+
const loc = this._loc();
|
|
685
|
+
let left = this._parseEquality();
|
|
686
|
+
while (this._check(T.AND)) {
|
|
687
|
+
const op = this._advance().value;
|
|
688
|
+
left = node.binary(op, left, this._parseEquality(), loc);
|
|
689
|
+
}
|
|
690
|
+
return left;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
_parseEquality() {
|
|
694
|
+
const loc = this._loc();
|
|
695
|
+
let left = this._parseComparison();
|
|
696
|
+
while (this._checkAny(T.EQ_EQ, T.NOT_EQ, T.EQ_EQ_EQ, T.NOT_EQ_EQ)) {
|
|
697
|
+
const op = this._advance().value;
|
|
698
|
+
left = node.binary(op, left, this._parseComparison(), loc);
|
|
699
|
+
}
|
|
700
|
+
return left;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
_parseComparison() {
|
|
704
|
+
const loc = this._loc();
|
|
705
|
+
let left = this._parseAddSub();
|
|
706
|
+
while (this._checkAny(T.LT, T.GT, T.LT_EQ, T.GT_EQ)) {
|
|
707
|
+
const op = this._advance().value;
|
|
708
|
+
left = node.binary(op, left, this._parseAddSub(), loc);
|
|
709
|
+
}
|
|
710
|
+
return left;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_parseAddSub() {
|
|
714
|
+
const loc = this._loc();
|
|
715
|
+
let left = this._parseMulDiv();
|
|
716
|
+
while (this._checkAny(T.PLUS, T.MINUS)) {
|
|
717
|
+
const op = this._advance().value;
|
|
718
|
+
left = node.binary(op, left, this._parseMulDiv(), loc);
|
|
719
|
+
}
|
|
720
|
+
return left;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
_parseMulDiv() {
|
|
724
|
+
const loc = this._loc();
|
|
725
|
+
let left = this._parseUnary();
|
|
726
|
+
while (this._checkAny(T.STAR, T.SLASH, T.PERCENT)) {
|
|
727
|
+
const op = this._advance().value;
|
|
728
|
+
left = node.binary(op, left, this._parseUnary(), loc);
|
|
729
|
+
}
|
|
730
|
+
return left;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
_parseUnary() {
|
|
734
|
+
const loc = this._loc();
|
|
735
|
+
if (this._checkAny(T.NOT, T.MINUS)) {
|
|
736
|
+
const op = this._advance().value;
|
|
737
|
+
return node.unary(op, this._parseUnary(), loc);
|
|
738
|
+
}
|
|
739
|
+
if (this._checkAny(T.PLUS_PLUS, T.MINUS_MINUS)) {
|
|
740
|
+
const op = this._advance().value;
|
|
741
|
+
return node.update(op, this._parsePostfix(), true, loc);
|
|
742
|
+
}
|
|
743
|
+
// `await` — only meaningful inside server { } blocks but we parse it anywhere
|
|
744
|
+
// so the AST is well-formed; the codegen emits `await` only in async functions.
|
|
745
|
+
if (this._check(T.IDENT) && this._current().value === 'await') {
|
|
746
|
+
this._advance();
|
|
747
|
+
return node.awaitExpr(this._parseUnary(), loc);
|
|
748
|
+
}
|
|
749
|
+
return this._parsePostfix();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
_parsePostfix() {
|
|
753
|
+
const loc = this._loc();
|
|
754
|
+
let expr = this._parseCallMember();
|
|
755
|
+
|
|
756
|
+
// Post-increment/decrement
|
|
757
|
+
if (this._checkAny(T.PLUS_PLUS, T.MINUS_MINUS)) {
|
|
758
|
+
const op = this._advance().value;
|
|
759
|
+
return node.update(op, expr, false, loc);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Assignment operators
|
|
763
|
+
// FIX: use _parseTernary (not _parseExpr) for RHS — prevents comma from being
|
|
764
|
+
// swallowed into the value. e.g. a = x, b = y must parse as (a=x),(b=y) not a=(x,b=y)
|
|
765
|
+
// This matches JavaScript assignment precedence (right-associative, below comma).
|
|
766
|
+
if (this._checkAny(T.ASSIGN, T.PLUS_EQ, T.MINUS_EQ)) {
|
|
767
|
+
const op = this._advance().value;
|
|
768
|
+
const value = this._parseTernary();
|
|
769
|
+
return node.assign(expr, op, value, loc);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return expr;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_parseCallMember() {
|
|
776
|
+
const loc = this._loc();
|
|
777
|
+
let expr = this._parsePrimary();
|
|
778
|
+
|
|
779
|
+
while (true) {
|
|
780
|
+
if (this._check(T.DOT)) {
|
|
781
|
+
this._advance();
|
|
782
|
+
const prop = this._eat(T.IDENT).value;
|
|
783
|
+
expr = node.member(expr, node.ident(prop, loc), false, loc);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (this._check(T.LBRACKET)) {
|
|
788
|
+
this._advance();
|
|
789
|
+
const index = this._parseExpr();
|
|
790
|
+
this._eat(T.RBRACKET);
|
|
791
|
+
expr = node.member(expr, index, true, loc);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (this._check(T.LPAREN)) {
|
|
796
|
+
this._advance();
|
|
797
|
+
const args = [];
|
|
798
|
+
const prevFlag = this._inAttrValue;
|
|
799
|
+
this._inAttrValue = true; // commas separate arguments
|
|
800
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
801
|
+
args.push(this._parseTernary());
|
|
802
|
+
if (this._check(T.COMMA)) this._advance();
|
|
803
|
+
}
|
|
804
|
+
this._inAttrValue = prevFlag;
|
|
805
|
+
this._eat(T.RPAREN);
|
|
806
|
+
expr = node.call(expr, args, loc);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return expr;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_parsePrimary() {
|
|
817
|
+
const loc = this._loc();
|
|
818
|
+
const tok = this._peek();
|
|
819
|
+
|
|
820
|
+
// Literals
|
|
821
|
+
if (tok.type === T.NUMBER) return node.literal(this._advance().value, loc);
|
|
822
|
+
if (tok.type === T.STRING) return node.literal(this._advance().value, loc);
|
|
823
|
+
if (tok.type === T.TEMPLATE) return this._parseTemplateLiteral(this._advance(), loc);
|
|
824
|
+
if (tok.type === T.BOOL) return node.literal(this._advance().value === 'true', loc);
|
|
825
|
+
if (tok.type === T.NULL) { this._advance(); return node.literal(null, loc); }
|
|
826
|
+
if (tok.type === T.UNDEFINED) { this._advance(); return node.literal(undefined, loc); }
|
|
827
|
+
|
|
828
|
+
// Grouping
|
|
829
|
+
if (tok.type === T.LPAREN) {
|
|
830
|
+
this._advance();
|
|
831
|
+
// Check for arrow function: (params) =>
|
|
832
|
+
const saved = this.pos;
|
|
833
|
+
try {
|
|
834
|
+
const params = this._tryParseArrowParams();
|
|
835
|
+
if (params !== null && this._check(T.ARROW)) {
|
|
836
|
+
this._advance(); // =>
|
|
837
|
+
const body = this._check(T.LBRACE)
|
|
838
|
+
? this._parseBlock()
|
|
839
|
+
: this._parseExpr();
|
|
840
|
+
return node.arrow(params, body, loc);
|
|
841
|
+
}
|
|
842
|
+
} catch (_) { /* fall through */ }
|
|
843
|
+
this.pos = saved;
|
|
844
|
+
const expr = this._parseExpr();
|
|
845
|
+
this._eat(T.RPAREN);
|
|
846
|
+
return expr;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Array literal
|
|
850
|
+
if (tok.type === T.LBRACKET) {
|
|
851
|
+
this._advance();
|
|
852
|
+
const elements = [];
|
|
853
|
+
const prevFlag = this._inAttrValue;
|
|
854
|
+
this._inAttrValue = true; // commas are separators inside arrays
|
|
855
|
+
while (!this._check(T.RBRACKET) && !this._atEnd()) {
|
|
856
|
+
if (this._check(T.SPREAD)) {
|
|
857
|
+
this._advance(); // consume ...
|
|
858
|
+
elements.push({ type: 'SpreadExpr', expr: this._parseTernary(), loc });
|
|
859
|
+
} else {
|
|
860
|
+
elements.push(this._parseTernary());
|
|
861
|
+
}
|
|
862
|
+
if (this._check(T.COMMA)) this._advance();
|
|
863
|
+
}
|
|
864
|
+
this._inAttrValue = prevFlag;
|
|
865
|
+
this._eat(T.RBRACKET);
|
|
866
|
+
return node.array(elements, loc);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Object literal
|
|
870
|
+
if (tok.type === T.LBRACE) {
|
|
871
|
+
this._advance();
|
|
872
|
+
const props = [];
|
|
873
|
+
const prevFlag = this._inAttrValue;
|
|
874
|
+
this._inAttrValue = true; // commas are separators inside objects
|
|
875
|
+
while (!this._check(T.RBRACE) && !this._atEnd()) {
|
|
876
|
+
const key = this._eat(T.IDENT).value;
|
|
877
|
+
if (this._check(T.COLON)) {
|
|
878
|
+
this._advance();
|
|
879
|
+
props.push(node.prop(key, this._parseTernary(), loc));
|
|
880
|
+
} else {
|
|
881
|
+
// Shorthand: { foo } → { foo: foo }
|
|
882
|
+
props.push(node.prop(key, node.ident(key, loc), loc));
|
|
883
|
+
}
|
|
884
|
+
if (this._check(T.COMMA)) this._advance();
|
|
885
|
+
}
|
|
886
|
+
this._inAttrValue = prevFlag;
|
|
887
|
+
this._eat(T.RBRACE);
|
|
888
|
+
return node.object(props, loc);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Identifier (could be arrow function: x => ..., or `new` expression)
|
|
892
|
+
if (tok.type === T.IDENT) {
|
|
893
|
+
// new ClassName(...args)
|
|
894
|
+
if (tok.value === 'new') {
|
|
895
|
+
this._advance(); // consume 'new'
|
|
896
|
+
const callee = this._parsePrimary(); // parse constructor name (may be member expr)
|
|
897
|
+
const args = [];
|
|
898
|
+
if (this._check(T.LPAREN)) {
|
|
899
|
+
this._advance(); // consume (
|
|
900
|
+
const prevFlag = this._inAttrValue;
|
|
901
|
+
this._inAttrValue = true;
|
|
902
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
903
|
+
if (this._check(T.SPREAD)) {
|
|
904
|
+
this._advance();
|
|
905
|
+
args.push({ type: 'SpreadExpr', expr: this._parseTernary(), loc });
|
|
906
|
+
} else {
|
|
907
|
+
args.push(this._parseTernary());
|
|
908
|
+
}
|
|
909
|
+
if (this._check(T.COMMA)) this._advance();
|
|
910
|
+
}
|
|
911
|
+
this._inAttrValue = prevFlag;
|
|
912
|
+
this._eat(T.RPAREN);
|
|
913
|
+
}
|
|
914
|
+
return { type: 'NewExpr', callee, args, loc };
|
|
915
|
+
}
|
|
916
|
+
const name = this._advance().value;
|
|
917
|
+
if (this._check(T.ARROW)) {
|
|
918
|
+
this._advance();
|
|
919
|
+
const body = this._check(T.LBRACE) ? this._parseBlock() : this._parseExpr();
|
|
920
|
+
return node.arrow([{ name, type: null }], body, loc);
|
|
921
|
+
}
|
|
922
|
+
return node.ident(name, loc);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Keywords used as identifiers in expressions
|
|
926
|
+
if ([T.STATE, T.EFFECT, T.ON, T.COMPONENT, T.RENDER, T.SERVER, T.COMPUTED].includes(tok.type)) {
|
|
927
|
+
return node.ident(this._advance().value, loc);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Fallback: skip and return null literal
|
|
931
|
+
return node.literal(null, loc);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ── Template Literal Parsing ──
|
|
935
|
+
// Splits the raw token value on ${...} boundaries, parses each expression
|
|
936
|
+
// part using a fresh Lexer+Parser, and builds a TemplateLiteral AST node.
|
|
937
|
+
//
|
|
938
|
+
// Example: `Hello ${name}, you have ${count} items`
|
|
939
|
+
// → TemplateLiteral { parts: [
|
|
940
|
+
// { type: 'str', value: 'Hello ' },
|
|
941
|
+
// { type: 'expr', expr: Ident('name') },
|
|
942
|
+
// { type: 'str', value: ', you have ' },
|
|
943
|
+
// { type: 'expr', expr: Ident('count') },
|
|
944
|
+
// { type: 'str', value: ' items' },
|
|
945
|
+
// ]}
|
|
946
|
+
_parseTemplateLiteral(token, loc) {
|
|
947
|
+
const raw = token.value;
|
|
948
|
+
const parts = [];
|
|
949
|
+
|
|
950
|
+
// Match ${...} interpolations — no support for nested braces inside the
|
|
951
|
+
// expression (the lexer already handles depth tracking during scan, so
|
|
952
|
+
// the raw content only has balanced single-level ${...}).
|
|
953
|
+
const rx = /\$\{([^}]*)\}/g;
|
|
954
|
+
let lastIdx = 0;
|
|
955
|
+
let match;
|
|
956
|
+
|
|
957
|
+
while ((match = rx.exec(raw)) !== null) {
|
|
958
|
+
// Static string before this interpolation
|
|
959
|
+
if (match.index > lastIdx) {
|
|
960
|
+
parts.push({ type: 'str', value: raw.slice(lastIdx, match.index) });
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Parse the expression inside ${...}
|
|
964
|
+
const exprSrc = match[1].trim();
|
|
965
|
+
let exprAST;
|
|
966
|
+
try {
|
|
967
|
+
const exprTokens = new Lexer(exprSrc, '<template>').tokenize();
|
|
968
|
+
const exprParser = new Parser(exprTokens, exprSrc);
|
|
969
|
+
exprAST = exprParser._parseTernary();
|
|
970
|
+
} catch (_) {
|
|
971
|
+
// Fallback: treat the raw text as an identifier reference
|
|
972
|
+
exprAST = node.ident(exprSrc, loc);
|
|
973
|
+
}
|
|
974
|
+
parts.push({ type: 'expr', expr: exprAST });
|
|
975
|
+
lastIdx = match.index + match[0].length;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Trailing static string
|
|
979
|
+
if (lastIdx < raw.length) {
|
|
980
|
+
parts.push({ type: 'str', value: raw.slice(lastIdx) });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return node.template(parts, loc);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
_tryParseArrowParams() {
|
|
987
|
+
const params = [];
|
|
988
|
+
while (!this._check(T.RPAREN) && !this._atEnd()) {
|
|
989
|
+
const name = this._eat(T.IDENT).value;
|
|
990
|
+
let type = null;
|
|
991
|
+
if (this._check(T.COLON)) {
|
|
992
|
+
this._advance();
|
|
993
|
+
type = this._eat(T.IDENT).value;
|
|
994
|
+
}
|
|
995
|
+
params.push({ name, type });
|
|
996
|
+
if (this._check(T.COMMA)) this._advance();
|
|
997
|
+
}
|
|
998
|
+
this._eat(T.RPAREN);
|
|
999
|
+
return params;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ── Statement Parsing ──
|
|
1003
|
+
_parseBlock() {
|
|
1004
|
+
const loc = this._loc();
|
|
1005
|
+
this._eat(T.LBRACE);
|
|
1006
|
+
const body = [];
|
|
1007
|
+
while (!this._check(T.RBRACE) && !this._atEnd()) {
|
|
1008
|
+
const stmt = this._parseStatement();
|
|
1009
|
+
if (stmt) body.push(stmt);
|
|
1010
|
+
}
|
|
1011
|
+
this._eat(T.RBRACE);
|
|
1012
|
+
return node.block(body, loc);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
_parseStatement() {
|
|
1016
|
+
const loc = this._loc();
|
|
1017
|
+
|
|
1018
|
+
if (this._check(T.RETURN)) {
|
|
1019
|
+
this._advance();
|
|
1020
|
+
const value = !this._checkAny(T.RBRACE, T.NEWLINE, T.SEMICOLON, T.EOF)
|
|
1021
|
+
? this._parseExpr()
|
|
1022
|
+
: null;
|
|
1023
|
+
return node.returnStmt(value, loc);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// if statement: if (cond) { block } [else { block }]
|
|
1027
|
+
// also handles single-statement: if (cond) return expr
|
|
1028
|
+
if (this._check(T.IDENT) && this._current().value === 'if') {
|
|
1029
|
+
this._advance(); // consume 'if'
|
|
1030
|
+
this._eat(T.LPAREN);
|
|
1031
|
+
const cond = this._parseExpr();
|
|
1032
|
+
this._eat(T.RPAREN);
|
|
1033
|
+
// Support both braced block and single inline statement
|
|
1034
|
+
const then = this._check(T.LBRACE)
|
|
1035
|
+
? this._parseBlock()
|
|
1036
|
+
: node.block([this._parseStatement()].filter(Boolean), loc);
|
|
1037
|
+
let else_ = null;
|
|
1038
|
+
if (this._check(T.IDENT) && this._current().value === 'else') {
|
|
1039
|
+
this._advance();
|
|
1040
|
+
else_ = this._check(T.LBRACE)
|
|
1041
|
+
? this._parseBlock()
|
|
1042
|
+
: node.block([this._parseStatement()].filter(Boolean), loc);
|
|
1043
|
+
}
|
|
1044
|
+
return node.ifStmt(cond, then, else_, loc);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Variable declaration: const/let/var + ident = expr [, ident = expr ...]
|
|
1048
|
+
if (this._check(T.IDENT) && ['const', 'let', 'var'].includes(this._current().value)) {
|
|
1049
|
+
const kind = this._advance().value;
|
|
1050
|
+
const declarators = [];
|
|
1051
|
+
do {
|
|
1052
|
+
const dname = this._eat(T.IDENT).value;
|
|
1053
|
+
this._eat(T.ASSIGN);
|
|
1054
|
+
const dinit = this._parseTernary(); // not _parseExpr — avoids eating commas between declarators
|
|
1055
|
+
declarators.push({ name: dname, init: dinit });
|
|
1056
|
+
} while (this._check(T.COMMA) && this._advance());
|
|
1057
|
+
if (declarators.length === 1) {
|
|
1058
|
+
return node.varDecl(kind, declarators[0].name, declarators[0].init, loc);
|
|
1059
|
+
}
|
|
1060
|
+
return { type: 'MultiVarDecl', kind, declarators, loc };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (this._check(T.SEMICOLON)) {
|
|
1064
|
+
this._advance();
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Consume a bare comma (e.g. inside arrow-function blocks that are arguments
|
|
1069
|
+
// to a call — _inAttrValue suppresses CommaExpr, so the comma is left over)
|
|
1070
|
+
if (this._check(T.COMMA)) {
|
|
1071
|
+
this._advance();
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const expr = this._parseExpr();
|
|
1076
|
+
if (this._check(T.SEMICOLON)) this._advance();
|
|
1077
|
+
return node.exprStmt(expr, loc);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ── Token Utilities ──
|
|
1081
|
+
_peek() { return this.tokens[this.pos] ?? { type: T.EOF, value: null, line: 0, col: 0 }; }
|
|
1082
|
+
_current() { return this.tokens[this.pos]; }
|
|
1083
|
+
_peekNext() { return this.tokens[this.pos + 1]; }
|
|
1084
|
+
_checkAt(offset, type) { return (this.tokens[this.pos + offset]?.type ?? T.EOF) === type; }
|
|
1085
|
+
_atEnd() { return this._peek().type === T.EOF; }
|
|
1086
|
+
_check(type) { return this._peek().type === type; }
|
|
1087
|
+
_checkAny(...types) { return types.includes(this._peek().type); }
|
|
1088
|
+
_loc() { const t = this._peek(); return { line: t.line, col: t.col }; }
|
|
1089
|
+
|
|
1090
|
+
_advance() {
|
|
1091
|
+
const tok = this._peek();
|
|
1092
|
+
if (!this._atEnd()) this.pos++;
|
|
1093
|
+
return tok;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
_eat(type) {
|
|
1097
|
+
if (!this._check(type)) {
|
|
1098
|
+
throw new ParseError(`Expected ${type}`, this._peek(), this.source);
|
|
1099
|
+
}
|
|
1100
|
+
return this._advance();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ─── Convenience export ───────────────────────────────────────────────────────
|
|
1105
|
+
export function parse(tokens, source) {
|
|
1106
|
+
return new Parser(tokens, source).parse();
|
|
1107
|
+
}
|
|
1108
|
+
|