sketchmark 1.0.3 → 1.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/README.md +40 -29
- package/dist/index.cjs +324 -453
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +324 -453
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +324 -453
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -225,7 +225,7 @@ function tokenize$1(src) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// ============================================================
|
|
228
|
-
// sketchmark
|
|
228
|
+
// sketchmark - Parser (Tokens -> DiagramAST)
|
|
229
229
|
// ============================================================
|
|
230
230
|
let _uid = 0;
|
|
231
231
|
function uid(prefix) {
|
|
@@ -284,15 +284,16 @@ function propsToStyle(p) {
|
|
|
284
284
|
s.textAlign = p["text-align"];
|
|
285
285
|
if (p.padding)
|
|
286
286
|
s.padding = parseFloat(p.padding);
|
|
287
|
-
if (p["vertical-align"])
|
|
287
|
+
if (p["vertical-align"]) {
|
|
288
288
|
s.verticalAlign = p["vertical-align"];
|
|
289
|
+
}
|
|
289
290
|
if (p["line-height"])
|
|
290
291
|
s.lineHeight = parseFloat(p["line-height"]);
|
|
291
292
|
if (p["letter-spacing"])
|
|
292
293
|
s.letterSpacing = parseFloat(p["letter-spacing"]);
|
|
293
294
|
if (p.font)
|
|
294
295
|
s.font = p.font;
|
|
295
|
-
const dashVal = p
|
|
296
|
+
const dashVal = p.dash || p["stroke-dash"];
|
|
296
297
|
if (dashVal) {
|
|
297
298
|
const parts = dashVal
|
|
298
299
|
.split(",")
|
|
@@ -303,10 +304,15 @@ function propsToStyle(p) {
|
|
|
303
304
|
}
|
|
304
305
|
return s;
|
|
305
306
|
}
|
|
307
|
+
function isValueToken(t) {
|
|
308
|
+
return !!t && (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD");
|
|
309
|
+
}
|
|
310
|
+
function isPropKeyToken(t) {
|
|
311
|
+
return !!t && (t.type === "IDENT" || t.type === "KEYWORD");
|
|
312
|
+
}
|
|
306
313
|
function parse(src) {
|
|
307
314
|
resetUid();
|
|
308
315
|
const tokens = tokenize$1(src).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
|
|
309
|
-
// Collapse multiple consecutive NEWLINEs into one
|
|
310
316
|
const flat = [];
|
|
311
317
|
let lastNL = false;
|
|
312
318
|
for (const t of tokens) {
|
|
@@ -335,11 +341,9 @@ function parse(src) {
|
|
|
335
341
|
config: {},
|
|
336
342
|
rootOrder: [],
|
|
337
343
|
};
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const groupIds = new Set();
|
|
342
|
-
const markdownIds = new Set();
|
|
344
|
+
const authoredEntityKinds = new Map();
|
|
345
|
+
const unresolvedGroupItems = new Map();
|
|
346
|
+
const groupTokens = new Map();
|
|
343
347
|
let i = 0;
|
|
344
348
|
const cur = () => flat[i] ?? flat[flat.length - 1];
|
|
345
349
|
const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
|
|
@@ -348,7 +352,6 @@ function parse(src) {
|
|
|
348
352
|
while (cur().type === "NEWLINE")
|
|
349
353
|
skip();
|
|
350
354
|
};
|
|
351
|
-
// Consume until EOL, return all tokens
|
|
352
355
|
function lineTokens() {
|
|
353
356
|
const acc = [];
|
|
354
357
|
while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
|
|
@@ -359,24 +362,104 @@ function parse(src) {
|
|
|
359
362
|
skip();
|
|
360
363
|
return acc;
|
|
361
364
|
}
|
|
365
|
+
function requireExplicitId(keywordTok, toks) {
|
|
366
|
+
const first = toks[0];
|
|
367
|
+
if (!isValueToken(first) || toks[1]?.type === "EQUALS") {
|
|
368
|
+
throw new ParseError(`${keywordTok.value} requires an explicit id before properties`, keywordTok.line, keywordTok.col);
|
|
369
|
+
}
|
|
370
|
+
return first.value;
|
|
371
|
+
}
|
|
372
|
+
function parseSimpleProps(toks, startIndex) {
|
|
373
|
+
const props = {};
|
|
374
|
+
let j = startIndex;
|
|
375
|
+
while (j < toks.length - 1) {
|
|
376
|
+
const key = toks[j];
|
|
377
|
+
const eq = toks[j + 1];
|
|
378
|
+
if (isPropKeyToken(key) && eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
379
|
+
props[key.value] = toks[j + 2].value;
|
|
380
|
+
j += 3;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
j++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return props;
|
|
387
|
+
}
|
|
388
|
+
function parseGroupProps(toks, startIndex) {
|
|
389
|
+
const props = {};
|
|
390
|
+
const itemIds = [];
|
|
391
|
+
let j = startIndex;
|
|
392
|
+
while (j < toks.length) {
|
|
393
|
+
const key = toks[j];
|
|
394
|
+
const eq = toks[j + 1];
|
|
395
|
+
if (!isPropKeyToken(key) || eq?.type !== "EQUALS") {
|
|
396
|
+
j++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (key.value === "items") {
|
|
400
|
+
const open = toks[j + 2];
|
|
401
|
+
if (open?.type !== "LBRACKET") {
|
|
402
|
+
throw new ParseError(`items must use bracket syntax like items=[a,b]`, key.line, key.col);
|
|
403
|
+
}
|
|
404
|
+
j += 3;
|
|
405
|
+
while (j < toks.length && toks[j].type !== "RBRACKET") {
|
|
406
|
+
const tok = toks[j];
|
|
407
|
+
if (tok.type === "COMMA") {
|
|
408
|
+
j++;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (!isValueToken(tok)) {
|
|
412
|
+
const invalidTok = toks[j];
|
|
413
|
+
throw new ParseError(`items can only contain ids like items=[a,b]`, invalidTok.line, invalidTok.col);
|
|
414
|
+
}
|
|
415
|
+
itemIds.push(tok.value);
|
|
416
|
+
j++;
|
|
417
|
+
if (toks[j]?.type === "COMMA") {
|
|
418
|
+
j++;
|
|
419
|
+
}
|
|
420
|
+
else if (toks[j] && toks[j].type !== "RBRACKET") {
|
|
421
|
+
throw new ParseError(`Expected ',' or ']' in items list`, toks[j].line, toks[j].col);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (toks[j]?.type !== "RBRACKET") {
|
|
425
|
+
throw new ParseError(`Unterminated items list; expected ']'`, key.line, key.col);
|
|
426
|
+
}
|
|
427
|
+
j++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (j + 2 < toks.length) {
|
|
431
|
+
props[key.value] = toks[j + 2].value;
|
|
432
|
+
j += 3;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
j++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { props, itemIds };
|
|
439
|
+
}
|
|
440
|
+
function registerAuthoredId(id, kind, tok) {
|
|
441
|
+
const existing = authoredEntityKinds.get(id);
|
|
442
|
+
if (existing) {
|
|
443
|
+
throw new ParseError(`Duplicate id "${id}" already declared as a ${existing}`, tok.line, tok.col);
|
|
444
|
+
}
|
|
445
|
+
authoredEntityKinds.set(id, kind);
|
|
446
|
+
}
|
|
362
447
|
function parseDataArray() {
|
|
363
448
|
const rows = [];
|
|
364
449
|
while (cur().type !== "LBRACKET" && cur().type !== "EOF")
|
|
365
450
|
skip();
|
|
366
|
-
skip();
|
|
451
|
+
skip();
|
|
367
452
|
skipNL();
|
|
368
453
|
while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
|
|
369
454
|
skipNL();
|
|
370
455
|
if (cur().type === "RBRACKET" || cur().type === "EOF")
|
|
371
|
-
break;
|
|
456
|
+
break;
|
|
372
457
|
if (cur().type === "LBRACKET") {
|
|
373
458
|
skip();
|
|
374
459
|
const row = [];
|
|
375
460
|
while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
|
|
376
461
|
const v = cur();
|
|
377
|
-
if (v.type === "STRING" ||
|
|
378
|
-
v.type === "IDENT" ||
|
|
379
|
-
v.type === "KEYWORD") {
|
|
462
|
+
if (v.type === "STRING" || v.type === "IDENT" || v.type === "KEYWORD") {
|
|
380
463
|
row.push(v.value);
|
|
381
464
|
skip();
|
|
382
465
|
}
|
|
@@ -387,8 +470,9 @@ function parse(src) {
|
|
|
387
470
|
else if (v.type === "COMMA" || v.type === "NEWLINE") {
|
|
388
471
|
skip();
|
|
389
472
|
}
|
|
390
|
-
else
|
|
473
|
+
else {
|
|
391
474
|
break;
|
|
475
|
+
}
|
|
392
476
|
}
|
|
393
477
|
if (cur().type === "RBRACKET")
|
|
394
478
|
skip();
|
|
@@ -397,46 +481,25 @@ function parse(src) {
|
|
|
397
481
|
else if (cur().type === "COMMA" || cur().type === "NEWLINE") {
|
|
398
482
|
skip();
|
|
399
483
|
}
|
|
400
|
-
else
|
|
484
|
+
else {
|
|
401
485
|
skip();
|
|
486
|
+
}
|
|
402
487
|
}
|
|
403
488
|
if (cur().type === "RBRACKET")
|
|
404
489
|
skip();
|
|
405
490
|
return rows;
|
|
406
491
|
}
|
|
407
|
-
function parseNode(shape
|
|
408
|
-
|
|
492
|
+
function parseNode(shape) {
|
|
493
|
+
const keywordTok = cur();
|
|
494
|
+
skip();
|
|
409
495
|
const toks = lineTokens();
|
|
410
|
-
|
|
411
|
-
const props =
|
|
412
|
-
let j = 0;
|
|
413
|
-
// First token may be the node id
|
|
414
|
-
if (j < toks.length &&
|
|
415
|
-
(toks[j].type === "IDENT" || toks[j].type === "STRING")) {
|
|
416
|
-
id = toks[j++].value;
|
|
417
|
-
}
|
|
418
|
-
// Remaining tokens are key=value pairs
|
|
419
|
-
while (j < toks.length) {
|
|
420
|
-
const t = toks[j];
|
|
421
|
-
if ((t.type === "IDENT" || t.type === "KEYWORD") &&
|
|
422
|
-
j + 1 < toks.length &&
|
|
423
|
-
toks[j + 1].type === "EQUALS") {
|
|
424
|
-
const key = t.value;
|
|
425
|
-
j += 2;
|
|
426
|
-
if (j < toks.length) {
|
|
427
|
-
props[key] = toks[j].value;
|
|
428
|
-
j++;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else
|
|
432
|
-
j++;
|
|
433
|
-
}
|
|
496
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
497
|
+
const props = parseSimpleProps(toks, 1);
|
|
434
498
|
const node = {
|
|
435
499
|
kind: "node",
|
|
436
500
|
id,
|
|
437
501
|
shape,
|
|
438
502
|
label: props.label || "",
|
|
439
|
-
...(groupId ? { groupId } : {}),
|
|
440
503
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
441
504
|
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
442
505
|
...(props.deg ? { deg: parseFloat(props.deg) } : {}),
|
|
@@ -454,113 +517,56 @@ function parse(src) {
|
|
|
454
517
|
node.pathData = props.value;
|
|
455
518
|
return node;
|
|
456
519
|
}
|
|
457
|
-
function
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
throw new ParseError("Expected edge target", 0, 0);
|
|
461
|
-
const toId = toTok.value;
|
|
462
|
-
const props = {};
|
|
463
|
-
let j = 0;
|
|
464
|
-
while (j < rest.length) {
|
|
465
|
-
const t = rest[j];
|
|
466
|
-
if ((t.type === "IDENT" || t.type === "KEYWORD") &&
|
|
467
|
-
j + 1 < rest.length &&
|
|
468
|
-
rest[j + 1].type === "EQUALS") {
|
|
469
|
-
const key = t.value;
|
|
470
|
-
j += 2;
|
|
471
|
-
if (j < rest.length) {
|
|
472
|
-
props[key] = rest[j].value;
|
|
473
|
-
j++;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
else
|
|
477
|
-
j++;
|
|
478
|
-
}
|
|
479
|
-
const dashed = connector.includes("--") ||
|
|
480
|
-
connector.includes(".-") ||
|
|
481
|
-
connector.includes("-.");
|
|
482
|
-
const bidir = connector.includes("<") && connector.includes(">");
|
|
483
|
-
return {
|
|
484
|
-
kind: "edge",
|
|
485
|
-
id: uid("edge"),
|
|
486
|
-
from: fromId,
|
|
487
|
-
to: toId,
|
|
488
|
-
connector: connector,
|
|
489
|
-
label: props.label,
|
|
490
|
-
dashed,
|
|
491
|
-
bidirectional: bidir,
|
|
492
|
-
style: propsToStyle(props),
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
// ── parseNote → returns ASTNode with shape='note' ────────
|
|
496
|
-
function parseNote(groupId) {
|
|
497
|
-
skip(); // 'note'
|
|
520
|
+
function parseNote() {
|
|
521
|
+
const keywordTok = cur();
|
|
522
|
+
skip();
|
|
498
523
|
const toks = lineTokens();
|
|
499
|
-
|
|
500
|
-
if (toks[0])
|
|
501
|
-
id = toks[0].value;
|
|
524
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
502
525
|
const props = {};
|
|
503
526
|
let j = 1;
|
|
504
|
-
// Backward compat: second token is a bare/quoted string → label
|
|
505
527
|
if (toks[1] &&
|
|
506
528
|
(toks[1].type === "STRING" ||
|
|
507
529
|
(toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
|
|
508
530
|
props.label = toks[1].value;
|
|
509
531
|
j = 2;
|
|
510
532
|
}
|
|
511
|
-
|
|
512
|
-
while (j < toks.length - 1) {
|
|
513
|
-
const k = toks[j];
|
|
514
|
-
const eq = toks[j + 1];
|
|
515
|
-
if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
|
|
516
|
-
props[k.value] = toks[j + 2].value;
|
|
517
|
-
j += 3;
|
|
518
|
-
}
|
|
519
|
-
else
|
|
520
|
-
j++;
|
|
521
|
-
}
|
|
522
|
-
// Support multiline via literal \n in label string
|
|
523
|
-
const rawLabel = props.label ?? "";
|
|
533
|
+
Object.assign(props, parseSimpleProps(toks, j));
|
|
524
534
|
return {
|
|
525
535
|
kind: "node",
|
|
526
536
|
id,
|
|
527
537
|
shape: "note",
|
|
528
|
-
label:
|
|
529
|
-
groupId,
|
|
538
|
+
label: (props.label ?? "").replace(/\\n/g, "\n"),
|
|
530
539
|
theme: props.theme,
|
|
531
540
|
style: propsToStyle(props),
|
|
532
541
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
533
542
|
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
534
543
|
};
|
|
535
544
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
skip();
|
|
545
|
+
function parseGroup() {
|
|
546
|
+
const keywordTok = cur();
|
|
547
|
+
skip();
|
|
539
548
|
const toks = lineTokens();
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
549
|
+
if (toks.some((t) => t.type === "LBRACE" || t.type === "RBRACE")) {
|
|
550
|
+
throw new ParseError(`Nested group blocks were removed. Use ${keywordTok.value} <id> items=[...] instead.`, keywordTok.line, keywordTok.col);
|
|
551
|
+
}
|
|
552
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
543
553
|
const props = {};
|
|
544
554
|
let j = 1;
|
|
545
|
-
// Backward compat: second token is a quoted/bare string (old label syntax)
|
|
546
555
|
if (toks[1] &&
|
|
547
556
|
(toks[1].type === "STRING" ||
|
|
548
557
|
(toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
|
|
549
558
|
props.label = toks[1].value;
|
|
550
559
|
j = 2;
|
|
551
560
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
props[k.value] = toks[j + 2].value;
|
|
558
|
-
j += 3;
|
|
559
|
-
}
|
|
560
|
-
else
|
|
561
|
-
j++;
|
|
561
|
+
const parsed = parseGroupProps(toks, j);
|
|
562
|
+
Object.assign(props, parsed.props);
|
|
563
|
+
skipNL();
|
|
564
|
+
if (cur().type === "LBRACE") {
|
|
565
|
+
throw new ParseError(`Nested group blocks were removed. Use ${keywordTok.value} ${id} items=[...] instead.`, cur().line, cur().col);
|
|
562
566
|
}
|
|
563
|
-
|
|
567
|
+
unresolvedGroupItems.set(id, parsed.itemIds);
|
|
568
|
+
groupTokens.set(id, keywordTok);
|
|
569
|
+
return {
|
|
564
570
|
kind: "group",
|
|
565
571
|
id,
|
|
566
572
|
label: props.label ?? "",
|
|
@@ -573,109 +579,43 @@ function parse(src) {
|
|
|
573
579
|
justify: props.justify,
|
|
574
580
|
theme: props.theme,
|
|
575
581
|
style: propsToStyle(props),
|
|
576
|
-
width: props.width !== undefined ? parseFloat(props.width) : undefined,
|
|
582
|
+
width: props.width !== undefined ? parseFloat(props.width) : undefined,
|
|
577
583
|
height: props.height !== undefined ? parseFloat(props.height) : undefined,
|
|
578
584
|
};
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const isBare = v === "bare";
|
|
594
|
-
const nested = parseGroup();
|
|
595
|
-
if (isBare) {
|
|
596
|
-
nested.label = "";
|
|
597
|
-
nested.style = {
|
|
598
|
-
...nested.style,
|
|
599
|
-
fill: nested.style?.fill ?? "none",
|
|
600
|
-
stroke: nested.style?.stroke ?? "none",
|
|
601
|
-
strokeWidth: nested.style?.strokeWidth ?? 0,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
ast.groups.push(nested);
|
|
605
|
-
groupIds.add(nested.id);
|
|
606
|
-
group.children.push({ kind: "group", id: nested.id });
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
// ── Table ─────────────────────────────────────────
|
|
610
|
-
if (v === "table") {
|
|
611
|
-
const tbl = parseTable();
|
|
612
|
-
ast.tables.push(tbl);
|
|
613
|
-
tableIds.add(tbl.id);
|
|
614
|
-
group.children.push({ kind: "table", id: tbl.id });
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
// ── Note (parsed as node with shape='note') ──────
|
|
618
|
-
if (v === "note") {
|
|
619
|
-
const note = parseNote(id);
|
|
620
|
-
ast.nodes.push(note);
|
|
621
|
-
nodeIds.add(note.id);
|
|
622
|
-
group.children.push({ kind: "node", id: note.id });
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
// ── Markdown ───────────────────────────────────────
|
|
626
|
-
if (v === "markdown") {
|
|
627
|
-
const md = parseMarkdown(id);
|
|
628
|
-
ast.markdowns.push(md);
|
|
629
|
-
markdownIds.add(md.id);
|
|
630
|
-
group.children.push({ kind: "markdown", id: md.id });
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
if (v === "bare") {
|
|
634
|
-
// treat exactly like 'group' but inject defaults
|
|
635
|
-
const grp = parseGroup(); // reuse parseGroup
|
|
636
|
-
grp.label = "";
|
|
637
|
-
grp.style = { ...grp.style, stroke: "none", fill: "none" };
|
|
638
|
-
// rest is identical to group handling
|
|
639
|
-
}
|
|
640
|
-
// ── Chart ──────────────────────────────────────────
|
|
641
|
-
if (CHART_TYPES.includes(v)) {
|
|
642
|
-
const chart = parseChart(v);
|
|
643
|
-
ast.charts.push(chart);
|
|
644
|
-
chartIds.add(chart.id);
|
|
645
|
-
group.children.push({ kind: "chart", id: chart.id });
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
// ── Node shape ────────────────────────────────────
|
|
649
|
-
if (SHAPES$1.includes(v)) {
|
|
650
|
-
const node = parseNode(v, id);
|
|
651
|
-
if (!nodeIds.has(node.id)) {
|
|
652
|
-
nodeIds.add(node.id);
|
|
653
|
-
ast.nodes.push(node);
|
|
654
|
-
}
|
|
655
|
-
group.children.push({ kind: "node", id: node.id });
|
|
656
|
-
continue;
|
|
585
|
+
}
|
|
586
|
+
function parseEdge(fromId, connector, rest) {
|
|
587
|
+
const toTok = rest.shift();
|
|
588
|
+
if (!toTok)
|
|
589
|
+
throw new ParseError("Expected edge target", 0, 0);
|
|
590
|
+
const props = {};
|
|
591
|
+
let j = 0;
|
|
592
|
+
while (j < rest.length) {
|
|
593
|
+
const t = rest[j];
|
|
594
|
+
if ((t.type === "IDENT" || t.type === "KEYWORD") &&
|
|
595
|
+
j + 1 < rest.length &&
|
|
596
|
+
rest[j + 1].type === "EQUALS") {
|
|
597
|
+
props[t.value] = rest[j + 2]?.value ?? "";
|
|
598
|
+
j += 3;
|
|
657
599
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
cur().type === "STRING" ||
|
|
661
|
-
cur().type === "KEYWORD") {
|
|
662
|
-
const nextTok = flat[i + 1];
|
|
663
|
-
if (nextTok && nextTok.type === "ARROW") {
|
|
664
|
-
const lineToks = lineTokens();
|
|
665
|
-
if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
|
|
666
|
-
const fromId = lineToks[0].value;
|
|
667
|
-
const conn = lineToks[1].value;
|
|
668
|
-
const edge = parseEdge(fromId, conn, lineToks.slice(2));
|
|
669
|
-
ast.edges.push(edge);
|
|
670
|
-
}
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
600
|
+
else {
|
|
601
|
+
j++;
|
|
673
602
|
}
|
|
674
|
-
skip();
|
|
675
603
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
604
|
+
const dashed = connector.includes("--") ||
|
|
605
|
+
connector.includes(".-") ||
|
|
606
|
+
connector.includes("-.");
|
|
607
|
+
const bidirectional = connector.includes("<") && connector.includes(">");
|
|
608
|
+
return {
|
|
609
|
+
kind: "edge",
|
|
610
|
+
id: uid("edge"),
|
|
611
|
+
from: fromId,
|
|
612
|
+
to: toTok.value,
|
|
613
|
+
connector: connector,
|
|
614
|
+
label: props.label,
|
|
615
|
+
dashed,
|
|
616
|
+
bidirectional,
|
|
617
|
+
style: propsToStyle(props),
|
|
618
|
+
};
|
|
679
619
|
}
|
|
680
620
|
function parseStep() {
|
|
681
621
|
skip();
|
|
@@ -686,12 +626,10 @@ function parse(src) {
|
|
|
686
626
|
target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
687
627
|
}
|
|
688
628
|
const step = { kind: "step", action, target };
|
|
689
|
-
// narrate: text is the value, not a target
|
|
690
629
|
if (action === "narrate") {
|
|
691
630
|
step.target = "";
|
|
692
631
|
step.value = toks[1]?.value ?? "";
|
|
693
632
|
}
|
|
694
|
-
// bracket: needs two targets
|
|
695
633
|
if (action === "bracket" && toks.length >= 3) {
|
|
696
634
|
step.target = toks[1]?.value ?? "";
|
|
697
635
|
step.target2 = toks[2]?.value ?? "";
|
|
@@ -701,7 +639,6 @@ function parse(src) {
|
|
|
701
639
|
const k = toks[j]?.value;
|
|
702
640
|
const eq = toks[j + 1];
|
|
703
641
|
const vt = toks[j + 2];
|
|
704
|
-
// key=value form
|
|
705
642
|
if (eq?.type === "EQUALS" && vt) {
|
|
706
643
|
if (k === "dx") {
|
|
707
644
|
step.dx = parseFloat(vt.value);
|
|
@@ -733,12 +670,7 @@ function parse(src) {
|
|
|
733
670
|
j += 2;
|
|
734
671
|
continue;
|
|
735
672
|
}
|
|
736
|
-
if (k === "fill") {
|
|
737
|
-
step.value = vt.value;
|
|
738
|
-
j += 2;
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
if (k === "color") {
|
|
673
|
+
if (k === "fill" || k === "color") {
|
|
742
674
|
step.value = vt.value;
|
|
743
675
|
j += 2;
|
|
744
676
|
continue;
|
|
@@ -749,7 +681,6 @@ function parse(src) {
|
|
|
749
681
|
continue;
|
|
750
682
|
}
|
|
751
683
|
}
|
|
752
|
-
// bare key value (legacy)
|
|
753
684
|
if (k === "delay" && eq?.type === "NUMBER") {
|
|
754
685
|
step.delay = parseFloat(eq.value);
|
|
755
686
|
j++;
|
|
@@ -763,89 +694,16 @@ function parse(src) {
|
|
|
763
694
|
if (k === "trigger") {
|
|
764
695
|
step.trigger = eq?.value;
|
|
765
696
|
j++;
|
|
766
|
-
continue;
|
|
767
697
|
}
|
|
768
698
|
}
|
|
769
699
|
return step;
|
|
770
700
|
}
|
|
771
|
-
// function parseStep(): ASTStep {
|
|
772
|
-
// skip(); // 'step'
|
|
773
|
-
// const toks = lineTokens();
|
|
774
|
-
// const action = (toks[0]?.value ?? "highlight") as AnimationAction;
|
|
775
|
-
// let target = toks[1]?.value ?? "";
|
|
776
|
-
// if (toks[2]?.type === "ARROW" && toks[3]) {
|
|
777
|
-
// target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
778
|
-
// }
|
|
779
|
-
// const step: ASTStep = { kind: "step", action, target };
|
|
780
|
-
// for (let j = 2; j < toks.length - 1; j++) {
|
|
781
|
-
// const k = toks[j].value;
|
|
782
|
-
// const eq = toks[j + 1];
|
|
783
|
-
// const vt = toks[j + 2];
|
|
784
|
-
// // key=value form (dx=50, dy=-80, duration=600)
|
|
785
|
-
// if (eq?.type === "EQUALS" && vt) {
|
|
786
|
-
// if (k === "dx") {
|
|
787
|
-
// step.dx = parseFloat(vt.value);
|
|
788
|
-
// j += 2;
|
|
789
|
-
// continue;
|
|
790
|
-
// }
|
|
791
|
-
// if (k === "dy") {
|
|
792
|
-
// step.dy = parseFloat(vt.value);
|
|
793
|
-
// j += 2;
|
|
794
|
-
// continue;
|
|
795
|
-
// }
|
|
796
|
-
// if (k === "duration") {
|
|
797
|
-
// step.duration = parseFloat(vt.value);
|
|
798
|
-
// j += 2;
|
|
799
|
-
// continue;
|
|
800
|
-
// }
|
|
801
|
-
// if (k === "delay") {
|
|
802
|
-
// step.delay = parseFloat(vt.value);
|
|
803
|
-
// j += 2;
|
|
804
|
-
// continue;
|
|
805
|
-
// }
|
|
806
|
-
// }
|
|
807
|
-
// // bare key value form (legacy: delay 500, duration 400)
|
|
808
|
-
// if (k === "delay" && eq?.type === "NUMBER") {
|
|
809
|
-
// step.delay = parseFloat(eq.value);
|
|
810
|
-
// j++;
|
|
811
|
-
// }
|
|
812
|
-
// if (k === "duration" && eq?.type === "NUMBER") {
|
|
813
|
-
// step.duration = parseFloat(eq.value);
|
|
814
|
-
// j++;
|
|
815
|
-
// }
|
|
816
|
-
// if (k === "trigger") {
|
|
817
|
-
// step.trigger = eq?.value as AnimationTrigger;
|
|
818
|
-
// j++;
|
|
819
|
-
// }
|
|
820
|
-
// if (k === "factor") {
|
|
821
|
-
// step.factor = parseFloat(vt.value);
|
|
822
|
-
// j += 2;
|
|
823
|
-
// continue;
|
|
824
|
-
// }
|
|
825
|
-
// if (k === "deg") {
|
|
826
|
-
// step.deg = parseFloat(vt.value);
|
|
827
|
-
// j += 2;
|
|
828
|
-
// continue;
|
|
829
|
-
// }
|
|
830
|
-
// }
|
|
831
|
-
// return step;
|
|
832
|
-
// }
|
|
833
701
|
function parseChart(chartType) {
|
|
702
|
+
const keywordTok = cur();
|
|
834
703
|
skip();
|
|
835
704
|
const toks = lineTokens();
|
|
836
|
-
const id = toks
|
|
837
|
-
const props =
|
|
838
|
-
let j = 1;
|
|
839
|
-
while (j < toks.length - 1) {
|
|
840
|
-
const k = toks[j];
|
|
841
|
-
const eq = toks[j + 1];
|
|
842
|
-
if (eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
843
|
-
props[k.value] = toks[j + 2].value;
|
|
844
|
-
j += 3;
|
|
845
|
-
}
|
|
846
|
-
else
|
|
847
|
-
j++;
|
|
848
|
-
}
|
|
705
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
706
|
+
const props = parseSimpleProps(toks, 1);
|
|
849
707
|
let dataRows = [];
|
|
850
708
|
skipNL();
|
|
851
709
|
while (cur().type !== "EOF" && cur().value !== "end") {
|
|
@@ -858,30 +716,31 @@ function parse(src) {
|
|
|
858
716
|
}
|
|
859
717
|
else if ((cur().type === "IDENT" || cur().type === "KEYWORD") &&
|
|
860
718
|
peek1().type === "EQUALS") {
|
|
861
|
-
const
|
|
719
|
+
const key = cur().value;
|
|
862
720
|
skip();
|
|
863
721
|
skip();
|
|
864
|
-
props[
|
|
722
|
+
props[key] = cur().value;
|
|
865
723
|
skip();
|
|
866
724
|
}
|
|
867
725
|
else if (SHAPES$1.includes(v) ||
|
|
868
726
|
v === "step" ||
|
|
869
727
|
v === "group" ||
|
|
870
|
-
v === "
|
|
728
|
+
v === "bare" ||
|
|
729
|
+
v === "note" ||
|
|
871
730
|
v === "table" ||
|
|
872
|
-
v === "config" ||
|
|
873
|
-
v === "theme" ||
|
|
731
|
+
v === "config" ||
|
|
732
|
+
v === "theme" ||
|
|
874
733
|
v === "style" ||
|
|
875
734
|
v === "markdown" ||
|
|
876
735
|
CHART_TYPES.includes(v)) {
|
|
877
736
|
break;
|
|
878
737
|
}
|
|
879
738
|
else if (peek1().type === "ARROW") {
|
|
880
|
-
// ← ADD THIS WHOLE BLOCK
|
|
881
739
|
break;
|
|
882
740
|
}
|
|
883
|
-
else
|
|
741
|
+
else {
|
|
884
742
|
skip();
|
|
743
|
+
}
|
|
885
744
|
}
|
|
886
745
|
const headers = dataRows[0]?.map(String) ?? [];
|
|
887
746
|
const rows = dataRows.slice(1);
|
|
@@ -898,29 +757,19 @@ function parse(src) {
|
|
|
898
757
|
};
|
|
899
758
|
}
|
|
900
759
|
function parseTable() {
|
|
901
|
-
|
|
760
|
+
const keywordTok = cur();
|
|
761
|
+
skip();
|
|
902
762
|
const toks = lineTokens();
|
|
903
|
-
|
|
904
|
-
if (toks[0])
|
|
905
|
-
id = toks[0].value;
|
|
763
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
906
764
|
const props = {};
|
|
907
765
|
let j = 1;
|
|
908
|
-
// label="..." or bare second token
|
|
909
766
|
if (toks[1] &&
|
|
910
767
|
(toks[1].type === "STRING" ||
|
|
911
768
|
(toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
|
|
912
769
|
props.label = toks[1].value;
|
|
913
770
|
j = 2;
|
|
914
771
|
}
|
|
915
|
-
|
|
916
|
-
const k = toks[j], eq = toks[j + 1];
|
|
917
|
-
if (eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
918
|
-
props[k.value] = toks[j + 2].value;
|
|
919
|
-
j += 3;
|
|
920
|
-
}
|
|
921
|
-
else
|
|
922
|
-
j++;
|
|
923
|
-
}
|
|
772
|
+
Object.assign(props, parseSimpleProps(toks, j));
|
|
924
773
|
const table = {
|
|
925
774
|
kind: "table",
|
|
926
775
|
id,
|
|
@@ -947,7 +796,8 @@ function parse(src) {
|
|
|
947
796
|
while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
|
|
948
797
|
if (cur().type === "STRING" ||
|
|
949
798
|
cur().type === "IDENT" ||
|
|
950
|
-
cur().type === "NUMBER"
|
|
799
|
+
cur().type === "NUMBER" ||
|
|
800
|
+
cur().type === "KEYWORD") {
|
|
951
801
|
cells.push(cur().value);
|
|
952
802
|
}
|
|
953
803
|
skip();
|
|
@@ -956,31 +806,20 @@ function parse(src) {
|
|
|
956
806
|
skip();
|
|
957
807
|
table.rows.push({ kind: v === "header" ? "header" : "data", cells });
|
|
958
808
|
}
|
|
959
|
-
else
|
|
809
|
+
else {
|
|
960
810
|
skip();
|
|
811
|
+
}
|
|
961
812
|
}
|
|
962
813
|
if (cur().type === "RBRACE")
|
|
963
814
|
skip();
|
|
964
815
|
return table;
|
|
965
816
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
skip();
|
|
817
|
+
function parseMarkdown() {
|
|
818
|
+
const keywordTok = cur();
|
|
819
|
+
skip();
|
|
969
820
|
const toks = lineTokens();
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
id = toks[0].value;
|
|
973
|
-
const props = {};
|
|
974
|
-
let j = 1;
|
|
975
|
-
while (j < toks.length - 1) {
|
|
976
|
-
const k = toks[j], eq = toks[j + 1];
|
|
977
|
-
if (eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
978
|
-
props[k.value] = toks[j + 2].value;
|
|
979
|
-
j += 3;
|
|
980
|
-
}
|
|
981
|
-
else
|
|
982
|
-
j++;
|
|
983
|
-
}
|
|
821
|
+
const id = requireExplicitId(keywordTok, toks);
|
|
822
|
+
const props = parseSimpleProps(toks, 1);
|
|
984
823
|
skipNL();
|
|
985
824
|
let content = "";
|
|
986
825
|
if (cur().type === "STRING_BLOCK") {
|
|
@@ -997,7 +836,6 @@ function parse(src) {
|
|
|
997
836
|
style: propsToStyle(props),
|
|
998
837
|
};
|
|
999
838
|
}
|
|
1000
|
-
// ── Main parse loop ─────────────────────────────────────
|
|
1001
839
|
skipNL();
|
|
1002
840
|
if (cur().value === "diagram")
|
|
1003
841
|
skip();
|
|
@@ -1016,19 +854,16 @@ function parse(src) {
|
|
|
1016
854
|
}
|
|
1017
855
|
if (v === "end")
|
|
1018
856
|
break;
|
|
1019
|
-
// direction — silently ignored (removed from engine)
|
|
1020
857
|
if (v === "direction") {
|
|
1021
858
|
lineTokens();
|
|
1022
859
|
continue;
|
|
1023
860
|
}
|
|
1024
|
-
// layout
|
|
1025
861
|
if (v === "layout") {
|
|
1026
862
|
skip();
|
|
1027
863
|
ast.layout = cur().value ?? "column";
|
|
1028
864
|
skip();
|
|
1029
865
|
continue;
|
|
1030
866
|
}
|
|
1031
|
-
// title
|
|
1032
867
|
if (v === "title") {
|
|
1033
868
|
skip();
|
|
1034
869
|
const toks = lineTokens();
|
|
@@ -1038,14 +873,10 @@ function parse(src) {
|
|
|
1038
873
|
ast.title = toks[idx + 2]?.value ?? "";
|
|
1039
874
|
}
|
|
1040
875
|
else {
|
|
1041
|
-
ast.title = toks
|
|
1042
|
-
.map((t2) => t2.value)
|
|
1043
|
-
.join(" ")
|
|
1044
|
-
.replace(/"/g, "");
|
|
876
|
+
ast.title = toks.map((t2) => t2.value).join(" ").replace(/"/g, "");
|
|
1045
877
|
}
|
|
1046
878
|
continue;
|
|
1047
879
|
}
|
|
1048
|
-
// description
|
|
1049
880
|
if (v === "description") {
|
|
1050
881
|
skip();
|
|
1051
882
|
ast.description = lineTokens()
|
|
@@ -1054,65 +885,38 @@ function parse(src) {
|
|
|
1054
885
|
.replace(/"/g, "");
|
|
1055
886
|
continue;
|
|
1056
887
|
}
|
|
1057
|
-
// config
|
|
1058
888
|
if (v === "config") {
|
|
1059
889
|
skip();
|
|
1060
|
-
const
|
|
890
|
+
const key = cur().value;
|
|
1061
891
|
skip();
|
|
1062
892
|
if (cur().type === "EQUALS")
|
|
1063
893
|
skip();
|
|
1064
|
-
const
|
|
894
|
+
const value = cur().value;
|
|
1065
895
|
skip();
|
|
1066
|
-
ast.config[
|
|
896
|
+
ast.config[key] = value;
|
|
1067
897
|
continue;
|
|
1068
898
|
}
|
|
1069
|
-
// style
|
|
1070
899
|
if (v === "style") {
|
|
1071
900
|
skip();
|
|
1072
901
|
const targetId = cur().value;
|
|
1073
902
|
skip();
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
let j = 0;
|
|
1077
|
-
while (j < lineToks.length - 1) {
|
|
1078
|
-
const k2 = lineToks[j];
|
|
1079
|
-
const eq = lineToks[j + 1];
|
|
1080
|
-
if (eq.type === "EQUALS") {
|
|
1081
|
-
p[k2.value] = lineToks[j + 2]?.value ?? "";
|
|
1082
|
-
j += 3;
|
|
1083
|
-
}
|
|
1084
|
-
else
|
|
1085
|
-
j++;
|
|
1086
|
-
}
|
|
1087
|
-
ast.styles[targetId] = { ...ast.styles[targetId], ...propsToStyle(p) };
|
|
903
|
+
const props = parseSimpleProps(lineTokens(), 0);
|
|
904
|
+
ast.styles[targetId] = { ...ast.styles[targetId], ...propsToStyle(props) };
|
|
1088
905
|
continue;
|
|
1089
906
|
}
|
|
1090
|
-
// theme
|
|
1091
907
|
if (v === "theme") {
|
|
1092
908
|
skip();
|
|
1093
909
|
const toks = lineTokens();
|
|
1094
910
|
const themeId = toks[0]?.value;
|
|
1095
911
|
if (!themeId)
|
|
1096
912
|
continue;
|
|
1097
|
-
|
|
1098
|
-
let j = 1;
|
|
1099
|
-
while (j < toks.length - 1) {
|
|
1100
|
-
const k2 = toks[j];
|
|
1101
|
-
const eq = toks[j + 1];
|
|
1102
|
-
if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
|
|
1103
|
-
props[k2.value] = toks[j + 2].value;
|
|
1104
|
-
j += 3;
|
|
1105
|
-
}
|
|
1106
|
-
else
|
|
1107
|
-
j++;
|
|
1108
|
-
}
|
|
1109
|
-
ast.themes[themeId] = propsToStyle(props);
|
|
913
|
+
ast.themes[themeId] = propsToStyle(parseSimpleProps(toks, 1));
|
|
1110
914
|
continue;
|
|
1111
915
|
}
|
|
1112
|
-
// group
|
|
1113
916
|
if (v === "group" || v === "bare") {
|
|
1114
917
|
const isBare = v === "bare";
|
|
1115
918
|
const grp = parseGroup();
|
|
919
|
+
registerAuthoredId(grp.id, "group", t);
|
|
1116
920
|
if (isBare) {
|
|
1117
921
|
grp.label = "";
|
|
1118
922
|
grp.style = {
|
|
@@ -1123,36 +927,34 @@ function parse(src) {
|
|
|
1123
927
|
};
|
|
1124
928
|
}
|
|
1125
929
|
ast.groups.push(grp);
|
|
1126
|
-
groupIds.add(grp.id);
|
|
1127
930
|
ast.rootOrder.push({ kind: "group", id: grp.id });
|
|
1128
931
|
continue;
|
|
1129
932
|
}
|
|
1130
|
-
// table
|
|
1131
933
|
if (v === "table") {
|
|
1132
934
|
const tbl = parseTable();
|
|
935
|
+
registerAuthoredId(tbl.id, "table", t);
|
|
1133
936
|
ast.tables.push(tbl);
|
|
1134
|
-
tableIds.add(tbl.id);
|
|
1135
937
|
ast.rootOrder.push({ kind: "table", id: tbl.id });
|
|
1136
938
|
continue;
|
|
1137
939
|
}
|
|
1138
|
-
// note (parsed as node with shape='note')
|
|
1139
940
|
if (v === "note") {
|
|
1140
941
|
const note = parseNote();
|
|
942
|
+
registerAuthoredId(note.id, "node", t);
|
|
1141
943
|
ast.nodes.push(note);
|
|
1142
|
-
nodeIds.add(note.id);
|
|
1143
944
|
ast.rootOrder.push({ kind: "node", id: note.id });
|
|
1144
945
|
continue;
|
|
1145
946
|
}
|
|
1146
|
-
// beat { ... } — parallel steps
|
|
1147
947
|
if (v === "beat") {
|
|
1148
|
-
skip();
|
|
948
|
+
skip();
|
|
1149
949
|
skipNL();
|
|
1150
950
|
if (cur().type === "LBRACE") {
|
|
1151
951
|
skip();
|
|
1152
952
|
skipNL();
|
|
1153
953
|
}
|
|
1154
954
|
const children = [];
|
|
1155
|
-
while (cur().type !== "RBRACE" &&
|
|
955
|
+
while (cur().type !== "RBRACE" &&
|
|
956
|
+
cur().value !== "end" &&
|
|
957
|
+
cur().type !== "EOF") {
|
|
1156
958
|
skipNL();
|
|
1157
959
|
if (cur().type === "RBRACE")
|
|
1158
960
|
break;
|
|
@@ -1168,69 +970,120 @@ function parse(src) {
|
|
|
1168
970
|
ast.steps.push({ kind: "beat", children });
|
|
1169
971
|
continue;
|
|
1170
972
|
}
|
|
1171
|
-
// step
|
|
1172
973
|
if (v === "step") {
|
|
1173
974
|
ast.steps.push(parseStep());
|
|
1174
975
|
continue;
|
|
1175
976
|
}
|
|
1176
|
-
// charts
|
|
1177
977
|
if (CHART_TYPES.includes(v)) {
|
|
1178
978
|
const chart = parseChart(v);
|
|
979
|
+
registerAuthoredId(chart.id, "chart", t);
|
|
1179
980
|
ast.charts.push(chart);
|
|
1180
|
-
|
|
1181
|
-
ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
|
|
981
|
+
ast.rootOrder.push({ kind: "chart", id: chart.id });
|
|
1182
982
|
continue;
|
|
1183
983
|
}
|
|
1184
984
|
if (v === "markdown") {
|
|
1185
985
|
const md = parseMarkdown();
|
|
986
|
+
registerAuthoredId(md.id, "markdown", t);
|
|
1186
987
|
ast.markdowns.push(md);
|
|
1187
|
-
markdownIds.add(md.id);
|
|
1188
988
|
ast.rootOrder.push({ kind: "markdown", id: md.id });
|
|
1189
989
|
continue;
|
|
1190
990
|
}
|
|
1191
|
-
// edge: A -> B (MUST come before shape check)
|
|
1192
991
|
if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
|
|
1193
992
|
const nextTok = flat[i + 1];
|
|
1194
993
|
if (nextTok && nextTok.type === "ARROW") {
|
|
1195
994
|
const lineToks = lineTokens();
|
|
1196
995
|
if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
|
|
1197
996
|
const fromId = lineToks[0].value;
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
ast.edges.push(edge);
|
|
1201
|
-
// Auto-create implied nodes if they don't exist yet
|
|
1202
|
-
for (const nid of [fromId, edge.to]) {
|
|
1203
|
-
if (!nodeIds.has(nid) &&
|
|
1204
|
-
!tableIds.has(nid) &&
|
|
1205
|
-
!chartIds.has(nid) &&
|
|
1206
|
-
!groupIds.has(nid)) {
|
|
1207
|
-
nodeIds.add(nid);
|
|
1208
|
-
ast.nodes.push({
|
|
1209
|
-
kind: "node",
|
|
1210
|
-
id: nid,
|
|
1211
|
-
shape: "box",
|
|
1212
|
-
label: nid,
|
|
1213
|
-
style: {},
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
997
|
+
const connector = lineToks[1].value;
|
|
998
|
+
ast.edges.push(parseEdge(fromId, connector, lineToks.slice(2)));
|
|
1217
999
|
continue;
|
|
1218
1000
|
}
|
|
1219
1001
|
}
|
|
1220
1002
|
}
|
|
1221
|
-
// node shapes — only reached if NOT followed by an arrow
|
|
1222
1003
|
if (SHAPES$1.includes(v)) {
|
|
1223
1004
|
const node = parseNode(v);
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
ast.rootOrder.push({ kind: "node", id: node.id });
|
|
1228
|
-
}
|
|
1005
|
+
registerAuthoredId(node.id, "node", t);
|
|
1006
|
+
ast.nodes.push(node);
|
|
1007
|
+
ast.rootOrder.push({ kind: "node", id: node.id });
|
|
1229
1008
|
continue;
|
|
1230
1009
|
}
|
|
1231
1010
|
skip();
|
|
1232
1011
|
}
|
|
1233
|
-
|
|
1012
|
+
const allKnownIds = new Set(authoredEntityKinds.keys());
|
|
1013
|
+
for (const edge of ast.edges) {
|
|
1014
|
+
for (const id of [edge.from, edge.to]) {
|
|
1015
|
+
if (allKnownIds.has(id))
|
|
1016
|
+
continue;
|
|
1017
|
+
allKnownIds.add(id);
|
|
1018
|
+
ast.nodes.push({
|
|
1019
|
+
kind: "node",
|
|
1020
|
+
id,
|
|
1021
|
+
shape: "box",
|
|
1022
|
+
label: id,
|
|
1023
|
+
style: {},
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const entityKindById = new Map();
|
|
1028
|
+
ast.nodes.forEach((node) => entityKindById.set(node.id, "node"));
|
|
1029
|
+
ast.groups.forEach((group) => entityKindById.set(group.id, "group"));
|
|
1030
|
+
ast.tables.forEach((table) => entityKindById.set(table.id, "table"));
|
|
1031
|
+
ast.charts.forEach((chart) => entityKindById.set(chart.id, "chart"));
|
|
1032
|
+
ast.markdowns.forEach((md) => entityKindById.set(md.id, "markdown"));
|
|
1033
|
+
for (const group of ast.groups) {
|
|
1034
|
+
const itemIds = unresolvedGroupItems.get(group.id) ?? [];
|
|
1035
|
+
group.children = itemIds.map((itemId) => {
|
|
1036
|
+
if (itemId === group.id) {
|
|
1037
|
+
const tok = groupTokens.get(group.id) ?? cur();
|
|
1038
|
+
throw new ParseError(`Group "${group.id}" cannot include itself in items=[...]`, tok.line, tok.col);
|
|
1039
|
+
}
|
|
1040
|
+
const kind = entityKindById.get(itemId);
|
|
1041
|
+
if (!kind) {
|
|
1042
|
+
const tok = groupTokens.get(group.id) ?? cur();
|
|
1043
|
+
throw new ParseError(`Group "${group.id}" references unknown item "${itemId}" in items=[...]`, tok.line, tok.col);
|
|
1044
|
+
}
|
|
1045
|
+
return { kind, id: itemId };
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const parentByItemId = new Map();
|
|
1049
|
+
for (const group of ast.groups) {
|
|
1050
|
+
for (const child of group.children) {
|
|
1051
|
+
const existingParent = parentByItemId.get(child.id);
|
|
1052
|
+
if (existingParent) {
|
|
1053
|
+
const tok = groupTokens.get(group.id) ?? cur();
|
|
1054
|
+
throw new ParseError(`Item "${child.id}" cannot belong to both "${existingParent}" and "${group.id}"`, tok.line, tok.col);
|
|
1055
|
+
}
|
|
1056
|
+
parentByItemId.set(child.id, group.id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const groupsById = new Map(ast.groups.map((group) => [group.id, group]));
|
|
1060
|
+
const visiting = new Set();
|
|
1061
|
+
const visited = new Set();
|
|
1062
|
+
const stack = [];
|
|
1063
|
+
function visitGroup(groupId) {
|
|
1064
|
+
if (visiting.has(groupId)) {
|
|
1065
|
+
const start = stack.indexOf(groupId);
|
|
1066
|
+
const cycle = (start >= 0 ? stack.slice(start) : stack).concat(groupId);
|
|
1067
|
+
const tok = groupTokens.get(groupId) ?? cur();
|
|
1068
|
+
throw new ParseError(`Group cycle detected: ${cycle.join(" -> ")}`, tok.line, tok.col);
|
|
1069
|
+
}
|
|
1070
|
+
if (visited.has(groupId))
|
|
1071
|
+
return;
|
|
1072
|
+
visiting.add(groupId);
|
|
1073
|
+
stack.push(groupId);
|
|
1074
|
+
const group = groupsById.get(groupId);
|
|
1075
|
+
if (group) {
|
|
1076
|
+
for (const child of group.children) {
|
|
1077
|
+
if (child.kind === "group")
|
|
1078
|
+
visitGroup(child.id);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
stack.pop();
|
|
1082
|
+
visiting.delete(groupId);
|
|
1083
|
+
visited.add(groupId);
|
|
1084
|
+
}
|
|
1085
|
+
for (const group of ast.groups)
|
|
1086
|
+
visitGroup(group.id);
|
|
1234
1087
|
for (const node of ast.nodes) {
|
|
1235
1088
|
if (ast.styles[node.id]) {
|
|
1236
1089
|
node.style = { ...ast.styles[node.id], ...node.style };
|
|
@@ -3565,6 +3418,16 @@ function calcMarkdownWidth(lines, fontFamily = DEFAULT_FONT, pad = MARKDOWN.defa
|
|
|
3565
3418
|
// ============================================================
|
|
3566
3419
|
// ── Build scene graph from AST ────────────────────────────
|
|
3567
3420
|
function buildSceneGraph(ast) {
|
|
3421
|
+
const nodeParentById = new Map();
|
|
3422
|
+
const groupParentById = new Map();
|
|
3423
|
+
for (const g of ast.groups) {
|
|
3424
|
+
for (const child of g.children) {
|
|
3425
|
+
if (child.kind === "node")
|
|
3426
|
+
nodeParentById.set(child.id, g.id);
|
|
3427
|
+
if (child.kind === "group")
|
|
3428
|
+
groupParentById.set(child.id, g.id);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3568
3431
|
const nodes = ast.nodes.map((n) => {
|
|
3569
3432
|
const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
|
|
3570
3433
|
return {
|
|
@@ -3572,7 +3435,7 @@ function buildSceneGraph(ast) {
|
|
|
3572
3435
|
shape: n.shape,
|
|
3573
3436
|
label: n.label,
|
|
3574
3437
|
style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
|
|
3575
|
-
groupId: n.
|
|
3438
|
+
groupId: nodeParentById.get(n.id),
|
|
3576
3439
|
width: n.width,
|
|
3577
3440
|
height: n.height,
|
|
3578
3441
|
deg: n.deg,
|
|
@@ -3594,7 +3457,7 @@ function buildSceneGraph(ast) {
|
|
|
3594
3457
|
return {
|
|
3595
3458
|
id: g.id,
|
|
3596
3459
|
label: g.label,
|
|
3597
|
-
parentId:
|
|
3460
|
+
parentId: groupParentById.get(g.id),
|
|
3598
3461
|
children: g.children,
|
|
3599
3462
|
layout: (g.layout ?? "column"),
|
|
3600
3463
|
columns: g.columns ?? 1,
|
|
@@ -3642,28 +3505,21 @@ function buildSceneGraph(ast) {
|
|
|
3642
3505
|
h: c.height ?? CHART.defaultH,
|
|
3643
3506
|
};
|
|
3644
3507
|
});
|
|
3645
|
-
const markdowns = (ast.markdowns ?? []).map(m => {
|
|
3508
|
+
const markdowns = (ast.markdowns ?? []).map((m) => {
|
|
3646
3509
|
const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
|
|
3647
3510
|
return {
|
|
3648
3511
|
id: m.id,
|
|
3649
3512
|
content: m.content,
|
|
3650
3513
|
lines: parseMarkdownContent(m.content),
|
|
3651
|
-
style: { ...themeStyle, ...m.style },
|
|
3514
|
+
style: { ...ast.styles[m.id], ...themeStyle, ...m.style },
|
|
3652
3515
|
width: m.width,
|
|
3653
3516
|
height: m.height,
|
|
3654
|
-
x: 0,
|
|
3517
|
+
x: 0,
|
|
3518
|
+
y: 0,
|
|
3519
|
+
w: 0,
|
|
3520
|
+
h: 0,
|
|
3655
3521
|
};
|
|
3656
3522
|
});
|
|
3657
|
-
// Set parentId for nested groups
|
|
3658
|
-
for (const g of groups) {
|
|
3659
|
-
for (const child of g.children) {
|
|
3660
|
-
if (child.kind === "group") {
|
|
3661
|
-
const nested = groups.find((gg) => gg.id === child.id);
|
|
3662
|
-
if (nested)
|
|
3663
|
-
nested.parentId = g.id;
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
3523
|
const edges = ast.edges.map((e) => ({
|
|
3668
3524
|
id: e.id,
|
|
3669
3525
|
from: e.from,
|
|
@@ -4790,15 +4646,30 @@ function layout(sg) {
|
|
|
4790
4646
|
// 4. Build root order
|
|
4791
4647
|
// sg.rootOrder preserves DSL declaration order.
|
|
4792
4648
|
// Fall back: groups, then nodes, then tables.
|
|
4793
|
-
const
|
|
4794
|
-
|
|
4795
|
-
:
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4649
|
+
const defaultRootOrder = [
|
|
4650
|
+
...rootGroups.map((g) => ({ kind: "group", id: g.id })),
|
|
4651
|
+
...rootNodes.map((n) => ({ kind: "node", id: n.id })),
|
|
4652
|
+
...rootTables.map((t) => ({ kind: "table", id: t.id })),
|
|
4653
|
+
...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
|
|
4654
|
+
...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
|
|
4655
|
+
];
|
|
4656
|
+
const rootOrderSource = sg.rootOrder?.length ? sg.rootOrder : defaultRootOrder;
|
|
4657
|
+
const rootOrder = rootOrderSource.filter((ref) => {
|
|
4658
|
+
switch (ref.kind) {
|
|
4659
|
+
case "group":
|
|
4660
|
+
return !nestedGroupIds.has(ref.id);
|
|
4661
|
+
case "node":
|
|
4662
|
+
return !groupedNodeIds.has(ref.id);
|
|
4663
|
+
case "table":
|
|
4664
|
+
return !groupedTableIds.has(ref.id);
|
|
4665
|
+
case "chart":
|
|
4666
|
+
return !groupedChartIds.has(ref.id);
|
|
4667
|
+
case "markdown":
|
|
4668
|
+
return !groupedMarkdownIds.has(ref.id);
|
|
4669
|
+
default:
|
|
4670
|
+
return true;
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4802
4673
|
// 5. Root-level layout
|
|
4803
4674
|
// sg.layout:
|
|
4804
4675
|
// 'row' → items flow left to right (default)
|