sketchmark 1.0.2 → 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 +61 -36
- package/dist/animation/index.d.ts +9 -0
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/index.cjs +501 -474
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +501 -474
- 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/renderer/svg/index.d.ts.map +1 -1
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +501 -474
- 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)
|
|
@@ -7701,6 +7572,21 @@ function mkGroup(id, cls) {
|
|
|
7701
7572
|
g.setAttribute("class", cls);
|
|
7702
7573
|
return g;
|
|
7703
7574
|
}
|
|
7575
|
+
function buildParentGroupLookup(sg) {
|
|
7576
|
+
const parentGroups = new Map();
|
|
7577
|
+
for (const g of sg.groups) {
|
|
7578
|
+
if (g.parentId)
|
|
7579
|
+
parentGroups.set(`group:${g.id}`, g.parentId);
|
|
7580
|
+
for (const child of g.children) {
|
|
7581
|
+
parentGroups.set(`${child.kind}:${child.id}`, g.id);
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
return parentGroups;
|
|
7585
|
+
}
|
|
7586
|
+
function setParentGroupData(el, groupId) {
|
|
7587
|
+
if (groupId)
|
|
7588
|
+
el.dataset.parentGroup = groupId;
|
|
7589
|
+
}
|
|
7704
7590
|
// ── Node shapes ───────────────────────────────────────────────────────────
|
|
7705
7591
|
function renderShape$1(rc, n, palette) {
|
|
7706
7592
|
const s = n.style ?? {};
|
|
@@ -7797,6 +7683,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7797
7683
|
}
|
|
7798
7684
|
// ── Groups ───────────────────────────────────────────────
|
|
7799
7685
|
const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
|
|
7686
|
+
const parentGroups = buildParentGroupLookup(sg);
|
|
7800
7687
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gmMap) - groupDepth(b, gmMap));
|
|
7801
7688
|
const GL = mkGroup("grp-layer");
|
|
7802
7689
|
for (const g of sortedGroups) {
|
|
@@ -7804,6 +7691,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7804
7691
|
continue;
|
|
7805
7692
|
const gs = g.style ?? {};
|
|
7806
7693
|
const gg = mkGroup(`group-${g.id}`, "gg");
|
|
7694
|
+
setParentGroupData(gg, g.parentId);
|
|
7807
7695
|
if (gs.opacity != null)
|
|
7808
7696
|
gg.setAttribute("opacity", String(gs.opacity));
|
|
7809
7697
|
gg.appendChild(rc.rectangle(g.x, g.y, g.w, g.h, {
|
|
@@ -7908,6 +7796,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7908
7796
|
const idPrefix = shapeDef?.idPrefix ?? "node";
|
|
7909
7797
|
const cssClass = shapeDef?.cssClass ?? "ng";
|
|
7910
7798
|
const ng = mkGroup(`${idPrefix}-${n.id}`, cssClass);
|
|
7799
|
+
setParentGroupData(ng, n.groupId ?? parentGroups.get(`node:${n.id}`));
|
|
7911
7800
|
ng.dataset.nodeShape = n.shape;
|
|
7912
7801
|
ng.dataset.x = String(n.x);
|
|
7913
7802
|
ng.dataset.y = String(n.y);
|
|
@@ -7989,6 +7878,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
7989
7878
|
const TL = mkGroup("table-layer");
|
|
7990
7879
|
for (const t of sg.tables) {
|
|
7991
7880
|
const tg = mkGroup(`table-${t.id}`, "tg");
|
|
7881
|
+
setParentGroupData(tg, parentGroups.get(`table:${t.id}`));
|
|
7992
7882
|
const gs = t.style ?? {};
|
|
7993
7883
|
const fill = String(gs.fill ?? palette.tableFill);
|
|
7994
7884
|
const strk = String(gs.stroke ?? palette.tableStroke);
|
|
@@ -8089,6 +7979,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
8089
7979
|
const MDL = mkGroup('markdown-layer');
|
|
8090
7980
|
for (const m of sg.markdowns) {
|
|
8091
7981
|
const mg = mkGroup(`markdown-${m.id}`, 'mdg');
|
|
7982
|
+
setParentGroupData(mg, parentGroups.get(`markdown:${m.id}`));
|
|
8092
7983
|
const gs = m.style ?? {};
|
|
8093
7984
|
const mFont = resolveStyleFont(gs, diagramFont);
|
|
8094
7985
|
const baseColor = String(gs.color ?? palette.nodeText);
|
|
@@ -8152,7 +8043,9 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
8152
8043
|
// ── Charts ────────────────────────────────────────────────
|
|
8153
8044
|
const CL = mkGroup("chart-layer");
|
|
8154
8045
|
for (const c of sg.charts) {
|
|
8155
|
-
|
|
8046
|
+
const cg = renderRoughChartSVG(rc, c, palette, themeName !== "light");
|
|
8047
|
+
setParentGroupData(cg, parentGroups.get(`chart:${c.id}`));
|
|
8048
|
+
CL.appendChild(cg);
|
|
8156
8049
|
}
|
|
8157
8050
|
svg.appendChild(CL);
|
|
8158
8051
|
return svg;
|
|
@@ -8865,18 +8758,33 @@ const getTableEl = (svg, id) => getEl(svg, `table-${id}`);
|
|
|
8865
8758
|
const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
|
|
8866
8759
|
const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
|
|
8867
8760
|
const getMarkdownEl = (svg, id) => getEl(svg, `markdown-${id}`);
|
|
8761
|
+
const POSITIONABLE_SELECTOR = ".ng, .gg, .tg, .ntg, .cg, .mdg";
|
|
8762
|
+
function resolveNonEdgeDrawEl(svg, target) {
|
|
8763
|
+
return (getGroupEl(svg, target) ??
|
|
8764
|
+
getTableEl(svg, target) ??
|
|
8765
|
+
getNoteEl(svg, target) ??
|
|
8766
|
+
getChartEl(svg, target) ??
|
|
8767
|
+
getMarkdownEl(svg, target) ??
|
|
8768
|
+
getNodeEl(svg, target) ??
|
|
8769
|
+
null);
|
|
8770
|
+
}
|
|
8771
|
+
function hideDrawEl(el) {
|
|
8772
|
+
if (el.classList.contains("ng")) {
|
|
8773
|
+
el.classList.add("hidden");
|
|
8774
|
+
return;
|
|
8775
|
+
}
|
|
8776
|
+
el.classList.add("gg-hidden");
|
|
8777
|
+
}
|
|
8778
|
+
function showDrawEl(el) {
|
|
8779
|
+
el.classList.remove("hidden", "gg-hidden");
|
|
8780
|
+
}
|
|
8868
8781
|
function resolveEl(svg, target) {
|
|
8869
8782
|
// check edge first — target contains connector like "a-->b"
|
|
8870
8783
|
const edge = parseEdgeTarget(target);
|
|
8871
8784
|
if (edge)
|
|
8872
8785
|
return getEdgeEl(svg, edge.from, edge.to);
|
|
8873
8786
|
// everything else resolved by prefixed id
|
|
8874
|
-
return (
|
|
8875
|
-
getGroupEl(svg, target) ??
|
|
8876
|
-
getTableEl(svg, target) ??
|
|
8877
|
-
getNoteEl(svg, target) ??
|
|
8878
|
-
getChartEl(svg, target) ??
|
|
8879
|
-
getMarkdownEl(svg, target) ??
|
|
8787
|
+
return (resolveNonEdgeDrawEl(svg, target) ??
|
|
8880
8788
|
null);
|
|
8881
8789
|
}
|
|
8882
8790
|
function pathLength(p) {
|
|
@@ -9066,6 +8974,7 @@ function prepareNodeForDraw(el) {
|
|
|
9066
8974
|
el.appendChild(guide);
|
|
9067
8975
|
}
|
|
9068
8976
|
function revealNodeInstant(el) {
|
|
8977
|
+
showDrawEl(el);
|
|
9069
8978
|
clearNodeDrawStyles(el);
|
|
9070
8979
|
}
|
|
9071
8980
|
// ── Text writing reveal (clipPath) ───────────────────────
|
|
@@ -9114,6 +9023,7 @@ function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs)
|
|
|
9114
9023
|
}, delayMs);
|
|
9115
9024
|
}
|
|
9116
9025
|
function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
|
|
9026
|
+
showDrawEl(el);
|
|
9117
9027
|
const guide = nodeGuidePathEl(el);
|
|
9118
9028
|
if (!guide) {
|
|
9119
9029
|
const firstPath = el.querySelector("path");
|
|
@@ -9168,6 +9078,15 @@ function flattenSteps(items) {
|
|
|
9168
9078
|
}
|
|
9169
9079
|
return out;
|
|
9170
9080
|
}
|
|
9081
|
+
function forEachPlaybackStep(items, visit) {
|
|
9082
|
+
items.forEach((item, stepIndex) => {
|
|
9083
|
+
if (item.kind === "beat") {
|
|
9084
|
+
item.children.forEach((child) => visit(child, stepIndex));
|
|
9085
|
+
return;
|
|
9086
|
+
}
|
|
9087
|
+
visit(item, stepIndex);
|
|
9088
|
+
});
|
|
9089
|
+
}
|
|
9171
9090
|
// ── Draw target helpers ───────────────────────────────────
|
|
9172
9091
|
function getDrawTargetEdgeIds(steps) {
|
|
9173
9092
|
const ids = new Set();
|
|
@@ -9352,7 +9271,7 @@ class AnimationController {
|
|
|
9352
9271
|
for (const s of flattenSteps(steps)) {
|
|
9353
9272
|
if (s.action !== "draw" || parseEdgeTarget(s.target))
|
|
9354
9273
|
continue;
|
|
9355
|
-
if (svg.
|
|
9274
|
+
if (resolveNonEdgeDrawEl(svg, s.target)?.id === `group-${s.target}`) {
|
|
9356
9275
|
this.drawTargetGroups.add(`group-${s.target}`);
|
|
9357
9276
|
this.drawTargetNodes.delete(`node-${s.target}`);
|
|
9358
9277
|
}
|
|
@@ -9373,6 +9292,10 @@ class AnimationController {
|
|
|
9373
9292
|
this.drawTargetNodes.delete(`node-${s.target}`);
|
|
9374
9293
|
}
|
|
9375
9294
|
}
|
|
9295
|
+
this._drawStepIndexByElementId = this._buildDrawStepIndex();
|
|
9296
|
+
const { parentGroupByElementId, groupDescendantIds } = this._buildGroupVisibilityIndex();
|
|
9297
|
+
this._parentGroupByElementId = parentGroupByElementId;
|
|
9298
|
+
this._groupDescendantIds = groupDescendantIds;
|
|
9376
9299
|
this._clearAll();
|
|
9377
9300
|
// Init narration caption
|
|
9378
9301
|
if (this._container)
|
|
@@ -9391,6 +9314,104 @@ class AnimationController {
|
|
|
9391
9314
|
if (this._tts)
|
|
9392
9315
|
this._warmUpSpeech();
|
|
9393
9316
|
}
|
|
9317
|
+
_buildDrawStepIndex() {
|
|
9318
|
+
const drawStepIndexByElementId = new Map();
|
|
9319
|
+
forEachPlaybackStep(this.steps, (step, stepIndex) => {
|
|
9320
|
+
if (step.action !== "draw" || parseEdgeTarget(step.target))
|
|
9321
|
+
return;
|
|
9322
|
+
const el = resolveNonEdgeDrawEl(this.svg, step.target);
|
|
9323
|
+
if (el && !drawStepIndexByElementId.has(el.id)) {
|
|
9324
|
+
drawStepIndexByElementId.set(el.id, stepIndex);
|
|
9325
|
+
}
|
|
9326
|
+
});
|
|
9327
|
+
return drawStepIndexByElementId;
|
|
9328
|
+
}
|
|
9329
|
+
_buildGroupVisibilityIndex() {
|
|
9330
|
+
const parentGroupByElementId = new Map();
|
|
9331
|
+
const directChildIdsByGroup = new Map();
|
|
9332
|
+
this.svg.querySelectorAll(POSITIONABLE_SELECTOR).forEach((el) => {
|
|
9333
|
+
const parentGroupId = el.dataset.parentGroup;
|
|
9334
|
+
if (!parentGroupId)
|
|
9335
|
+
return;
|
|
9336
|
+
const parentGroupElId = `group-${parentGroupId}`;
|
|
9337
|
+
parentGroupByElementId.set(el.id, parentGroupElId);
|
|
9338
|
+
const children = directChildIdsByGroup.get(parentGroupElId) ?? new Set();
|
|
9339
|
+
children.add(el.id);
|
|
9340
|
+
directChildIdsByGroup.set(parentGroupElId, children);
|
|
9341
|
+
});
|
|
9342
|
+
const groupDescendantIds = new Map();
|
|
9343
|
+
const visit = (groupElId) => {
|
|
9344
|
+
if (groupDescendantIds.has(groupElId))
|
|
9345
|
+
return groupDescendantIds.get(groupElId);
|
|
9346
|
+
const descendants = new Set();
|
|
9347
|
+
const directChildren = directChildIdsByGroup.get(groupElId);
|
|
9348
|
+
if (directChildren) {
|
|
9349
|
+
for (const childId of directChildren) {
|
|
9350
|
+
descendants.add(childId);
|
|
9351
|
+
if (childId.startsWith("group-")) {
|
|
9352
|
+
visit(childId).forEach((nestedId) => descendants.add(nestedId));
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
groupDescendantIds.set(groupElId, descendants);
|
|
9357
|
+
return descendants;
|
|
9358
|
+
};
|
|
9359
|
+
this.svg.querySelectorAll(".gg").forEach((el) => {
|
|
9360
|
+
visit(el.id);
|
|
9361
|
+
});
|
|
9362
|
+
return { parentGroupByElementId, groupDescendantIds };
|
|
9363
|
+
}
|
|
9364
|
+
_hideGroupDescendants(groupElId) {
|
|
9365
|
+
const descendants = this._groupDescendantIds.get(groupElId);
|
|
9366
|
+
if (!descendants)
|
|
9367
|
+
return;
|
|
9368
|
+
for (const descendantId of descendants) {
|
|
9369
|
+
const el = getEl(this.svg, descendantId);
|
|
9370
|
+
if (el)
|
|
9371
|
+
hideDrawEl(el);
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
_isDeferredForGroupReveal(elementId, stepIndex, groupElId) {
|
|
9375
|
+
let currentId = elementId;
|
|
9376
|
+
while (currentId) {
|
|
9377
|
+
const firstDrawStep = this._drawStepIndexByElementId.get(currentId);
|
|
9378
|
+
if (firstDrawStep != null && firstDrawStep > stepIndex)
|
|
9379
|
+
return true;
|
|
9380
|
+
if (currentId === groupElId)
|
|
9381
|
+
break;
|
|
9382
|
+
currentId = this._parentGroupByElementId.get(currentId);
|
|
9383
|
+
}
|
|
9384
|
+
return false;
|
|
9385
|
+
}
|
|
9386
|
+
_revealGroupSubtree(groupElId, stepIndex) {
|
|
9387
|
+
const descendants = this._groupDescendantIds.get(groupElId);
|
|
9388
|
+
if (!descendants)
|
|
9389
|
+
return;
|
|
9390
|
+
for (const descendantId of descendants) {
|
|
9391
|
+
if (this._isDeferredForGroupReveal(descendantId, stepIndex, groupElId))
|
|
9392
|
+
continue;
|
|
9393
|
+
const el = getEl(this.svg, descendantId);
|
|
9394
|
+
if (el)
|
|
9395
|
+
showDrawEl(el);
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
_resolveCascadeTargets(target) {
|
|
9399
|
+
const edge = parseEdgeTarget(target);
|
|
9400
|
+
if (edge) {
|
|
9401
|
+
const el = getEdgeEl(this.svg, edge.from, edge.to);
|
|
9402
|
+
return el ? [el] : [];
|
|
9403
|
+
}
|
|
9404
|
+
const el = resolveEl(this.svg, target);
|
|
9405
|
+
if (!el)
|
|
9406
|
+
return [];
|
|
9407
|
+
if (!el.id.startsWith("group-"))
|
|
9408
|
+
return [el];
|
|
9409
|
+
const ids = new Set([el.id]);
|
|
9410
|
+
this._groupDescendantIds.get(el.id)?.forEach((id) => ids.add(id));
|
|
9411
|
+
return Array.from(ids)
|
|
9412
|
+
.map((id) => getEl(this.svg, id))
|
|
9413
|
+
.filter((candidate) => candidate != null);
|
|
9414
|
+
}
|
|
9394
9415
|
/** The narration caption element — mount it anywhere via `yourContainer.appendChild(anim.captionElement)` */
|
|
9395
9416
|
get captionElement() {
|
|
9396
9417
|
return this._captionEl;
|
|
@@ -9660,6 +9681,9 @@ class AnimationController {
|
|
|
9660
9681
|
el.style.opacity = "";
|
|
9661
9682
|
el.classList.remove("hl", "faded");
|
|
9662
9683
|
});
|
|
9684
|
+
for (const groupElId of this.drawTargetGroups) {
|
|
9685
|
+
this._hideGroupDescendants(groupElId);
|
|
9686
|
+
}
|
|
9663
9687
|
// Clear narration caption
|
|
9664
9688
|
if (this._captionEl) {
|
|
9665
9689
|
this._captionEl.style.opacity = "0";
|
|
@@ -9722,7 +9746,7 @@ class AnimationController {
|
|
|
9722
9746
|
this._doDraw(s, silent);
|
|
9723
9747
|
break;
|
|
9724
9748
|
case "erase":
|
|
9725
|
-
this._doErase(s.target, s.duration);
|
|
9749
|
+
this._doErase(s.target, silent, s.duration);
|
|
9726
9750
|
break;
|
|
9727
9751
|
case "show":
|
|
9728
9752
|
this._doShowHide(s.target, true, silent, s.duration);
|
|
@@ -9778,7 +9802,9 @@ class AnimationController {
|
|
|
9778
9802
|
}
|
|
9779
9803
|
// ── fade / unfade ─────────────────────────────────────────
|
|
9780
9804
|
_doFade(target, doFade) {
|
|
9781
|
-
|
|
9805
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
9806
|
+
el.classList.toggle("faded", doFade);
|
|
9807
|
+
}
|
|
9782
9808
|
}
|
|
9783
9809
|
_writeTransform(el, target, silent, duration = 420) {
|
|
9784
9810
|
const t = this._transforms.get(target) ?? {
|
|
@@ -9877,11 +9903,12 @@ class AnimationController {
|
|
|
9877
9903
|
// Check if target is a group (has #group-{target} element)
|
|
9878
9904
|
const groupEl = getGroupEl(this.svg, target);
|
|
9879
9905
|
if (groupEl) {
|
|
9906
|
+
showDrawEl(groupEl);
|
|
9907
|
+
this._revealGroupSubtree(groupEl.id, this._step);
|
|
9880
9908
|
// ── Group draw ──────────────────────────────────────
|
|
9881
9909
|
if (silent) {
|
|
9882
9910
|
clearDrawStyles(groupEl);
|
|
9883
9911
|
groupEl.style.transition = "none";
|
|
9884
|
-
groupEl.classList.remove("gg-hidden");
|
|
9885
9912
|
groupEl.style.opacity = "1";
|
|
9886
9913
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
9887
9914
|
groupEl.style.transition = "";
|
|
@@ -9889,7 +9916,6 @@ class AnimationController {
|
|
|
9889
9916
|
}));
|
|
9890
9917
|
}
|
|
9891
9918
|
else {
|
|
9892
|
-
groupEl.classList.remove("gg-hidden");
|
|
9893
9919
|
// Groups use slightly longer stroke-draw (bigger box, dashed border = more paths)
|
|
9894
9920
|
const firstPath = groupEl.querySelector("path");
|
|
9895
9921
|
if (!firstPath?.style.strokeDasharray)
|
|
@@ -10000,6 +10026,7 @@ class AnimationController {
|
|
|
10000
10026
|
const nodeEl = getNodeEl(this.svg, target);
|
|
10001
10027
|
if (!nodeEl)
|
|
10002
10028
|
return;
|
|
10029
|
+
showDrawEl(nodeEl);
|
|
10003
10030
|
if (silent) {
|
|
10004
10031
|
revealNodeInstant(nodeEl);
|
|
10005
10032
|
}
|
|
@@ -10011,20 +10038,20 @@ class AnimationController {
|
|
|
10011
10038
|
}
|
|
10012
10039
|
}
|
|
10013
10040
|
// ── erase ─────────────────────────────────────────────────
|
|
10014
|
-
_doErase(target, duration = 400) {
|
|
10015
|
-
const el
|
|
10016
|
-
|
|
10017
|
-
el.style.transition = `opacity ${duration}ms`;
|
|
10041
|
+
_doErase(target, silent, duration = 400) {
|
|
10042
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
10043
|
+
el.style.transition = silent ? "none" : `opacity ${duration}ms`;
|
|
10018
10044
|
el.style.opacity = "0";
|
|
10019
10045
|
}
|
|
10020
10046
|
}
|
|
10021
10047
|
// ── show / hide ───────────────────────────────────────────
|
|
10022
10048
|
_doShowHide(target, show, silent, duration = 400) {
|
|
10023
|
-
const el
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10049
|
+
for (const el of this._resolveCascadeTargets(target)) {
|
|
10050
|
+
if (show)
|
|
10051
|
+
showDrawEl(el);
|
|
10052
|
+
el.style.transition = silent ? "none" : `opacity ${duration}ms`;
|
|
10053
|
+
el.style.opacity = show ? "1" : "0";
|
|
10054
|
+
}
|
|
10028
10055
|
}
|
|
10029
10056
|
// ── pulse ─────────────────────────────────────────────────
|
|
10030
10057
|
_doPulse(target, duration = 500) {
|