@kernlang/core 2.0.0 → 3.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/dist/codegen-core.d.ts +59 -2
- package/dist/codegen-core.js +1251 -25
- package/dist/codegen-core.js.map +1 -1
- package/dist/concepts.d.ts +100 -0
- package/dist/concepts.js +17 -0
- package/dist/concepts.js.map +1 -0
- package/dist/config.d.ts +37 -1
- package/dist/config.js +29 -1
- package/dist/config.js.map +1 -1
- package/dist/coverage-gap.d.ts +27 -0
- package/dist/coverage-gap.js +73 -0
- package/dist/coverage-gap.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +20 -0
- package/dist/parser.js +434 -138
- package/dist/parser.js.map +1 -1
- package/dist/scanner.js +48 -3
- package/dist/scanner.js.map +1 -1
- package/dist/spec.d.ts +13 -1
- package/dist/spec.js +54 -3
- package/dist/spec.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +10 -2
package/dist/parser.js
CHANGED
|
@@ -1,157 +1,461 @@
|
|
|
1
1
|
let _parseWarnings = [];
|
|
2
|
-
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
rest = rest.slice(nameMatch[0].length);
|
|
2
|
+
// ── Tokenizer ────────────────────────────────────────────────────────────
|
|
3
|
+
function isIdentStart(ch) {
|
|
4
|
+
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_';
|
|
5
|
+
}
|
|
6
|
+
function isIdentChar(ch) {
|
|
7
|
+
return isIdentStart(ch) || (ch >= '0' && ch <= '9') || ch === '-';
|
|
8
|
+
}
|
|
9
|
+
function isDigit(ch) {
|
|
10
|
+
return ch >= '0' && ch <= '9';
|
|
11
|
+
}
|
|
12
|
+
/** Character-by-character tokenizer for a single KERN line (after indent stripped). */
|
|
13
|
+
export function tokenizeLine(line) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < line.length) {
|
|
17
|
+
const ch = line[i];
|
|
18
|
+
// Whitespace
|
|
19
|
+
if (ch === ' ' || ch === '\t') {
|
|
20
|
+
const start = i;
|
|
21
|
+
while (i < line.length && (line[i] === ' ' || line[i] === '\t'))
|
|
22
|
+
i++;
|
|
23
|
+
tokens.push({ kind: 'whitespace', value: line.slice(start, i), pos: start });
|
|
24
|
+
continue;
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
// Expression block {{ ... }}
|
|
27
|
+
if (ch === '{' && i + 1 < line.length && line[i + 1] === '{') {
|
|
28
|
+
const start = i;
|
|
29
|
+
i += 2;
|
|
30
|
+
let depth = 1;
|
|
31
|
+
while (i < line.length - 1 && depth > 0) {
|
|
32
|
+
if (line[i] === '{' && line[i + 1] === '{') {
|
|
33
|
+
depth++;
|
|
34
|
+
i += 2;
|
|
35
|
+
}
|
|
36
|
+
else if (line[i] === '}' && line[i + 1] === '}') {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth === 0)
|
|
39
|
+
break;
|
|
40
|
+
i += 2;
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
i++;
|
|
38
44
|
}
|
|
45
|
+
const inner = line.slice(start + 2, i).trim();
|
|
46
|
+
i += 2;
|
|
47
|
+
tokens.push({ kind: 'expr', value: inner, pos: start });
|
|
48
|
+
continue;
|
|
39
49
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
props.name = nameMatch[1];
|
|
44
|
-
rest = rest.slice(nameMatch[0].length);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Parse the remainder: props, style blocks, theme refs
|
|
48
|
-
while (rest.length > 0) {
|
|
49
|
-
rest = rest.replace(/^ +/, '');
|
|
50
|
-
if (rest.length === 0)
|
|
51
|
-
break;
|
|
52
|
-
// Style block — find matching } respecting quotes
|
|
53
|
-
if (rest[0] === '{') {
|
|
54
|
-
let close = -1;
|
|
50
|
+
// Style block { ... } — find matching } respecting quotes
|
|
51
|
+
if (ch === '{') {
|
|
52
|
+
const start = i;
|
|
55
53
|
let inQuote = false;
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
let j = i + 1;
|
|
55
|
+
while (j < line.length) {
|
|
56
|
+
if (line[j] === '"')
|
|
58
57
|
inQuote = !inQuote;
|
|
59
|
-
if (!inQuote &&
|
|
60
|
-
|
|
58
|
+
if (!inQuote && line[j] === '}') {
|
|
59
|
+
j++;
|
|
61
60
|
break;
|
|
62
61
|
}
|
|
62
|
+
j++;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const block = rest.slice(1, close);
|
|
67
|
-
parseStyleBlock(block, styles, pseudoStyles);
|
|
68
|
-
rest = rest.slice(close + 1);
|
|
64
|
+
tokens.push({ kind: 'style', value: line.slice(start + 1, j - 1), pos: start });
|
|
65
|
+
i = j;
|
|
69
66
|
continue;
|
|
70
67
|
}
|
|
71
|
-
//
|
|
72
|
-
if (
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
// Quoted string "..."
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
const start = i;
|
|
71
|
+
i++;
|
|
72
|
+
while (i < line.length && line[i] !== '"')
|
|
73
|
+
i++;
|
|
74
|
+
const inner = line.slice(start + 1, i);
|
|
75
|
+
i++;
|
|
76
|
+
tokens.push({ kind: 'quoted', value: inner, pos: start });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Theme ref $name
|
|
80
|
+
if (ch === '$' && i + 1 < line.length && isIdentStart(line[i + 1])) {
|
|
81
|
+
const start = i;
|
|
82
|
+
i++;
|
|
83
|
+
while (i < line.length && isIdentChar(line[i]))
|
|
84
|
+
i++;
|
|
85
|
+
tokens.push({ kind: 'themeRef', value: line.slice(start + 1, i), pos: start });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Equals
|
|
89
|
+
if (ch === '=') {
|
|
90
|
+
tokens.push({ kind: 'equals', value: '=', pos: i });
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Comma
|
|
95
|
+
if (ch === ',') {
|
|
96
|
+
tokens.push({ kind: 'comma', value: ',', pos: i });
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Slash-prefixed path: /something
|
|
101
|
+
if (ch === '/') {
|
|
102
|
+
const start = i;
|
|
103
|
+
while (i < line.length && line[i] !== ' ' && line[i] !== '\t' && line[i] !== '{' && line[i] !== '$')
|
|
104
|
+
i++;
|
|
105
|
+
tokens.push({ kind: 'slash', value: line.slice(start, i), pos: start });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Number (pure digits)
|
|
109
|
+
if (isDigit(ch)) {
|
|
110
|
+
const start = i;
|
|
111
|
+
while (i < line.length && isDigit(line[i]))
|
|
112
|
+
i++;
|
|
113
|
+
tokens.push({ kind: 'number', value: line.slice(start, i), pos: start });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Identifier: [A-Za-z_][A-Za-z0-9_-]*
|
|
117
|
+
// Handles evolved: prefix (evolved:keyword → strips prefix, returns keyword)
|
|
118
|
+
if (isIdentStart(ch)) {
|
|
119
|
+
const start = i;
|
|
120
|
+
while (i < line.length && isIdentChar(line[i]))
|
|
121
|
+
i++;
|
|
122
|
+
if (line[i] === ':' && line.slice(start, i) === 'evolved' && i + 1 < line.length && isIdentStart(line[i + 1])) {
|
|
123
|
+
i++;
|
|
124
|
+
const nameStart = i;
|
|
125
|
+
while (i < line.length && isIdentChar(line[i]))
|
|
126
|
+
i++;
|
|
127
|
+
tokens.push({ kind: 'identifier', value: line.slice(nameStart, i), pos: start });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
tokens.push({ kind: 'identifier', value: line.slice(start, i), pos: start });
|
|
78
131
|
}
|
|
132
|
+
continue;
|
|
79
133
|
}
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
// Unknown character
|
|
135
|
+
tokens.push({ kind: 'unknown', value: ch, pos: i });
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
return tokens;
|
|
139
|
+
}
|
|
140
|
+
// ── Token stream ─────────────────────────────────────────────────────────
|
|
141
|
+
// Opus: class-based cursor. Codex contribution: consumeAnyValue for evolved hints.
|
|
142
|
+
class TokenStream {
|
|
143
|
+
tokens;
|
|
144
|
+
idx = 0;
|
|
145
|
+
constructor(tokens) { this.tokens = tokens; }
|
|
146
|
+
peek() { return this.tokens[this.idx]; }
|
|
147
|
+
next() { return this.tokens[this.idx++]; }
|
|
148
|
+
done() { return this.idx >= this.tokens.length; }
|
|
149
|
+
position() { return this.idx; }
|
|
150
|
+
setPosition(pos) { this.idx = pos; }
|
|
151
|
+
skipWS() {
|
|
152
|
+
while (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'whitespace')
|
|
153
|
+
this.idx++;
|
|
154
|
+
}
|
|
155
|
+
/** Try to consume an identifier. Returns its value or null. */
|
|
156
|
+
tryIdent() {
|
|
157
|
+
if (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'identifier') {
|
|
158
|
+
return this.tokens[this.idx++].value;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/** Try to consume a number token. Returns its value or null. */
|
|
163
|
+
tryNumber() {
|
|
164
|
+
if (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'number') {
|
|
165
|
+
return this.tokens[this.idx++].value;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Check if the next non-WS token is an identifier followed by '='. */
|
|
170
|
+
isKeyValue() {
|
|
171
|
+
let j = this.idx;
|
|
172
|
+
while (j < this.tokens.length && this.tokens[j].kind === 'whitespace')
|
|
173
|
+
j++;
|
|
174
|
+
if (j >= this.tokens.length || this.tokens[j].kind !== 'identifier')
|
|
175
|
+
return false;
|
|
176
|
+
return j + 1 < this.tokens.length && this.tokens[j + 1].kind === 'equals';
|
|
177
|
+
}
|
|
178
|
+
/** Check if any remaining token contains '='. */
|
|
179
|
+
hasEquals() {
|
|
180
|
+
for (let j = this.idx; j < this.tokens.length; j++) {
|
|
181
|
+
if (this.tokens[j].kind === 'equals')
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/** Check if there are more non-whitespace tokens. */
|
|
187
|
+
hasMore() {
|
|
188
|
+
let j = this.idx;
|
|
189
|
+
while (j < this.tokens.length && this.tokens[j].kind === 'whitespace')
|
|
190
|
+
j++;
|
|
191
|
+
return j < this.tokens.length;
|
|
192
|
+
}
|
|
193
|
+
/** Get remaining raw text from current position (for fallback / params). */
|
|
194
|
+
remainingRaw(line) {
|
|
195
|
+
if (this.idx >= this.tokens.length)
|
|
196
|
+
return '';
|
|
197
|
+
const startPos = this.tokens[this.idx].pos;
|
|
198
|
+
this.idx = this.tokens.length;
|
|
199
|
+
return line.slice(startPos);
|
|
200
|
+
}
|
|
201
|
+
/** Consume any single non-whitespace token as a value (for evolved positional args). */
|
|
202
|
+
consumeAnyValue() {
|
|
203
|
+
this.skipWS();
|
|
204
|
+
const tok = this.peek();
|
|
205
|
+
if (!tok || tok.kind === 'whitespace')
|
|
206
|
+
return undefined;
|
|
207
|
+
return this.next();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ── Prop parsing (extracted from Codex's parsePropToken pattern) ──────────
|
|
211
|
+
/** Map a value token to its JS representation. */
|
|
212
|
+
function tokenValue(tok) {
|
|
213
|
+
if (tok.kind === 'expr')
|
|
214
|
+
return { __expr: true, code: tok.value };
|
|
215
|
+
if (tok.kind === 'quoted')
|
|
216
|
+
return tok.value;
|
|
217
|
+
return tok.value;
|
|
218
|
+
}
|
|
219
|
+
/** Try to parse a key=value prop from the stream. Returns true if consumed. */
|
|
220
|
+
function parseProp(s, props) {
|
|
221
|
+
if (!s.isKeyValue())
|
|
222
|
+
return false;
|
|
223
|
+
s.skipWS();
|
|
224
|
+
const key = s.next().value; // identifier
|
|
225
|
+
s.next(); // =
|
|
226
|
+
const valTok = s.peek();
|
|
227
|
+
if (!valTok || valTok.kind === 'whitespace') {
|
|
228
|
+
props[key] = '';
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// key={{expr}} or key="quoted"
|
|
232
|
+
if (valTok.kind === 'expr' || valTok.kind === 'quoted') {
|
|
233
|
+
props[key] = tokenValue(s.next());
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
// key=bareValue — collect tokens up to next WS/style/themeRef
|
|
237
|
+
let value = '';
|
|
238
|
+
while (!s.done()) {
|
|
239
|
+
const vt = s.peek();
|
|
240
|
+
if (vt.kind === 'whitespace' || vt.kind === 'style' || vt.kind === 'themeRef')
|
|
241
|
+
break;
|
|
242
|
+
value += vt.value;
|
|
243
|
+
s.next();
|
|
244
|
+
}
|
|
245
|
+
props[key] = value;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const MULTILINE_BLOCK_TYPES = new Set(['logic', 'handler', 'cleanup', 'body']);
|
|
249
|
+
const _parserHints = new Map();
|
|
250
|
+
/** Register parser hints for an evolved node type. */
|
|
251
|
+
export function registerParserHints(keyword, hints) {
|
|
252
|
+
_parserHints.set(keyword, hints);
|
|
253
|
+
if (hints.multilineBlock) {
|
|
254
|
+
MULTILINE_BLOCK_TYPES.add(keyword);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Unregister parser hints (for rollback/testing). */
|
|
258
|
+
export function unregisterParserHints(keyword) {
|
|
259
|
+
const hints = _parserHints.get(keyword);
|
|
260
|
+
if (hints?.multilineBlock) {
|
|
261
|
+
MULTILINE_BLOCK_TYPES.delete(keyword);
|
|
262
|
+
}
|
|
263
|
+
_parserHints.delete(keyword);
|
|
264
|
+
}
|
|
265
|
+
/** Clear all parser hints (for test isolation). */
|
|
266
|
+
export function clearParserHints() {
|
|
267
|
+
for (const [keyword, hints] of _parserHints) {
|
|
268
|
+
if (hints.multilineBlock)
|
|
269
|
+
MULTILINE_BLOCK_TYPES.delete(keyword);
|
|
270
|
+
}
|
|
271
|
+
_parserHints.clear();
|
|
272
|
+
}
|
|
273
|
+
/** Consume a bare identifier into props if it's not a key=value pair. */
|
|
274
|
+
function consumeBareIdent(s, props, propName) {
|
|
275
|
+
s.skipWS();
|
|
276
|
+
if (s.isKeyValue())
|
|
277
|
+
return;
|
|
278
|
+
const id = s.tryIdent();
|
|
279
|
+
if (id)
|
|
280
|
+
props[propName] = id;
|
|
281
|
+
}
|
|
282
|
+
const KEYWORD_HANDLERS = new Map([
|
|
283
|
+
['theme', (s, props) => {
|
|
284
|
+
consumeBareIdent(s, props, 'name');
|
|
285
|
+
}],
|
|
286
|
+
['import', (s, props) => {
|
|
287
|
+
s.skipWS();
|
|
288
|
+
const pos = s.position();
|
|
289
|
+
const id = s.tryIdent();
|
|
290
|
+
if (id === 'default') {
|
|
291
|
+
if (!s.done() && s.peek()?.kind !== 'equals') {
|
|
292
|
+
props.default = true;
|
|
293
|
+
s.skipWS();
|
|
92
294
|
}
|
|
93
|
-
else if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
295
|
+
else if (s.peek()?.kind === 'equals') {
|
|
296
|
+
s.setPosition(pos);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
props.default = true;
|
|
301
|
+
return;
|
|
98
302
|
}
|
|
99
303
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
props[key] = { __expr: true, code: expr };
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
// Prop: key=value or key="quoted value"
|
|
106
|
-
const propMatch = rest.match(/^([A-Za-z_][A-Za-z0-9_-]*)=/);
|
|
107
|
-
if (propMatch) {
|
|
108
|
-
const key = propMatch[1];
|
|
109
|
-
rest = rest.slice(propMatch[0].length);
|
|
110
|
-
let value;
|
|
111
|
-
if (rest[0] === '"') {
|
|
112
|
-
const endQuote = rest.indexOf('"', 1);
|
|
113
|
-
value = rest.slice(1, endQuote);
|
|
114
|
-
rest = rest.slice(endQuote + 1);
|
|
304
|
+
else if (id) {
|
|
305
|
+
s.setPosition(pos);
|
|
115
306
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
307
|
+
if (!s.isKeyValue()) {
|
|
308
|
+
s.skipWS();
|
|
309
|
+
const name = s.tryIdent();
|
|
310
|
+
if (name)
|
|
311
|
+
props.name = name;
|
|
312
|
+
}
|
|
313
|
+
}],
|
|
314
|
+
['route', (s, props) => {
|
|
315
|
+
s.skipWS();
|
|
316
|
+
const pos = s.position();
|
|
317
|
+
const verb = s.tryIdent();
|
|
318
|
+
if (verb && /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/i.test(verb)) {
|
|
319
|
+
props.method = verb.toLowerCase();
|
|
320
|
+
s.skipWS();
|
|
321
|
+
const tok = s.peek();
|
|
322
|
+
if (tok && tok.kind === 'slash') {
|
|
323
|
+
props.path = tok.value;
|
|
324
|
+
s.next();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (verb) {
|
|
328
|
+
s.setPosition(pos);
|
|
329
|
+
}
|
|
330
|
+
}],
|
|
331
|
+
['params', (s, props, content) => {
|
|
332
|
+
s.skipWS();
|
|
333
|
+
const remaining = s.remainingRaw(content);
|
|
334
|
+
if (remaining.length > 0) {
|
|
335
|
+
const items = [];
|
|
336
|
+
const parts = remaining.split(',').map(p => p.trim()).filter(Boolean);
|
|
337
|
+
for (const part of parts) {
|
|
338
|
+
const m = part.match(/^([A-Za-z_]\w*):([A-Za-z_]\w*(?:\[\])?)(?:\s*=\s*(.+))?$/);
|
|
339
|
+
if (m) {
|
|
340
|
+
const item = { name: m[1], type: m[2] };
|
|
341
|
+
if (m[3] !== undefined)
|
|
342
|
+
item.default = m[3].trim();
|
|
343
|
+
items.push(item);
|
|
131
344
|
}
|
|
132
345
|
}
|
|
133
|
-
|
|
134
|
-
rest = rest.slice(j + 1);
|
|
135
|
-
props[key] = { __expr: true, code: expr };
|
|
136
|
-
continue;
|
|
346
|
+
props.items = items;
|
|
137
347
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
348
|
+
}],
|
|
349
|
+
['auth', (s, props) => { consumeBareIdent(s, props, 'mode'); }],
|
|
350
|
+
['validate', (s, props) => { consumeBareIdent(s, props, 'schema'); }],
|
|
351
|
+
['error', (s, props) => {
|
|
352
|
+
s.skipWS();
|
|
353
|
+
const num = s.tryNumber();
|
|
354
|
+
if (num) {
|
|
355
|
+
props.status = parseInt(num, 10);
|
|
356
|
+
s.skipWS();
|
|
357
|
+
const tok = s.peek();
|
|
358
|
+
if (tok && tok.kind === 'quoted') {
|
|
359
|
+
props.message = tok.value;
|
|
360
|
+
s.next();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}],
|
|
364
|
+
['derive', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
365
|
+
['guard', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
366
|
+
['effect', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
367
|
+
['strategy', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
368
|
+
['trigger', (s, props) => { consumeBareIdent(s, props, 'kind'); }],
|
|
369
|
+
['respond', (s, props) => {
|
|
370
|
+
s.skipWS();
|
|
371
|
+
const num = s.tryNumber();
|
|
372
|
+
if (num)
|
|
373
|
+
props.status = parseInt(num, 10);
|
|
374
|
+
}],
|
|
375
|
+
['middleware', (s, props, content) => {
|
|
376
|
+
s.skipWS();
|
|
377
|
+
if (!s.hasMore())
|
|
378
|
+
return;
|
|
379
|
+
if (s.hasEquals())
|
|
380
|
+
return;
|
|
381
|
+
const remaining = s.remainingRaw(content).trim();
|
|
382
|
+
if (remaining.length > 0) {
|
|
383
|
+
const names = remaining.split(',').map(n => n.trim()).filter(Boolean);
|
|
384
|
+
if (names.length > 1) {
|
|
385
|
+
props.names = names;
|
|
386
|
+
}
|
|
387
|
+
else if (names.length === 1) {
|
|
388
|
+
props.name = names[0];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}],
|
|
392
|
+
]);
|
|
393
|
+
// ── parseLine (token-based) ──────────────────────────────────────────────
|
|
394
|
+
function parseLine(raw, lineNum) {
|
|
395
|
+
if (raw.trim() === '')
|
|
396
|
+
return null;
|
|
397
|
+
const indent = raw.search(/\S/);
|
|
398
|
+
const content = raw.slice(indent);
|
|
399
|
+
const col = indent + 1;
|
|
400
|
+
const tokens = tokenizeLine(content);
|
|
401
|
+
const s = new TokenStream(tokens);
|
|
402
|
+
// First token must be an identifier (the node type)
|
|
403
|
+
const typeToken = s.tryIdent();
|
|
404
|
+
if (!typeToken)
|
|
405
|
+
return null;
|
|
406
|
+
const type = typeToken;
|
|
407
|
+
const props = {};
|
|
408
|
+
const styles = {};
|
|
409
|
+
const pseudoStyles = {};
|
|
410
|
+
const themeRefs = [];
|
|
411
|
+
// ── Evolved node parser hints (v4) ──────────────────────────────────
|
|
412
|
+
const evolvedHints = _parserHints.get(type);
|
|
413
|
+
if (evolvedHints) {
|
|
414
|
+
if (evolvedHints.positionalArgs) {
|
|
415
|
+
for (const argName of evolvedHints.positionalArgs) {
|
|
416
|
+
const tok = s.consumeAnyValue();
|
|
417
|
+
if (tok)
|
|
418
|
+
props[argName] = tok.value;
|
|
142
419
|
}
|
|
143
|
-
|
|
420
|
+
}
|
|
421
|
+
if (evolvedHints.bareWord) {
|
|
422
|
+
s.skipWS();
|
|
423
|
+
if (!s.isKeyValue()) {
|
|
424
|
+
const id = s.tryIdent();
|
|
425
|
+
if (id)
|
|
426
|
+
props[evolvedHints.bareWord] = id;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// ── Keyword-specific handling ──────────────────────────────────────
|
|
431
|
+
const handler = KEYWORD_HANDLERS.get(type);
|
|
432
|
+
if (handler)
|
|
433
|
+
handler(s, props, content);
|
|
434
|
+
// ── Generic prop/style/theme parsing ───────────────────────────────
|
|
435
|
+
while (!s.done()) {
|
|
436
|
+
s.skipWS();
|
|
437
|
+
if (s.done())
|
|
438
|
+
break;
|
|
439
|
+
const tok = s.peek();
|
|
440
|
+
// Style block
|
|
441
|
+
if (tok.kind === 'style') {
|
|
442
|
+
parseStyleBlock(tok.value, styles, pseudoStyles);
|
|
443
|
+
s.next();
|
|
144
444
|
continue;
|
|
145
445
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
_parseWarnings.push(`Unexpected token "${skipped[0]}" at line ${lineNum}:${errCol}`);
|
|
151
|
-
rest = rest.slice(skipped[0].length);
|
|
446
|
+
// Theme ref
|
|
447
|
+
if (tok.kind === 'themeRef') {
|
|
448
|
+
themeRefs.push(tok.value);
|
|
449
|
+
s.next();
|
|
152
450
|
continue;
|
|
153
451
|
}
|
|
154
|
-
|
|
452
|
+
// Key=value prop (extracted helper from Codex)
|
|
453
|
+
if (parseProp(s, props))
|
|
454
|
+
continue;
|
|
455
|
+
// Unknown token — skip with warning
|
|
456
|
+
const skipped = s.next();
|
|
457
|
+
const errCol = col + skipped.pos;
|
|
458
|
+
_parseWarnings.push(`Unexpected token "${skipped.value}" at line ${lineNum}:${errCol}`);
|
|
155
459
|
}
|
|
156
460
|
return {
|
|
157
461
|
indent: indent / 2,
|
|
@@ -163,6 +467,7 @@ function parseLine(raw, lineNum) {
|
|
|
163
467
|
loc: { line: lineNum, col },
|
|
164
468
|
};
|
|
165
469
|
}
|
|
470
|
+
// ── Style block parsing (unchanged) ──────────────────────────────────────
|
|
166
471
|
function splitStylePairs(block) {
|
|
167
472
|
const pairs = [];
|
|
168
473
|
let current = '';
|
|
@@ -212,7 +517,6 @@ function parseStyleBlock(block, styles, pseudoStyles) {
|
|
|
212
517
|
if (quotedKeyMatch) {
|
|
213
518
|
const key = quotedKeyMatch[1];
|
|
214
519
|
let value = quotedKeyMatch[2].trim();
|
|
215
|
-
// Strip surrounding quotes from value if present
|
|
216
520
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
217
521
|
value = value.slice(1, -1);
|
|
218
522
|
}
|
|
@@ -224,7 +528,6 @@ function parseStyleBlock(block, styles, pseudoStyles) {
|
|
|
224
528
|
if (colonIdx > 0) {
|
|
225
529
|
const key = pair.slice(0, colonIdx).trim();
|
|
226
530
|
let value = pair.slice(colonIdx + 1).trim();
|
|
227
|
-
// Strip surrounding quotes from value if present
|
|
228
531
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
229
532
|
value = value.slice(1, -1);
|
|
230
533
|
}
|
|
@@ -233,8 +536,6 @@ function parseStyleBlock(block, styles, pseudoStyles) {
|
|
|
233
536
|
}
|
|
234
537
|
}
|
|
235
538
|
function expandMinified(source) {
|
|
236
|
-
// Detect minified S-expression format: node(child1,child2)
|
|
237
|
-
// Convert to indented format for the standard parser
|
|
238
539
|
if (!source.includes('(') || source.split('\n').length > 2)
|
|
239
540
|
return source;
|
|
240
541
|
const result = [];
|
|
@@ -295,7 +596,6 @@ function expandMinified(source) {
|
|
|
295
596
|
export function getParseWarnings() { return [..._parseWarnings]; }
|
|
296
597
|
export function parse(source) {
|
|
297
598
|
_parseWarnings = [];
|
|
298
|
-
// Handle minified S-expression format
|
|
299
599
|
source = expandMinified(source);
|
|
300
600
|
const lines = source.split('\n');
|
|
301
601
|
const parsed = [];
|
|
@@ -307,7 +607,6 @@ export function parse(source) {
|
|
|
307
607
|
const codeLines = [];
|
|
308
608
|
const startLine = i + 1;
|
|
309
609
|
const blockOpen = `${multilineType} <<<`;
|
|
310
|
-
// Check if inline close on same line
|
|
311
610
|
const afterOpen = trimmed.slice(blockOpen.length);
|
|
312
611
|
if (afterOpen.includes('>>>')) {
|
|
313
612
|
codeLines.push(afterOpen.split('>>>')[0]);
|
|
@@ -318,7 +617,6 @@ export function parse(source) {
|
|
|
318
617
|
codeLines.push(lines[i]);
|
|
319
618
|
i++;
|
|
320
619
|
}
|
|
321
|
-
// Capture text before >>> on closing line
|
|
322
620
|
if (i < lines.length) {
|
|
323
621
|
const closeLine = lines[i];
|
|
324
622
|
const closeIdx = closeLine.indexOf('>>>');
|
|
@@ -359,13 +657,11 @@ export function parse(source) {
|
|
|
359
657
|
node.props.themeRefs = p.themeRefs;
|
|
360
658
|
return node;
|
|
361
659
|
}
|
|
362
|
-
// Build tree using indent levels
|
|
363
660
|
const root = toNode(parsed[0]);
|
|
364
661
|
const stack = [{ node: root, indent: parsed[0].indent }];
|
|
365
662
|
for (let i = 1; i < parsed.length; i++) {
|
|
366
663
|
const p = parsed[i];
|
|
367
664
|
const node = toNode(p);
|
|
368
|
-
// Pop stack until we find a parent at a lower indent level
|
|
369
665
|
while (stack.length > 1 && stack[stack.length - 1].indent >= p.indent) {
|
|
370
666
|
stack.pop();
|
|
371
667
|
}
|