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