@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/parser.js CHANGED
@@ -1,157 +1,461 @@
1
1
  let _parseWarnings = [];
2
- const MULTILINE_BLOCK_TYPES = new Set(['logic', 'handler', 'cleanup', 'body']);
3
- function parseLine(raw, lineNum) {
4
- if (raw.trim() === '')
5
- return null;
6
- const indent = raw.search(/\S/);
7
- let rest = raw.slice(indent);
8
- const col = indent + 1;
9
- // Extract type
10
- const typeMatch = rest.match(/^([A-Za-z_][A-Za-z0-9_-]*)/);
11
- if (!typeMatch)
12
- return null;
13
- const type = typeMatch[1];
14
- rest = rest.slice(type.length);
15
- const props = {};
16
- const styles = {};
17
- const pseudoStyles = {};
18
- const themeRefs = [];
19
- // Special: theme nodes have a bare name after the type: "theme bar {h:8}"
20
- if (type === 'theme') {
21
- rest = rest.replace(/^ +/, '');
22
- const nameMatch = rest.match(/^([A-Za-z_][A-Za-z0-9_-]*)/);
23
- if (nameMatch) {
24
- props.name = nameMatch[1];
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
- // Special: import nodes support bare words for name and optional "default" flag
29
- // Syntax: "import [default] <name> from=<path>"
30
- if (type === 'import') {
31
- rest = rest.replace(/^ +/, '');
32
- // Check for "default" keyword
33
- if (rest.startsWith('default')) {
34
- const afterDefault = rest.slice(7);
35
- if (afterDefault.length === 0 || afterDefault[0] === ' ') {
36
- props.default = true;
37
- rest = afterDefault.replace(/^ +/, '');
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
- // Capture the import name (bare word before from=)
41
- const nameMatch = rest.match(/^([A-Za-z_][A-Za-z0-9_-]*)/);
42
- if (nameMatch && !rest.startsWith('from=')) {
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
- for (let j = 1; j < rest.length; j++) {
57
- if (rest[j] === '"')
54
+ let j = i + 1;
55
+ while (j < line.length) {
56
+ if (line[j] === '"')
58
57
  inQuote = !inQuote;
59
- if (!inQuote && rest[j] === '}') {
60
- close = j;
58
+ if (!inQuote && line[j] === '}') {
59
+ j++;
61
60
  break;
62
61
  }
62
+ j++;
63
63
  }
64
- if (close === -1)
65
- break;
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
- // Theme ref
72
- if (rest[0] === '$') {
73
- const refMatch = rest.match(/^\$([A-Za-z_][A-Za-z0-9_-]*)/);
74
- if (refMatch) {
75
- themeRefs.push(refMatch[1]);
76
- rest = rest.slice(refMatch[0].length);
77
- continue;
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
- // Prop: key={{ expression }}
81
- const exprPropMatch = rest.match(/^([A-Za-z_][A-Za-z0-9_-]*)=\{\{/);
82
- if (exprPropMatch) {
83
- const key = exprPropMatch[1];
84
- rest = rest.slice(exprPropMatch[0].length);
85
- // Find matching }}
86
- let depth = 1;
87
- let j = 0;
88
- for (; j < rest.length - 1; j++) {
89
- if (rest[j] === '{' && rest[j + 1] === '{') {
90
- depth++;
91
- j++;
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 (rest[j] === '}' && rest[j + 1] === '}') {
94
- depth--;
95
- j++;
96
- if (depth === 0)
97
- break;
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
- const expr = rest.slice(0, j - 1).trim();
101
- rest = rest.slice(j + 1);
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
- else if (rest.startsWith('{{')) {
117
- // Bare expression without key= prefix (e.g. value={{ x }})
118
- rest = rest.slice(2);
119
- let depth = 1;
120
- let j = 0;
121
- for (; j < rest.length - 1; j++) {
122
- if (rest[j] === '{' && rest[j + 1] === '{') {
123
- depth++;
124
- j++;
125
- }
126
- else if (rest[j] === '}' && rest[j + 1] === '}') {
127
- depth--;
128
- j++;
129
- if (depth === 0)
130
- break;
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
- const expr = rest.slice(0, j - 1).trim();
134
- rest = rest.slice(j + 1);
135
- props[key] = { __expr: true, code: expr };
136
- continue;
346
+ props.items = items;
137
347
  }
138
- else {
139
- const endMatch = rest.match(/^[^\s{$]+/);
140
- value = endMatch ? endMatch[0] : '';
141
- rest = rest.slice(value.length);
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
- props[key] = value;
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
- // Unknown token — collect as warning, skip to next whitespace
147
- const skipped = rest.match(/^\S+/);
148
- if (skipped) {
149
- const errCol = col + (raw.length - rest.length);
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
- break;
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
  }