@kernlang/core 3.1.5 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +5 -2
- package/dist/codegen/data-layer.d.ts +12 -0
- package/dist/codegen/data-layer.js +291 -0
- package/dist/codegen/data-layer.js.map +1 -0
- package/dist/codegen/emitters.js +1 -4
- package/dist/codegen/emitters.js.map +1 -1
- package/dist/codegen/events.d.ts +9 -0
- package/dist/codegen/events.js +169 -0
- package/dist/codegen/events.js.map +1 -0
- package/dist/codegen/functions.d.ts +8 -0
- package/dist/codegen/functions.js +149 -0
- package/dist/codegen/functions.js.map +1 -0
- package/dist/codegen/ground-layer.d.ts +22 -0
- package/dist/codegen/ground-layer.js +321 -0
- package/dist/codegen/ground-layer.js.map +1 -0
- package/dist/codegen/helpers.d.ts +2 -0
- package/dist/codegen/helpers.js +19 -7
- package/dist/codegen/helpers.js.map +1 -1
- package/dist/codegen/machines.d.ts +9 -0
- package/dist/codegen/machines.js +129 -0
- package/dist/codegen/machines.js.map +1 -0
- package/dist/codegen/modules.d.ts +10 -0
- package/dist/codegen/modules.js +43 -0
- package/dist/codegen/modules.js.map +1 -0
- package/dist/codegen/screens.d.ts +29 -0
- package/dist/codegen/screens.js +202 -0
- package/dist/codegen/screens.js.map +1 -0
- package/dist/codegen/semantic-types.d.ts +14 -0
- package/dist/codegen/semantic-types.js +31 -0
- package/dist/codegen/semantic-types.js.map +1 -0
- package/dist/codegen/test-gen.d.ts +7 -0
- package/dist/codegen/test-gen.js +56 -0
- package/dist/codegen/test-gen.js.map +1 -0
- package/dist/codegen/type-system.d.ts +11 -0
- package/dist/codegen/type-system.js +171 -0
- package/dist/codegen/type-system.js.map +1 -0
- package/dist/codegen-core.d.ts +30 -36
- package/dist/codegen-core.js +258 -1459
- package/dist/codegen-core.js.map +1 -1
- package/dist/concepts.js.map +1 -1
- package/dist/config.d.ts +20 -1
- package/dist/config.js +36 -3
- package/dist/config.js.map +1 -1
- package/dist/coverage-gap.js +9 -5
- package/dist/coverage-gap.js.map +1 -1
- package/dist/decompiler.d.ts +10 -1
- package/dist/decompiler.js +21 -4
- package/dist/decompiler.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +11 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +33 -28
- package/dist/index.js +40 -35
- package/dist/index.js.map +1 -1
- package/dist/node-props.d.ts +255 -0
- package/dist/node-props.js +35 -0
- package/dist/node-props.js.map +1 -0
- package/dist/parser-core.d.ts +5 -0
- package/dist/parser-core.js +364 -0
- package/dist/parser-core.js.map +1 -0
- package/dist/parser-diagnostics.d.ts +14 -0
- package/dist/parser-diagnostics.js +32 -0
- package/dist/parser-diagnostics.js.map +1 -0
- package/dist/parser-keywords.d.ts +5 -0
- package/dist/parser-keywords.js +203 -0
- package/dist/parser-keywords.js.map +1 -0
- package/dist/parser-style.d.ts +3 -0
- package/dist/parser-style.js +73 -0
- package/dist/parser-style.js.map +1 -0
- package/dist/parser-token-stream.d.ts +27 -0
- package/dist/parser-token-stream.js +81 -0
- package/dist/parser-token-stream.js.map +1 -0
- package/dist/parser-tokenizer.d.ts +11 -0
- package/dist/parser-tokenizer.js +188 -0
- package/dist/parser-tokenizer.js.map +1 -0
- package/dist/parser.d.ts +61 -14
- package/dist/parser.js +65 -865
- package/dist/parser.js.map +1 -1
- package/dist/scanner.js +85 -25
- package/dist/scanner.js.map +1 -1
- package/dist/schema.d.ts +7 -2
- package/dist/schema.js +7 -2
- package/dist/schema.js.map +1 -1
- package/dist/source-map.d.ts +27 -0
- package/dist/source-map.js +82 -0
- package/dist/source-map.js.map +1 -0
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +197 -55
- package/dist/spec.js.map +1 -1
- package/dist/styles-react.js +1 -1
- package/dist/styles-react.js.map +1 -1
- package/dist/styles-tailwind.d.ts +10 -0
- package/dist/styles-tailwind.js +62 -15
- package/dist/styles-tailwind.js.map +1 -1
- package/dist/template-catalog.js +1 -1
- package/dist/template-catalog.js.map +1 -1
- package/dist/template-engine.d.ts +11 -6
- package/dist/template-engine.js +20 -12
- package/dist/template-engine.js.map +1 -1
- package/dist/types.d.ts +8 -3
- package/dist/utils.d.ts +21 -1
- package/dist/utils.js +29 -5
- package/dist/utils.js.map +1 -1
- package/dist/version-adapters.js +1 -2
- package/dist/version-adapters.js.map +1 -1
- package/dist/version-detect.js +3 -3
- package/dist/version-detect.js.map +1 -1
- package/dist/walk.d.ts +40 -0
- package/dist/walk.js +111 -0
- package/dist/walk.js.map +1 -0
- package/package.json +2 -2
package/dist/parser.js
CHANGED
|
@@ -1,341 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KERN Parser — public API surface.
|
|
3
|
+
*
|
|
4
|
+
* Implementation is split across sibling modules:
|
|
5
|
+
* - parser-diagnostics.ts — diagnostic infrastructure
|
|
6
|
+
* - parser-tokenizer.ts — character-level tokenizer
|
|
7
|
+
* - parser-token-stream.ts — token cursor
|
|
8
|
+
* - parser-style.ts — style block parsing
|
|
9
|
+
* - parser-keywords.ts — keyword-specific handlers
|
|
10
|
+
* - parser-core.ts — line parsing, tree building, orchestration
|
|
11
|
+
*/
|
|
1
12
|
import { KernParseError } from './errors.js';
|
|
2
|
-
import {
|
|
13
|
+
import { parseInternal } from './parser-core.js';
|
|
3
14
|
import { defaultRuntime } from './runtime.js';
|
|
4
15
|
import { validateSchema } from './schema.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
UNCLOSED_EXPR: 'Close the `{{ ... }}` expression or move the unfinished code into a quoted string.',
|
|
8
|
-
UNCLOSED_STYLE: 'Close the `{ ... }` style block with `}` and keep any commas inside the block.',
|
|
9
|
-
UNCLOSED_STRING: 'Add the missing closing quote or escape any embedded quotes inside the string.',
|
|
10
|
-
UNEXPECTED_TOKEN: 'Remove the stray token or quote it so the parser can treat it as a value.',
|
|
11
|
-
EMPTY_DOCUMENT: 'Add at least one root KERN node such as `screen`, `view`, or `text`.',
|
|
12
|
-
INVALID_INDENT: 'Replace tabs with spaces so indentation is consistent across sibling nodes.',
|
|
13
|
-
UNKNOWN_NODE_TYPE: 'Rename this node to a supported KERN keyword or register it as an evolved node type.',
|
|
14
|
-
INDENT_JUMP: 'Align this line with an existing indentation level so the parent-child structure is unambiguous.',
|
|
15
|
-
DUPLICATE_PROP: 'Remove the duplicate property or merge the values into a single prop assignment.',
|
|
16
|
-
DROPPED_LINE: 'Rewrite this line so it starts with a valid KERN node type and move stray symbols into props.',
|
|
17
|
-
};
|
|
18
|
-
function createParseState() {
|
|
19
|
-
return { diagnostics: [] };
|
|
20
|
-
}
|
|
21
|
-
function commitParseState(state, runtime = defaultRuntime) {
|
|
22
|
-
runtime.lastParseDiagnostics = state.diagnostics.map(d => ({ ...d }));
|
|
23
|
-
}
|
|
24
|
-
function emitDiagnostic(state, code, severity, message, line, col, options = {}) {
|
|
25
|
-
state.diagnostics.push({
|
|
26
|
-
code,
|
|
27
|
-
severity,
|
|
28
|
-
message,
|
|
29
|
-
line,
|
|
30
|
-
col,
|
|
31
|
-
endCol: Math.max(options.endCol ?? (col + 1), col),
|
|
32
|
-
suggestion: options.suggestion ?? DIAGNOSTIC_SUGGESTIONS[code],
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
// ── Tokenizer ────────────────────────────────────────────────────────────
|
|
36
|
-
function isIdentStart(ch) {
|
|
37
|
-
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_';
|
|
38
|
-
}
|
|
39
|
-
function isIdentChar(ch) {
|
|
40
|
-
return isIdentStart(ch) || (ch >= '0' && ch <= '9') || ch === '-';
|
|
41
|
-
}
|
|
42
|
-
function isDigit(ch) {
|
|
43
|
-
return ch >= '0' && ch <= '9';
|
|
44
|
-
}
|
|
45
|
-
/** Character-by-character tokenizer for a single KERN line (after indent stripped). */
|
|
46
|
-
function tokenizeLineInternal(line, state) {
|
|
47
|
-
const tokens = [];
|
|
48
|
-
let i = 0;
|
|
49
|
-
while (i < line.length) {
|
|
50
|
-
const ch = line[i];
|
|
51
|
-
// Whitespace
|
|
52
|
-
if (ch === ' ' || ch === '\t') {
|
|
53
|
-
const start = i;
|
|
54
|
-
while (i < line.length && (line[i] === ' ' || line[i] === '\t'))
|
|
55
|
-
i++;
|
|
56
|
-
tokens.push({ kind: 'whitespace', value: line.slice(start, i), pos: start });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
// Expression block {{ ... }}
|
|
60
|
-
if (ch === '{' && i + 1 < line.length && line[i + 1] === '{') {
|
|
61
|
-
const start = i;
|
|
62
|
-
i += 2;
|
|
63
|
-
let depth = 1;
|
|
64
|
-
while (i < line.length - 1 && depth > 0) {
|
|
65
|
-
if (line[i] === '{' && line[i + 1] === '{') {
|
|
66
|
-
depth++;
|
|
67
|
-
i += 2;
|
|
68
|
-
}
|
|
69
|
-
else if (line[i] === '}' && line[i + 1] === '}') {
|
|
70
|
-
depth--;
|
|
71
|
-
if (depth === 0)
|
|
72
|
-
break;
|
|
73
|
-
i += 2;
|
|
74
|
-
}
|
|
75
|
-
else
|
|
76
|
-
i++;
|
|
77
|
-
}
|
|
78
|
-
if (depth > 0) {
|
|
79
|
-
if (state) {
|
|
80
|
-
emitDiagnostic(state, 'UNCLOSED_EXPR', 'error', `Unclosed expression block '{{' at column ${start + 1}`, 0, start + 1, { endCol: start + 3 });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const inner = line.slice(start + 2, i).trim();
|
|
84
|
-
if (i < line.length - 1)
|
|
85
|
-
i += 2;
|
|
86
|
-
else
|
|
87
|
-
i = line.length;
|
|
88
|
-
tokens.push({ kind: 'expr', value: inner, pos: start });
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
// Style block { ... } — find matching } respecting quotes
|
|
92
|
-
if (ch === '{') {
|
|
93
|
-
const start = i;
|
|
94
|
-
let inQuote = false;
|
|
95
|
-
let j = i + 1;
|
|
96
|
-
let closed = false;
|
|
97
|
-
while (j < line.length) {
|
|
98
|
-
if (line[j] === '\\' && j + 1 < line.length) {
|
|
99
|
-
j += 2;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (line[j] === '"')
|
|
103
|
-
inQuote = !inQuote;
|
|
104
|
-
if (!inQuote && line[j] === '}') {
|
|
105
|
-
j++;
|
|
106
|
-
closed = true;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
j++;
|
|
110
|
-
}
|
|
111
|
-
if (!closed) {
|
|
112
|
-
if (state) {
|
|
113
|
-
emitDiagnostic(state, 'UNCLOSED_STYLE', 'error', `Unclosed style block '{' at column ${start + 1}`, 0, start + 1, { endCol: start + 2 });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
tokens.push({ kind: 'style', value: line.slice(start + 1, closed ? j - 1 : j), pos: start });
|
|
117
|
-
i = j;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
// Quoted string "..." with \" escape support
|
|
121
|
-
if (ch === '"') {
|
|
122
|
-
const start = i;
|
|
123
|
-
i++;
|
|
124
|
-
let inner = '';
|
|
125
|
-
while (i < line.length && line[i] !== '"') {
|
|
126
|
-
if (line[i] === '\\' && i + 1 < line.length) {
|
|
127
|
-
const next = line[i + 1];
|
|
128
|
-
if (next === '"') {
|
|
129
|
-
inner += '"';
|
|
130
|
-
i += 2;
|
|
131
|
-
}
|
|
132
|
-
else if (next === '\\') {
|
|
133
|
-
inner += '\\';
|
|
134
|
-
i += 2;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
inner += line[i];
|
|
138
|
-
i++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
inner += line[i];
|
|
143
|
-
i++;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (i >= line.length) {
|
|
147
|
-
if (state) {
|
|
148
|
-
emitDiagnostic(state, 'UNCLOSED_STRING', 'error', `Unclosed quoted string at column ${start + 1}`, 0, start + 1, { endCol: start + 2 });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
i++; // skip closing quote
|
|
153
|
-
}
|
|
154
|
-
tokens.push({ kind: 'quoted', value: inner, pos: start });
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
// Theme ref $name
|
|
158
|
-
if (ch === '$' && i + 1 < line.length && isIdentStart(line[i + 1])) {
|
|
159
|
-
const start = i;
|
|
160
|
-
i++;
|
|
161
|
-
while (i < line.length && isIdentChar(line[i]))
|
|
162
|
-
i++;
|
|
163
|
-
tokens.push({ kind: 'themeRef', value: line.slice(start + 1, i), pos: start });
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
// Equals
|
|
167
|
-
if (ch === '=') {
|
|
168
|
-
tokens.push({ kind: 'equals', value: '=', pos: i });
|
|
169
|
-
i++;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
// Comma
|
|
173
|
-
if (ch === ',') {
|
|
174
|
-
tokens.push({ kind: 'comma', value: ',', pos: i });
|
|
175
|
-
i++;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
// Slash-prefixed path: /something
|
|
179
|
-
if (ch === '/') {
|
|
180
|
-
const start = i;
|
|
181
|
-
while (i < line.length && line[i] !== ' ' && line[i] !== '\t' && line[i] !== '{' && line[i] !== '$')
|
|
182
|
-
i++;
|
|
183
|
-
tokens.push({ kind: 'slash', value: line.slice(start, i), pos: start });
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
// Number (pure digits)
|
|
187
|
-
if (isDigit(ch)) {
|
|
188
|
-
const start = i;
|
|
189
|
-
while (i < line.length && isDigit(line[i]))
|
|
190
|
-
i++;
|
|
191
|
-
tokens.push({ kind: 'number', value: line.slice(start, i), pos: start });
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
// Identifier: [A-Za-z_][A-Za-z0-9_-]*
|
|
195
|
-
// Handles evolved: prefix (evolved:keyword → strips prefix, returns keyword)
|
|
196
|
-
if (isIdentStart(ch)) {
|
|
197
|
-
const start = i;
|
|
198
|
-
while (i < line.length && isIdentChar(line[i]))
|
|
199
|
-
i++;
|
|
200
|
-
if (line[i] === ':' && line.slice(start, i) === 'evolved' && i + 1 < line.length && isIdentStart(line[i + 1])) {
|
|
201
|
-
i++;
|
|
202
|
-
const nameStart = i;
|
|
203
|
-
while (i < line.length && isIdentChar(line[i]))
|
|
204
|
-
i++;
|
|
205
|
-
tokens.push({ kind: 'identifier', value: line.slice(nameStart, i), pos: start });
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
tokens.push({ kind: 'identifier', value: line.slice(start, i), pos: start });
|
|
209
|
-
}
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
// Unknown character
|
|
213
|
-
tokens.push({ kind: 'unknown', value: ch, pos: i });
|
|
214
|
-
i++;
|
|
215
|
-
}
|
|
216
|
-
return tokens;
|
|
217
|
-
}
|
|
218
|
-
export function tokenizeLine(line) {
|
|
219
|
-
return tokenizeLineInternal(line);
|
|
220
|
-
}
|
|
221
|
-
// ── Token stream ─────────────────────────────────────────────────────────
|
|
222
|
-
// Opus: class-based cursor. Codex contribution: consumeAnyValue for evolved hints.
|
|
223
|
-
class TokenStream {
|
|
224
|
-
tokens;
|
|
225
|
-
idx = 0;
|
|
226
|
-
constructor(tokens) { this.tokens = tokens; }
|
|
227
|
-
peek() { return this.tokens[this.idx]; }
|
|
228
|
-
next() { return this.tokens[this.idx++]; }
|
|
229
|
-
done() { return this.idx >= this.tokens.length; }
|
|
230
|
-
position() { return this.idx; }
|
|
231
|
-
setPosition(pos) { this.idx = pos; }
|
|
232
|
-
skipWS() {
|
|
233
|
-
while (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'whitespace')
|
|
234
|
-
this.idx++;
|
|
235
|
-
}
|
|
236
|
-
/** Try to consume an identifier. Returns its value or null. */
|
|
237
|
-
tryIdent() {
|
|
238
|
-
if (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'identifier') {
|
|
239
|
-
return this.tokens[this.idx++].value;
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
/** Try to consume a number token. Returns its value or null. */
|
|
244
|
-
tryNumber() {
|
|
245
|
-
if (this.idx < this.tokens.length && this.tokens[this.idx].kind === 'number') {
|
|
246
|
-
return this.tokens[this.idx++].value;
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
/** Check if the next non-WS token is an identifier followed by '='. */
|
|
251
|
-
isKeyValue() {
|
|
252
|
-
let j = this.idx;
|
|
253
|
-
while (j < this.tokens.length && this.tokens[j].kind === 'whitespace')
|
|
254
|
-
j++;
|
|
255
|
-
if (j >= this.tokens.length || this.tokens[j].kind !== 'identifier')
|
|
256
|
-
return false;
|
|
257
|
-
return j + 1 < this.tokens.length && this.tokens[j + 1].kind === 'equals';
|
|
258
|
-
}
|
|
259
|
-
/** Check if any remaining token contains '='. */
|
|
260
|
-
hasEquals() {
|
|
261
|
-
for (let j = this.idx; j < this.tokens.length; j++) {
|
|
262
|
-
if (this.tokens[j].kind === 'equals')
|
|
263
|
-
return true;
|
|
264
|
-
}
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
/** Check if there are more non-whitespace tokens. */
|
|
268
|
-
hasMore() {
|
|
269
|
-
let j = this.idx;
|
|
270
|
-
while (j < this.tokens.length && this.tokens[j].kind === 'whitespace')
|
|
271
|
-
j++;
|
|
272
|
-
return j < this.tokens.length;
|
|
273
|
-
}
|
|
274
|
-
/** Get remaining raw text from current position (for fallback / params). */
|
|
275
|
-
remainingRaw(line) {
|
|
276
|
-
if (this.idx >= this.tokens.length)
|
|
277
|
-
return '';
|
|
278
|
-
const startPos = this.tokens[this.idx].pos;
|
|
279
|
-
this.idx = this.tokens.length;
|
|
280
|
-
return line.slice(startPos);
|
|
281
|
-
}
|
|
282
|
-
/** Consume any single non-whitespace token as a value (for evolved positional args). */
|
|
283
|
-
consumeAnyValue() {
|
|
284
|
-
this.skipWS();
|
|
285
|
-
const tok = this.peek();
|
|
286
|
-
if (!tok || tok.kind === 'whitespace')
|
|
287
|
-
return undefined;
|
|
288
|
-
return this.next();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// ── Prop parsing (extracted from Codex's parsePropToken pattern) ──────────
|
|
292
|
-
/** Map a value token to its JS representation. */
|
|
293
|
-
function tokenValue(tok) {
|
|
294
|
-
if (tok.kind === 'expr')
|
|
295
|
-
return { __expr: true, code: tok.value };
|
|
296
|
-
if (tok.kind === 'quoted')
|
|
297
|
-
return tok.value;
|
|
298
|
-
return tok.value;
|
|
299
|
-
}
|
|
300
|
-
/** Try to parse a key=value prop from the stream. Returns true if consumed. */
|
|
301
|
-
function parseProp(state, s, props, lineNum, col) {
|
|
302
|
-
if (!s.isKeyValue())
|
|
303
|
-
return false;
|
|
304
|
-
s.skipWS();
|
|
305
|
-
const keyTok = s.next();
|
|
306
|
-
const key = keyTok.value; // identifier
|
|
307
|
-
s.next(); // =
|
|
308
|
-
if (key in props) {
|
|
309
|
-
emitDiagnostic(state, 'DUPLICATE_PROP', 'warning', `Duplicate property '${key}' at line ${lineNum ?? 0}`, lineNum ?? 0, (col ?? 0) + keyTok.pos, {
|
|
310
|
-
endCol: (col ?? 0) + keyTok.pos + key.length,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
const valTok = s.peek();
|
|
314
|
-
if (!valTok || valTok.kind === 'whitespace') {
|
|
315
|
-
props[key] = '';
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
// key={{expr}} or key="quoted"
|
|
319
|
-
if (valTok.kind === 'expr' || valTok.kind === 'quoted') {
|
|
320
|
-
props[key] = tokenValue(s.next());
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
// key=bareValue — collect tokens up to next WS/style/themeRef
|
|
324
|
-
let value = '';
|
|
325
|
-
while (!s.done()) {
|
|
326
|
-
const vt = s.peek();
|
|
327
|
-
if (vt.kind === 'whitespace' || vt.kind === 'style' || vt.kind === 'themeRef')
|
|
328
|
-
break;
|
|
329
|
-
value += vt.value;
|
|
330
|
-
s.next();
|
|
331
|
-
}
|
|
332
|
-
props[key] = value;
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
// defaultRuntime.multilineBlockTypes now lives in defaultRuntime.multilineBlockTypes
|
|
336
|
-
// (initialized with 'logic', 'handler', 'cleanup', 'body' by the KernRuntime constructor).
|
|
337
|
-
// ── Evolved Node Parser Hints (v4) ──────────────────────────────────────
|
|
338
|
-
// ParserHintsConfig is now defined in runtime.ts. Parser hints live in defaultRuntime.
|
|
16
|
+
export { tokenizeLine } from './parser-tokenizer.js';
|
|
17
|
+
// ── Evolved Node Parser Hints ───────────────────────────────────────────
|
|
339
18
|
/** Register parser hints for an evolved node type. */
|
|
340
19
|
export function registerParserHints(keyword, hints) {
|
|
341
20
|
defaultRuntime.registerParserHints(keyword, hints);
|
|
@@ -348,515 +27,41 @@ export function unregisterParserHints(keyword) {
|
|
|
348
27
|
export function clearParserHints() {
|
|
349
28
|
defaultRuntime.clearParserHints();
|
|
350
29
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
props[propName] = id;
|
|
359
|
-
}
|
|
360
|
-
const KEYWORD_HANDLERS = new Map([
|
|
361
|
-
['theme', (s, props) => {
|
|
362
|
-
consumeBareIdent(s, props, 'name');
|
|
363
|
-
}],
|
|
364
|
-
['import', (s, props) => {
|
|
365
|
-
s.skipWS();
|
|
366
|
-
const pos = s.position();
|
|
367
|
-
const id = s.tryIdent();
|
|
368
|
-
if (id === 'default') {
|
|
369
|
-
if (!s.done() && s.peek()?.kind !== 'equals') {
|
|
370
|
-
props.default = true;
|
|
371
|
-
s.skipWS();
|
|
372
|
-
}
|
|
373
|
-
else if (s.peek()?.kind === 'equals') {
|
|
374
|
-
s.setPosition(pos);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
props.default = true;
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
else if (id) {
|
|
383
|
-
s.setPosition(pos);
|
|
384
|
-
}
|
|
385
|
-
if (!s.isKeyValue()) {
|
|
386
|
-
s.skipWS();
|
|
387
|
-
const name = s.tryIdent();
|
|
388
|
-
if (name)
|
|
389
|
-
props.name = name;
|
|
390
|
-
}
|
|
391
|
-
}],
|
|
392
|
-
['route', (s, props) => {
|
|
393
|
-
s.skipWS();
|
|
394
|
-
const pos = s.position();
|
|
395
|
-
const verb = s.tryIdent();
|
|
396
|
-
if (verb && /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/i.test(verb)) {
|
|
397
|
-
props.method = verb.toLowerCase();
|
|
398
|
-
s.skipWS();
|
|
399
|
-
const tok = s.peek();
|
|
400
|
-
if (tok && tok.kind === 'slash') {
|
|
401
|
-
props.path = tok.value;
|
|
402
|
-
s.next();
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else if (verb) {
|
|
406
|
-
s.setPosition(pos);
|
|
407
|
-
}
|
|
408
|
-
}],
|
|
409
|
-
['params', (s, props, content) => {
|
|
410
|
-
s.skipWS();
|
|
411
|
-
const remaining = s.remainingRaw(content);
|
|
412
|
-
if (remaining.length > 0) {
|
|
413
|
-
const items = [];
|
|
414
|
-
const parts = remaining.split(',').map(p => p.trim()).filter(Boolean);
|
|
415
|
-
for (const part of parts) {
|
|
416
|
-
const m = part.match(/^([A-Za-z_]\w*):([A-Za-z_]\w*(?:\[\])?)(?:\s*=\s*(.+))?$/);
|
|
417
|
-
if (m) {
|
|
418
|
-
const item = { name: m[1], type: m[2] };
|
|
419
|
-
if (m[3] !== undefined)
|
|
420
|
-
item.default = m[3].trim();
|
|
421
|
-
items.push(item);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
props.items = items;
|
|
425
|
-
}
|
|
426
|
-
}],
|
|
427
|
-
['auth', (s, props) => { consumeBareIdent(s, props, 'mode'); }],
|
|
428
|
-
['validate', (s, props) => { consumeBareIdent(s, props, 'schema'); }],
|
|
429
|
-
['error', (s, props) => {
|
|
430
|
-
s.skipWS();
|
|
431
|
-
const num = s.tryNumber();
|
|
432
|
-
if (num) {
|
|
433
|
-
props.status = parseInt(num, 10);
|
|
434
|
-
s.skipWS();
|
|
435
|
-
const tok = s.peek();
|
|
436
|
-
if (tok && tok.kind === 'quoted') {
|
|
437
|
-
props.message = tok.value;
|
|
438
|
-
s.next();
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}],
|
|
442
|
-
['derive', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
443
|
-
['guard', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
444
|
-
['effect', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
445
|
-
['strategy', (s, props) => { consumeBareIdent(s, props, 'name'); }],
|
|
446
|
-
['trigger', (s, props) => { consumeBareIdent(s, props, 'kind'); }],
|
|
447
|
-
['respond', (s, props) => {
|
|
448
|
-
s.skipWS();
|
|
449
|
-
const num = s.tryNumber();
|
|
450
|
-
if (num)
|
|
451
|
-
props.status = parseInt(num, 10);
|
|
452
|
-
}],
|
|
453
|
-
// Rule syntax — native .kern lint rules
|
|
454
|
-
['rule', (s, props) => {
|
|
455
|
-
// rule id severity=error category=bug confidence=0.9
|
|
456
|
-
consumeBareIdent(s, props, 'id');
|
|
457
|
-
}],
|
|
458
|
-
['message', (s, props) => {
|
|
459
|
-
// message "template with {{interpolation}}"
|
|
460
|
-
s.skipWS();
|
|
461
|
-
const tok = s.peek();
|
|
462
|
-
if (tok && tok.kind === 'quoted') {
|
|
463
|
-
props.template = tok.value;
|
|
464
|
-
s.next();
|
|
465
|
-
}
|
|
466
|
-
}],
|
|
467
|
-
['middleware', (s, props, content) => {
|
|
468
|
-
s.skipWS();
|
|
469
|
-
if (!s.hasMore())
|
|
470
|
-
return;
|
|
471
|
-
if (s.hasEquals())
|
|
472
|
-
return;
|
|
473
|
-
const remaining = s.remainingRaw(content).trim();
|
|
474
|
-
if (remaining.length > 0) {
|
|
475
|
-
const names = remaining.split(',').map(n => n.trim()).filter(Boolean);
|
|
476
|
-
if (names.length > 1) {
|
|
477
|
-
props.names = names;
|
|
478
|
-
}
|
|
479
|
-
else if (names.length === 1) {
|
|
480
|
-
props.name = names[0];
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}],
|
|
484
|
-
]);
|
|
485
|
-
// ── parseLine (token-based) ──────────────────────────────────────────────
|
|
486
|
-
function parseLine(state, raw, lineNum, runtime = defaultRuntime) {
|
|
487
|
-
if (raw.trim() === '')
|
|
488
|
-
return null;
|
|
489
|
-
const indent = raw.search(/\S/);
|
|
490
|
-
const indentText = raw.slice(0, indent);
|
|
491
|
-
const content = raw.slice(indent);
|
|
492
|
-
const col = indent + 1;
|
|
493
|
-
if (indentText.includes('\t')) {
|
|
494
|
-
emitDiagnostic(state, 'INVALID_INDENT', 'warning', `Tab indentation at line ${lineNum}`, lineNum, 1, {
|
|
495
|
-
endCol: indent + 1,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
const diagBefore = state.diagnostics.length;
|
|
499
|
-
const tokens = tokenizeLineInternal(content, state);
|
|
500
|
-
for (let d = diagBefore; d < state.diagnostics.length; d++) {
|
|
501
|
-
if (state.diagnostics[d].line === 0)
|
|
502
|
-
state.diagnostics[d].line = lineNum;
|
|
503
|
-
state.diagnostics[d].col += indent;
|
|
504
|
-
state.diagnostics[d].endCol += indent;
|
|
505
|
-
}
|
|
506
|
-
const s = new TokenStream(tokens);
|
|
507
|
-
// First token must be an identifier (the node type)
|
|
508
|
-
const typeToken = s.tryIdent();
|
|
509
|
-
if (!typeToken) {
|
|
510
|
-
const firstToken = tokens.find(tok => tok.kind !== 'whitespace');
|
|
511
|
-
if (firstToken) {
|
|
512
|
-
emitDiagnostic(state, 'DROPPED_LINE', 'error', `Dropped line ${lineNum}: expected a node type at the start of the line`, lineNum, col + firstToken.pos, {
|
|
513
|
-
endCol: col + content.length,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
const type = typeToken;
|
|
519
|
-
if (!isKnownNodeType(type, runtime) && !runtime.multilineBlockTypes.has(type) && !runtime.isTemplateNode(type)) {
|
|
520
|
-
emitDiagnostic(state, 'UNKNOWN_NODE_TYPE', 'warning', `Unknown node type '${type}' at line ${lineNum}`, lineNum, col, {
|
|
521
|
-
endCol: col + type.length,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
const props = {};
|
|
525
|
-
const styles = {};
|
|
526
|
-
const pseudoStyles = {};
|
|
527
|
-
const themeRefs = [];
|
|
528
|
-
// ── Evolved node parser hints (v4) ──────────────────────────────────
|
|
529
|
-
const evolvedHints = runtime.parserHints.get(type);
|
|
530
|
-
if (evolvedHints) {
|
|
531
|
-
if (evolvedHints.positionalArgs) {
|
|
532
|
-
for (const argName of evolvedHints.positionalArgs) {
|
|
533
|
-
const tok = s.consumeAnyValue();
|
|
534
|
-
if (tok)
|
|
535
|
-
props[argName] = tok.value;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (evolvedHints.bareWord) {
|
|
539
|
-
s.skipWS();
|
|
540
|
-
if (!s.isKeyValue()) {
|
|
541
|
-
const id = s.tryIdent();
|
|
542
|
-
if (id)
|
|
543
|
-
props[evolvedHints.bareWord] = id;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
// ── Keyword-specific handling ──────────────────────────────────────
|
|
548
|
-
const handler = KEYWORD_HANDLERS.get(type);
|
|
549
|
-
if (handler)
|
|
550
|
-
handler(s, props, content);
|
|
551
|
-
// ── Generic prop/style/theme parsing ───────────────────────────────
|
|
552
|
-
while (!s.done()) {
|
|
553
|
-
s.skipWS();
|
|
554
|
-
if (s.done())
|
|
555
|
-
break;
|
|
556
|
-
const tok = s.peek();
|
|
557
|
-
// Style block
|
|
558
|
-
if (tok.kind === 'style') {
|
|
559
|
-
parseStyleBlock(tok.value, styles, pseudoStyles);
|
|
560
|
-
s.next();
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
// Theme ref
|
|
564
|
-
if (tok.kind === 'themeRef') {
|
|
565
|
-
themeRefs.push(tok.value);
|
|
566
|
-
s.next();
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
// Key=value prop (extracted helper from Codex)
|
|
570
|
-
if (parseProp(state, s, props, lineNum, col))
|
|
571
|
-
continue;
|
|
572
|
-
// Unknown token — skip with warning
|
|
573
|
-
const skipped = s.next();
|
|
574
|
-
const errCol = col + skipped.pos;
|
|
575
|
-
emitDiagnostic(state, 'UNEXPECTED_TOKEN', 'warning', `Unexpected token "${skipped.value}" at line ${lineNum}:${errCol}`, lineNum, errCol, {
|
|
576
|
-
endCol: errCol + skipped.value.length,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
indent,
|
|
581
|
-
rawLength: content.length,
|
|
582
|
-
type,
|
|
583
|
-
props,
|
|
584
|
-
styles,
|
|
585
|
-
pseudoStyles,
|
|
586
|
-
themeRefs,
|
|
587
|
-
loc: { line: lineNum, col, endLine: lineNum, endCol: col + content.length },
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
// ── Style block parsing (unchanged) ──────────────────────────────────────
|
|
591
|
-
function splitStylePairs(block) {
|
|
592
|
-
const pairs = [];
|
|
593
|
-
let current = '';
|
|
594
|
-
let inQuote = false;
|
|
595
|
-
let parenDepth = 0;
|
|
596
|
-
for (let i = 0; i < block.length; i++) {
|
|
597
|
-
const ch = block[i];
|
|
598
|
-
if (ch === '\\' && i + 1 < block.length) {
|
|
599
|
-
current += ch + block[i + 1];
|
|
600
|
-
i++;
|
|
601
|
-
}
|
|
602
|
-
else if (ch === '"') {
|
|
603
|
-
inQuote = !inQuote;
|
|
604
|
-
current += ch;
|
|
605
|
-
}
|
|
606
|
-
else if (!inQuote && ch === '(') {
|
|
607
|
-
parenDepth++;
|
|
608
|
-
current += ch;
|
|
609
|
-
}
|
|
610
|
-
else if (!inQuote && ch === ')') {
|
|
611
|
-
parenDepth--;
|
|
612
|
-
current += ch;
|
|
613
|
-
}
|
|
614
|
-
else if (!inQuote && parenDepth === 0 && ch === ',') {
|
|
615
|
-
if (current.trim())
|
|
616
|
-
pairs.push(current.trim());
|
|
617
|
-
current = '';
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
current += ch;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
if (current.trim())
|
|
624
|
-
pairs.push(current.trim());
|
|
625
|
-
return pairs;
|
|
626
|
-
}
|
|
627
|
-
function parseStyleBlock(block, styles, pseudoStyles) {
|
|
628
|
-
const pairs = splitStylePairs(block);
|
|
629
|
-
for (const pair of pairs) {
|
|
630
|
-
// Pseudo-selector: :press:bg:#005BB5
|
|
631
|
-
const pseudoMatch = pair.match(/^:([a-z]+):([A-Za-z0-9_-]+):(.+)$/);
|
|
632
|
-
if (pseudoMatch) {
|
|
633
|
-
const [, pseudo, key, value] = pseudoMatch;
|
|
634
|
-
if (!pseudoStyles[pseudo])
|
|
635
|
-
pseudoStyles[pseudo] = {};
|
|
636
|
-
pseudoStyles[pseudo][key] = value.trim();
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
// Quoted key: "backdrop-filter":"blur(8px)"
|
|
640
|
-
const quotedKeyMatch = pair.match(/^"([^"]+)"\s*:\s*(.*)/);
|
|
641
|
-
if (quotedKeyMatch) {
|
|
642
|
-
const key = quotedKeyMatch[1];
|
|
643
|
-
let value = quotedKeyMatch[2].trim();
|
|
644
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
645
|
-
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
646
|
-
}
|
|
647
|
-
styles[key] = value;
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
// Normal: key:value (value may be quoted)
|
|
651
|
-
const colonIdx = pair.indexOf(':');
|
|
652
|
-
if (colonIdx > 0) {
|
|
653
|
-
const key = pair.slice(0, colonIdx).trim();
|
|
654
|
-
let value = pair.slice(colonIdx + 1).trim();
|
|
655
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
656
|
-
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
657
|
-
}
|
|
658
|
-
styles[key] = value;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
function expandMinified(source) {
|
|
663
|
-
if (!source.includes('(') || source.split('\n').length > 1)
|
|
664
|
-
return source;
|
|
665
|
-
const result = [];
|
|
666
|
-
let depth = 0;
|
|
667
|
-
let current = '';
|
|
668
|
-
let inQuote = false;
|
|
669
|
-
let inBraces = 0;
|
|
670
|
-
for (let i = 0; i < source.length; i++) {
|
|
671
|
-
const ch = source[i];
|
|
672
|
-
if (ch === '"') {
|
|
673
|
-
inQuote = !inQuote;
|
|
674
|
-
current += ch;
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (inQuote) {
|
|
678
|
-
current += ch;
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (ch === '{') {
|
|
682
|
-
inBraces++;
|
|
683
|
-
current += ch;
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (ch === '}') {
|
|
687
|
-
inBraces--;
|
|
688
|
-
current += ch;
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
if (inBraces > 0) {
|
|
692
|
-
current += ch;
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
if (ch === '(') {
|
|
696
|
-
result.push(' '.repeat(depth) + current.trim());
|
|
697
|
-
current = '';
|
|
698
|
-
depth++;
|
|
699
|
-
}
|
|
700
|
-
else if (ch === ')') {
|
|
701
|
-
if (current.trim())
|
|
702
|
-
result.push(' '.repeat(depth) + current.trim());
|
|
703
|
-
current = '';
|
|
704
|
-
depth--;
|
|
705
|
-
}
|
|
706
|
-
else if (ch === ',' && inBraces === 0) {
|
|
707
|
-
if (current.trim())
|
|
708
|
-
result.push(' '.repeat(depth) + current.trim());
|
|
709
|
-
current = '';
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
current += ch;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (current.trim())
|
|
716
|
-
result.push(' '.repeat(depth) + current.trim());
|
|
717
|
-
return result.join('\n');
|
|
718
|
-
}
|
|
719
|
-
/** Get warnings from the last parse() call */
|
|
30
|
+
// ── Diagnostics API ─────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Get diagnostic messages from the last parse() call as plain strings.
|
|
33
|
+
*
|
|
34
|
+
* @remarks Returns messages for all severities (error, warning, info).
|
|
35
|
+
* For structured diagnostics with severity filtering, use {@link getParseDiagnostics}.
|
|
36
|
+
*/
|
|
720
37
|
export function getParseWarnings() {
|
|
721
|
-
return defaultRuntime.lastParseDiagnostics.map(d => d.message);
|
|
38
|
+
return defaultRuntime.lastParseDiagnostics.map((d) => d.message);
|
|
722
39
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const parsed = [];
|
|
728
|
-
for (let i = 0; i < lines.length; i++) {
|
|
729
|
-
const trimmed = lines[i].trimStart();
|
|
730
|
-
const multilineType = [...runtime.multilineBlockTypes].find(type => trimmed.startsWith(`${type} <<<`));
|
|
731
|
-
if (multilineType) {
|
|
732
|
-
const indent = lines[i].search(/\S/);
|
|
733
|
-
const codeLines = [];
|
|
734
|
-
const startLine = i + 1;
|
|
735
|
-
const blockOpen = `${multilineType} <<<`;
|
|
736
|
-
const afterOpen = trimmed.slice(blockOpen.length);
|
|
737
|
-
let closed = false;
|
|
738
|
-
if (afterOpen.includes('>>>')) {
|
|
739
|
-
closed = true;
|
|
740
|
-
codeLines.push(afterOpen.split('>>>')[0]);
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
i++;
|
|
744
|
-
while (i < lines.length && !lines[i].trimStart().startsWith('>>>')) {
|
|
745
|
-
codeLines.push(lines[i]);
|
|
746
|
-
i++;
|
|
747
|
-
}
|
|
748
|
-
if (i < lines.length) {
|
|
749
|
-
closed = true;
|
|
750
|
-
const closeLine = lines[i];
|
|
751
|
-
const closeIdx = closeLine.indexOf('>>>');
|
|
752
|
-
if (closeIdx > 0) {
|
|
753
|
-
const before = closeLine.slice(0, closeIdx).trim();
|
|
754
|
-
if (before)
|
|
755
|
-
codeLines.push(before);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (!closed) {
|
|
760
|
-
emitDiagnostic(state, 'UNEXPECTED_TOKEN', 'error', `Unclosed multiline block '${multilineType} <<<' at line ${startLine}`, startLine, indent + 1, {
|
|
761
|
-
endCol: indent + 1 + blockOpen.length,
|
|
762
|
-
suggestion: `Close the '${multilineType} <<<' block with a matching '>>>' marker before the file ends.`,
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
parsed.push({
|
|
766
|
-
indent,
|
|
767
|
-
rawLength: lines[startLine - 1].slice(indent).length,
|
|
768
|
-
type: multilineType,
|
|
769
|
-
props: { code: codeLines.join('\n').replace(/^\n+|\n+$/g, '') },
|
|
770
|
-
styles: {},
|
|
771
|
-
pseudoStyles: {},
|
|
772
|
-
themeRefs: [],
|
|
773
|
-
loc: {
|
|
774
|
-
line: startLine,
|
|
775
|
-
col: indent + 1,
|
|
776
|
-
endLine: startLine,
|
|
777
|
-
endCol: indent + 1 + lines[startLine - 1].slice(indent).length,
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
const p = parseLine(state, lines[i], i + 1, runtime);
|
|
783
|
-
if (p)
|
|
784
|
-
parsed.push(p);
|
|
785
|
-
}
|
|
786
|
-
return parsed;
|
|
787
|
-
}
|
|
788
|
-
/** Convert a ParsedLine to an IRNode (no children yet). */
|
|
789
|
-
function toNode(p) {
|
|
790
|
-
const node = {
|
|
791
|
-
type: p.type,
|
|
792
|
-
loc: p.loc,
|
|
793
|
-
props: { ...p.props },
|
|
794
|
-
children: [],
|
|
795
|
-
};
|
|
796
|
-
if (Object.keys(p.styles).length > 0)
|
|
797
|
-
node.props.styles = p.styles;
|
|
798
|
-
if (Object.keys(p.pseudoStyles).length > 0)
|
|
799
|
-
node.props.pseudoStyles = p.pseudoStyles;
|
|
800
|
-
if (p.themeRefs.length > 0)
|
|
801
|
-
node.props.themeRefs = p.themeRefs;
|
|
802
|
-
return node;
|
|
803
|
-
}
|
|
804
|
-
/** Build a tree from parsed lines using indent-based stack. */
|
|
805
|
-
function buildTree(state, parsed, root, rootIndent) {
|
|
806
|
-
const stack = [{ node: root, indent: rootIndent }];
|
|
807
|
-
const seenIndents = new Set([rootIndent]);
|
|
808
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
809
|
-
const p = parsed[i];
|
|
810
|
-
const node = toNode(p);
|
|
811
|
-
if (p.indent < (stack[stack.length - 1]?.indent ?? 0) && !seenIndents.has(p.indent)) {
|
|
812
|
-
emitDiagnostic(state, 'INDENT_JUMP', 'warning', `Dedent to unseen indent level ${p.indent} at line ${p.loc.line}`, p.loc.line, p.loc.col, {
|
|
813
|
-
endCol: p.loc.col + p.type.length,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
while (stack.length > 1 && stack[stack.length - 1].indent >= p.indent) {
|
|
817
|
-
stack.pop();
|
|
818
|
-
}
|
|
819
|
-
seenIndents.add(p.indent);
|
|
820
|
-
const parent = stack[stack.length - 1].node;
|
|
821
|
-
if (!parent.children)
|
|
822
|
-
parent.children = [];
|
|
823
|
-
parent.children.push(node);
|
|
824
|
-
stack.push({ node, indent: p.indent });
|
|
825
|
-
}
|
|
40
|
+
/** Get structured diagnostics from the last parse() call. */
|
|
41
|
+
export function getParseDiagnostics(runtime) {
|
|
42
|
+
const rt = runtime ?? defaultRuntime;
|
|
43
|
+
return [...rt.lastParseDiagnostics];
|
|
826
44
|
}
|
|
827
|
-
// ── Public parse API
|
|
45
|
+
// ── Public parse API ────────────────────────────────────────────────────
|
|
828
46
|
/**
|
|
829
|
-
* Parse KERN source into an IR
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
47
|
+
* Parse KERN source into an IR node tree.
|
|
48
|
+
*
|
|
49
|
+
* Recovers from errors gracefully — malformed lines produce `DROPPED_LINE`
|
|
50
|
+
* diagnostics but never throw. Use {@link parseStrict} if you want errors to throw.
|
|
51
|
+
*
|
|
52
|
+
* @param source - KERN indentation-based source text
|
|
53
|
+
* @param runtime - Optional KernRuntime instance for isolation (defaults to shared singleton)
|
|
54
|
+
* @returns Root IRNode of the parsed tree
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const root = parse('page "Home"\n text "Hello"');
|
|
59
|
+
* // root.type === 'page', root.children[0].type === 'text'
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @see {@link parseWithDiagnostics} to also receive parse diagnostics
|
|
63
|
+
* @see {@link parseStrict} to throw on errors
|
|
834
64
|
*/
|
|
835
|
-
function parseInternal(source, asDocument, runtime) {
|
|
836
|
-
const rt = runtime ?? defaultRuntime;
|
|
837
|
-
const state = createParseState();
|
|
838
|
-
const parsed = parseLines(state, source, rt);
|
|
839
|
-
if (parsed.length === 0) {
|
|
840
|
-
if (source.trim() === '') {
|
|
841
|
-
emitDiagnostic(state, 'EMPTY_DOCUMENT', 'info', 'Source document is empty', 1, 1, { endCol: 1 });
|
|
842
|
-
}
|
|
843
|
-
const root = { type: 'document', children: [], loc: { line: 1, col: 1, endLine: 1, endCol: 1 } };
|
|
844
|
-
commitParseState(state, rt);
|
|
845
|
-
return { root, diagnostics: [...state.diagnostics] };
|
|
846
|
-
}
|
|
847
|
-
let root;
|
|
848
|
-
if (asDocument) {
|
|
849
|
-
root = { type: 'document', children: [], loc: { line: 1, col: 1 } };
|
|
850
|
-
buildTree(state, parsed, root, -1);
|
|
851
|
-
}
|
|
852
|
-
else {
|
|
853
|
-
root = toNode(parsed[0]);
|
|
854
|
-
buildTree(state, parsed.slice(1), root, parsed[0].indent);
|
|
855
|
-
}
|
|
856
|
-
computeEndSpans(root);
|
|
857
|
-
commitParseState(state, rt);
|
|
858
|
-
return { root, diagnostics: [...state.diagnostics] };
|
|
859
|
-
}
|
|
860
65
|
export function parse(source, runtime) {
|
|
861
66
|
return parseInternal(source, false, runtime).root;
|
|
862
67
|
}
|
|
@@ -868,29 +73,16 @@ export function parse(source, runtime) {
|
|
|
868
73
|
export function parseDocument(source, runtime) {
|
|
869
74
|
return parseInternal(source, true, runtime).root;
|
|
870
75
|
}
|
|
871
|
-
/**
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
else if (node.loc) {
|
|
883
|
-
node.loc.endLine = node.loc.endLine ?? node.loc.line;
|
|
884
|
-
node.loc.endCol = node.loc.endCol ?? node.loc.col;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// ── Diagnostics API ──────────────────────────────────────────────────────
|
|
888
|
-
/** Get structured diagnostics from the last parse() call. */
|
|
889
|
-
export function getParseDiagnostics(runtime) {
|
|
890
|
-
const rt = runtime ?? defaultRuntime;
|
|
891
|
-
return [...rt.lastParseDiagnostics];
|
|
892
|
-
}
|
|
893
|
-
/** Parse with diagnostics — returns both tree and structured diagnostics. */
|
|
76
|
+
/**
|
|
77
|
+
* Parse KERN source and return both the IR tree and structured diagnostics.
|
|
78
|
+
*
|
|
79
|
+
* Unlike {@link parse}, this returns a {@link ParseResult} containing the full
|
|
80
|
+
* diagnostics array, useful for editor integrations and lint-style reporting.
|
|
81
|
+
*
|
|
82
|
+
* @param source - KERN indentation-based source text
|
|
83
|
+
* @param runtime - Optional KernRuntime instance for isolation
|
|
84
|
+
* @returns `{ root: IRNode, diagnostics: ParseDiagnostic[] }`
|
|
85
|
+
*/
|
|
894
86
|
export function parseWithDiagnostics(source, runtime) {
|
|
895
87
|
return parseInternal(source, false, runtime);
|
|
896
88
|
}
|
|
@@ -898,10 +90,18 @@ export function parseWithDiagnostics(source, runtime) {
|
|
|
898
90
|
export function parseDocumentWithDiagnostics(source, runtime) {
|
|
899
91
|
return parseInternal(source, true, runtime);
|
|
900
92
|
}
|
|
901
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Strict parse — throws if any diagnostic has severity `'error'` or a schema violation is found.
|
|
95
|
+
*
|
|
96
|
+
* @param source - KERN indentation-based source text
|
|
97
|
+
* @param runtime - Optional KernRuntime instance for isolation
|
|
98
|
+
* @returns Root IRNode of the parsed tree
|
|
99
|
+
* @throws {KernParseError} When the source contains errors or schema violations.
|
|
100
|
+
* The error includes a code frame and the full diagnostics array.
|
|
101
|
+
*/
|
|
902
102
|
export function parseStrict(source, runtime) {
|
|
903
103
|
const { root, diagnostics } = parseWithDiagnostics(source, runtime);
|
|
904
|
-
const errors = diagnostics.filter(d => d.severity === 'error');
|
|
104
|
+
const errors = diagnostics.filter((d) => d.severity === 'error');
|
|
905
105
|
if (errors.length > 0) {
|
|
906
106
|
const first = errors[0];
|
|
907
107
|
const err = new KernParseError(first.message, first.line, first.col, source);
|
|
@@ -921,7 +121,7 @@ export function parseStrict(source, runtime) {
|
|
|
921
121
|
/** Strict document parse — throws KernParseError if any diagnostic has severity=error or schema violation. */
|
|
922
122
|
export function parseDocumentStrict(source, runtime) {
|
|
923
123
|
const { root, diagnostics } = parseDocumentWithDiagnostics(source, runtime);
|
|
924
|
-
const errors = diagnostics.filter(d => d.severity === 'error');
|
|
124
|
+
const errors = diagnostics.filter((d) => d.severity === 'error');
|
|
925
125
|
if (errors.length > 0) {
|
|
926
126
|
const first = errors[0];
|
|
927
127
|
const err = new KernParseError(first.message, first.line, first.col, source);
|