@diagrammo/dgmo 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1379,6 +1379,94 @@ var init_parsing = __esm({
1379
1379
  }
1380
1380
  });
1381
1381
 
1382
+ // src/utils/tag-groups.ts
1383
+ function isTagBlockHeading(trimmed) {
1384
+ return TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
1385
+ }
1386
+ function resolveTagColor(metadata, tagGroups, activeGroupName, isContainer) {
1387
+ if (!activeGroupName) return void 0;
1388
+ const group = tagGroups.find(
1389
+ (g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
1390
+ );
1391
+ if (!group) return void 0;
1392
+ const metaValue = metadata[group.name.toLowerCase()] ?? (isContainer ? void 0 : group.defaultValue);
1393
+ if (!metaValue) return "#999999";
1394
+ return group.entries.find(
1395
+ (e) => e.value.toLowerCase() === metaValue.toLowerCase()
1396
+ )?.color ?? "#999999";
1397
+ }
1398
+ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1399
+ if (tagGroups.length === 0) return;
1400
+ const groupMap = /* @__PURE__ */ new Map();
1401
+ for (const g of tagGroups) groupMap.set(g.name.toLowerCase(), g);
1402
+ for (const entity of entities) {
1403
+ for (const [key, value] of Object.entries(entity.metadata)) {
1404
+ const group = groupMap.get(key);
1405
+ if (!group) continue;
1406
+ const match = group.entries.some(
1407
+ (e) => e.value.toLowerCase() === value.toLowerCase()
1408
+ );
1409
+ if (!match) {
1410
+ const defined = group.entries.map((e) => e.value);
1411
+ let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1412
+ const hint = suggestFn?.(value, defined);
1413
+ if (hint) {
1414
+ msg += `. ${hint}`;
1415
+ } else {
1416
+ msg += ` \u2014 defined values: ${defined.join(", ")}`;
1417
+ }
1418
+ pushWarning(entity.lineNumber, msg);
1419
+ }
1420
+ }
1421
+ }
1422
+ }
1423
+ function injectDefaultTagMetadata(entities, tagGroups, skip) {
1424
+ const defaults = [];
1425
+ for (const group of tagGroups) {
1426
+ if (group.defaultValue) {
1427
+ defaults.push({ key: group.name.toLowerCase(), value: group.defaultValue });
1428
+ }
1429
+ }
1430
+ if (defaults.length === 0) return;
1431
+ for (const entity of entities) {
1432
+ if (skip?.(entity)) continue;
1433
+ for (const { key, value } of defaults) {
1434
+ if (!(key in entity.metadata)) {
1435
+ entity.metadata[key] = value;
1436
+ }
1437
+ }
1438
+ }
1439
+ }
1440
+ function matchTagBlockHeading(trimmed) {
1441
+ const tagMatch = trimmed.match(TAG_BLOCK_RE);
1442
+ if (tagMatch) {
1443
+ return {
1444
+ name: tagMatch[1].trim(),
1445
+ alias: tagMatch[2] || void 0,
1446
+ colorHint: tagMatch[3] || void 0,
1447
+ deprecated: false
1448
+ };
1449
+ }
1450
+ const groupMatch = trimmed.match(GROUP_HEADING_RE);
1451
+ if (groupMatch) {
1452
+ return {
1453
+ name: groupMatch[1].trim(),
1454
+ alias: groupMatch[2] || void 0,
1455
+ colorHint: groupMatch[3] || void 0,
1456
+ deprecated: true
1457
+ };
1458
+ }
1459
+ return null;
1460
+ }
1461
+ var TAG_BLOCK_RE, GROUP_HEADING_RE;
1462
+ var init_tag_groups = __esm({
1463
+ "src/utils/tag-groups.ts"() {
1464
+ "use strict";
1465
+ TAG_BLOCK_RE = /^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
1466
+ GROUP_HEADING_RE = /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
1467
+ }
1468
+ });
1469
+
1382
1470
  // src/sequence/participant-inference.ts
1383
1471
  function inferParticipantType(name) {
1384
1472
  for (const rule of PARTICIPANT_RULES) {
@@ -1708,94 +1796,6 @@ var init_arrows = __esm({
1708
1796
  }
1709
1797
  });
1710
1798
 
1711
- // src/utils/tag-groups.ts
1712
- function isTagBlockHeading(trimmed) {
1713
- return TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
1714
- }
1715
- function resolveTagColor(metadata, tagGroups, activeGroupName, isContainer) {
1716
- if (!activeGroupName) return void 0;
1717
- const group = tagGroups.find(
1718
- (g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
1719
- );
1720
- if (!group) return void 0;
1721
- const metaValue = metadata[group.name.toLowerCase()] ?? (isContainer ? void 0 : group.defaultValue);
1722
- if (!metaValue) return "#999999";
1723
- return group.entries.find(
1724
- (e) => e.value.toLowerCase() === metaValue.toLowerCase()
1725
- )?.color ?? "#999999";
1726
- }
1727
- function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1728
- if (tagGroups.length === 0) return;
1729
- const groupMap = /* @__PURE__ */ new Map();
1730
- for (const g of tagGroups) groupMap.set(g.name.toLowerCase(), g);
1731
- for (const entity of entities) {
1732
- for (const [key, value] of Object.entries(entity.metadata)) {
1733
- const group = groupMap.get(key);
1734
- if (!group) continue;
1735
- const match = group.entries.some(
1736
- (e) => e.value.toLowerCase() === value.toLowerCase()
1737
- );
1738
- if (!match) {
1739
- const defined = group.entries.map((e) => e.value);
1740
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1741
- const hint = suggestFn?.(value, defined);
1742
- if (hint) {
1743
- msg += `. ${hint}`;
1744
- } else {
1745
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1746
- }
1747
- pushWarning(entity.lineNumber, msg);
1748
- }
1749
- }
1750
- }
1751
- }
1752
- function injectDefaultTagMetadata(entities, tagGroups, skip) {
1753
- const defaults = [];
1754
- for (const group of tagGroups) {
1755
- if (group.defaultValue) {
1756
- defaults.push({ key: group.name.toLowerCase(), value: group.defaultValue });
1757
- }
1758
- }
1759
- if (defaults.length === 0) return;
1760
- for (const entity of entities) {
1761
- if (skip?.(entity)) continue;
1762
- for (const { key, value } of defaults) {
1763
- if (!(key in entity.metadata)) {
1764
- entity.metadata[key] = value;
1765
- }
1766
- }
1767
- }
1768
- }
1769
- function matchTagBlockHeading(trimmed) {
1770
- const tagMatch = trimmed.match(TAG_BLOCK_RE);
1771
- if (tagMatch) {
1772
- return {
1773
- name: tagMatch[1].trim(),
1774
- alias: tagMatch[2] || void 0,
1775
- colorHint: tagMatch[3] || void 0,
1776
- deprecated: false
1777
- };
1778
- }
1779
- const groupMatch = trimmed.match(GROUP_HEADING_RE);
1780
- if (groupMatch) {
1781
- return {
1782
- name: groupMatch[1].trim(),
1783
- alias: groupMatch[2] || void 0,
1784
- colorHint: groupMatch[3] || void 0,
1785
- deprecated: true
1786
- };
1787
- }
1788
- return null;
1789
- }
1790
- var TAG_BLOCK_RE, GROUP_HEADING_RE;
1791
- var init_tag_groups = __esm({
1792
- "src/utils/tag-groups.ts"() {
1793
- "use strict";
1794
- TAG_BLOCK_RE = /^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
1795
- GROUP_HEADING_RE = /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
1796
- }
1797
- });
1798
-
1799
1799
  // src/sequence/parser.ts
1800
1800
  var parser_exports = {};
1801
1801
  __export(parser_exports, {
@@ -3383,6 +3383,7 @@ function parseERDiagram(content, palette) {
3383
3383
  options: {},
3384
3384
  tables: [],
3385
3385
  relationships: [],
3386
+ tagGroups: [],
3386
3387
  diagnostics: [],
3387
3388
  error: null
3388
3389
  };
@@ -3400,6 +3401,8 @@ function parseERDiagram(content, palette) {
3400
3401
  const tableMap = /* @__PURE__ */ new Map();
3401
3402
  let currentTable = null;
3402
3403
  let contentStarted = false;
3404
+ let currentTagGroup = null;
3405
+ const aliasMap = /* @__PURE__ */ new Map();
3403
3406
  function getOrCreateTable(name, lineNumber) {
3404
3407
  const id = tableId(name);
3405
3408
  const existing = tableMap.get(id);
@@ -3408,6 +3411,7 @@ function parseERDiagram(content, palette) {
3408
3411
  id,
3409
3412
  name,
3410
3413
  columns: [],
3414
+ metadata: {},
3411
3415
  lineNumber
3412
3416
  };
3413
3417
  tableMap.set(id, table);
@@ -3424,6 +3428,50 @@ function parseERDiagram(content, palette) {
3424
3428
  continue;
3425
3429
  }
3426
3430
  if (trimmed.startsWith("//")) continue;
3431
+ if (!contentStarted && indent === 0) {
3432
+ const tagBlockMatch = matchTagBlockHeading(trimmed);
3433
+ if (tagBlockMatch) {
3434
+ if (tagBlockMatch.deprecated) {
3435
+ result.diagnostics.push(makeDgmoError(
3436
+ lineNumber,
3437
+ `'## ${tagBlockMatch.name}' is deprecated for tag groups \u2014 use 'tag: ${tagBlockMatch.name}' instead`,
3438
+ "warning"
3439
+ ));
3440
+ }
3441
+ currentTagGroup = {
3442
+ name: tagBlockMatch.name,
3443
+ alias: tagBlockMatch.alias,
3444
+ entries: [],
3445
+ lineNumber
3446
+ };
3447
+ if (tagBlockMatch.alias) {
3448
+ aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
3449
+ }
3450
+ result.tagGroups.push(currentTagGroup);
3451
+ continue;
3452
+ }
3453
+ }
3454
+ if (currentTagGroup && !contentStarted && indent > 0) {
3455
+ const isDefault = /\bdefault\s*$/.test(trimmed);
3456
+ const entryText = isDefault ? trimmed.replace(/\s+default\s*$/, "").trim() : trimmed;
3457
+ const { label, color } = extractColor(entryText, palette);
3458
+ if (!color) {
3459
+ result.diagnostics.push(makeDgmoError(
3460
+ lineNumber,
3461
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
3462
+ "warning"
3463
+ ));
3464
+ continue;
3465
+ }
3466
+ if (isDefault) {
3467
+ currentTagGroup.defaultValue = label;
3468
+ }
3469
+ currentTagGroup.entries.push({ value: label, color, lineNumber });
3470
+ continue;
3471
+ }
3472
+ if (currentTagGroup && indent === 0) {
3473
+ currentTagGroup = null;
3474
+ }
3427
3475
  if (!contentStarted && indent === 0 && /^[a-z][a-z0-9-]*\s*:/i.test(trimmed)) {
3428
3476
  const colonIdx = trimmed.indexOf(":");
3429
3477
  const key = trimmed.substring(0, colonIdx).trim().toLowerCase();
@@ -3505,6 +3553,11 @@ function parseERDiagram(content, palette) {
3505
3553
  const table = getOrCreateTable(name, lineNumber);
3506
3554
  if (color) table.color = color;
3507
3555
  table.lineNumber = lineNumber;
3556
+ const pipeStr = tableDecl[3]?.trim();
3557
+ if (pipeStr) {
3558
+ const meta = parsePipeMetadata(["", pipeStr], aliasMap);
3559
+ Object.assign(table.metadata, meta);
3560
+ }
3508
3561
  currentTable = table;
3509
3562
  continue;
3510
3563
  }
@@ -3514,6 +3567,27 @@ function parseERDiagram(content, palette) {
3514
3567
  result.diagnostics.push(diag);
3515
3568
  result.error = formatDgmoError(diag);
3516
3569
  }
3570
+ if (result.tagGroups.length > 0) {
3571
+ const tagEntities = result.tables.map((t) => ({
3572
+ metadata: t.metadata,
3573
+ lineNumber: t.lineNumber
3574
+ }));
3575
+ validateTagValues(
3576
+ tagEntities,
3577
+ result.tagGroups,
3578
+ (line10, msg) => result.diagnostics.push(makeDgmoError(line10, msg, "warning")),
3579
+ suggest
3580
+ );
3581
+ for (const group of result.tagGroups) {
3582
+ if (!group.defaultValue) continue;
3583
+ const key = group.name.toLowerCase();
3584
+ for (const table of result.tables) {
3585
+ if (!table.metadata[key]) {
3586
+ table.metadata[key] = group.defaultValue;
3587
+ }
3588
+ }
3589
+ }
3590
+ }
3517
3591
  if (result.tables.length >= 2 && result.relationships.length >= 1 && !result.error) {
3518
3592
  const connectedIds = /* @__PURE__ */ new Set();
3519
3593
  for (const rel of result.relationships) {
@@ -3565,7 +3639,8 @@ var init_parser3 = __esm({
3565
3639
  init_colors();
3566
3640
  init_diagnostics();
3567
3641
  init_parsing();
3568
- TABLE_DECL_RE = /^([a-zA-Z_]\w*)(?:\s+\(([^)]+)\))?\s*$/;
3642
+ init_tag_groups();
3643
+ TABLE_DECL_RE = /^([a-zA-Z_]\w*)(?:\s*\(([^)]+)\))?(?:\s*\|(.+))?$/;
3569
3644
  COLUMN_RE = /^(\w+)(?:\s*:\s*(\w[\w()]*(?:\s*\[\])?))?(?:\s+\[([^\]]+)\])?\s*$/;
3570
3645
  INDENT_REL_RE = /^([1*?])-(?:(.+)-)?([1*?])\s+([a-zA-Z_]\w*)\s*$/;
3571
3646
  CONSTRAINT_MAP = {
@@ -3603,7 +3678,10 @@ function parseChart(content, palette) {
3603
3678
  const trimmed = lines[i].trim();
3604
3679
  const lineNumber = i + 1;
3605
3680
  if (!trimmed) continue;
3606
- if (/^#{2,}\s+/.test(trimmed)) continue;
3681
+ if (/^#{2,}\s+/.test(trimmed)) {
3682
+ result.diagnostics.push(makeDgmoError(lineNumber, `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`));
3683
+ continue;
3684
+ }
3607
3685
  if (trimmed.startsWith("//")) continue;
3608
3686
  const colonIndex = trimmed.indexOf(":");
3609
3687
  if (colonIndex === -1) continue;
@@ -3750,9 +3828,16 @@ function parseEChart(content, palette) {
3750
3828
  const trimmed = lines[i].trim();
3751
3829
  const lineNumber = i + 1;
3752
3830
  if (!trimmed) continue;
3753
- const mdCategoryMatch = trimmed.match(/^#{2,}\s+(.+)$/);
3754
- if (mdCategoryMatch) {
3755
- const { label: catName, color: catColor } = extractColor(mdCategoryMatch[1].trim(), palette);
3831
+ if (/^#{2,}\s+/.test(trimmed)) {
3832
+ const name = trimmed.replace(/^#{2,}\s+/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
3833
+ result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`));
3834
+ continue;
3835
+ }
3836
+ if (trimmed.startsWith("//")) continue;
3837
+ const categoryMatch = trimmed.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
3838
+ if (categoryMatch) {
3839
+ const catName = categoryMatch[1].trim();
3840
+ const catColor = categoryMatch[2] ? resolveColor(categoryMatch[2].trim(), palette) : null;
3756
3841
  if (catColor) {
3757
3842
  if (!result.categoryColors) result.categoryColors = {};
3758
3843
  result.categoryColors[catName] = catColor;
@@ -3760,12 +3845,6 @@ function parseEChart(content, palette) {
3760
3845
  currentCategory = catName;
3761
3846
  continue;
3762
3847
  }
3763
- if (trimmed.startsWith("//")) continue;
3764
- const categoryMatch = trimmed.match(/^\[(.+)\]$/);
3765
- if (categoryMatch) {
3766
- currentCategory = categoryMatch[1].trim();
3767
- continue;
3768
- }
3769
3848
  const colonIndex = trimmed.indexOf(":");
3770
3849
  if (result.type === "sankey" && colonIndex === -1) {
3771
3850
  const indent = measureIndent(lines[i]);
@@ -5397,6 +5476,7 @@ function parseKanban(content, palette) {
5397
5476
  let currentTagGroup = null;
5398
5477
  let currentColumn = null;
5399
5478
  let currentCard = null;
5479
+ let cardBaseIndent = 0;
5400
5480
  let columnCounter = 0;
5401
5481
  let cardCounter = 0;
5402
5482
  const aliasMap = /* @__PURE__ */ new Map();
@@ -5496,7 +5576,14 @@ function parseKanban(content, palette) {
5496
5576
  }
5497
5577
  currentTagGroup = null;
5498
5578
  }
5499
- const columnMatch = trimmed.match(COLUMN_RE2);
5579
+ const indent = measureIndent(line10);
5580
+ if (LEGACY_COLUMN_RE.test(trimmed)) {
5581
+ const legacyMatch = trimmed.match(LEGACY_COLUMN_RE);
5582
+ const name = legacyMatch[1].replace(/\s*\(.*\)\s*$/, "").trim();
5583
+ warn(lineNumber, `'== ${name} ==' is no longer supported. Use '[${name}]' instead`);
5584
+ continue;
5585
+ }
5586
+ const columnMatch = indent === 0 ? trimmed.match(COLUMN_RE2) : null;
5500
5587
  if (columnMatch) {
5501
5588
  contentStarted = true;
5502
5589
  currentTagGroup = null;
@@ -5508,16 +5595,20 @@ function parseKanban(content, palette) {
5508
5595
  }
5509
5596
  currentCard = null;
5510
5597
  columnCounter++;
5511
- const rawColName = columnMatch[1].trim();
5512
- const wipStr = columnMatch[2];
5513
- const { label: colName, color: colColor } = extractColor(
5514
- rawColName,
5515
- palette
5516
- );
5598
+ const colName = columnMatch[1].trim();
5599
+ const colColor = columnMatch[2] ? resolveColor(columnMatch[2].trim(), palette) : void 0;
5600
+ let wipLimit;
5601
+ const pipeStr = columnMatch[3];
5602
+ if (pipeStr) {
5603
+ const wipMatch = pipeStr.match(/\bwip\s*:\s*(\d+)\b/i);
5604
+ if (wipMatch) {
5605
+ wipLimit = parseInt(wipMatch[1], 10);
5606
+ }
5607
+ }
5517
5608
  currentColumn = {
5518
5609
  id: `col-${columnCounter}`,
5519
5610
  name: colName,
5520
- wipLimit: wipStr ? parseInt(wipStr, 10) : void 0,
5611
+ wipLimit,
5521
5612
  color: colColor,
5522
5613
  cards: [],
5523
5614
  lineNumber
@@ -5532,24 +5623,25 @@ function parseKanban(content, palette) {
5532
5623
  warn(lineNumber, "Card line found before any column");
5533
5624
  continue;
5534
5625
  }
5535
- const indent = measureIndent(line10);
5536
- if (indent > 0 && currentCard) {
5626
+ if (currentCard && indent > cardBaseIndent) {
5537
5627
  currentCard.details.push(trimmed);
5538
5628
  currentCard.endLineNumber = lineNumber;
5539
5629
  continue;
5540
5630
  }
5541
- if (currentCard) {
5631
+ if (indent > 0) {
5632
+ cardCounter++;
5633
+ const card = parseCardLine(
5634
+ trimmed,
5635
+ lineNumber,
5636
+ cardCounter,
5637
+ aliasMap,
5638
+ palette
5639
+ );
5640
+ cardBaseIndent = indent;
5641
+ currentCard = card;
5642
+ currentColumn.cards.push(card);
5643
+ continue;
5542
5644
  }
5543
- cardCounter++;
5544
- const card = parseCardLine(
5545
- trimmed,
5546
- lineNumber,
5547
- cardCounter,
5548
- aliasMap,
5549
- palette
5550
- );
5551
- currentCard = card;
5552
- currentColumn.cards.push(card);
5553
5645
  }
5554
5646
  if (currentCard) {
5555
5647
  }
@@ -5583,7 +5675,7 @@ function parseKanban(content, palette) {
5583
5675
  }
5584
5676
  }
5585
5677
  if (result.columns.length === 0 && !result.error) {
5586
- return fail(1, "No columns found. Use == Column Name == to define columns");
5678
+ return fail(1, "No columns found. Use [Column Name] to define columns");
5587
5679
  }
5588
5680
  return result;
5589
5681
  }
@@ -5620,14 +5712,16 @@ function parseCardLine(trimmed, lineNumber, counter, aliasMap, palette) {
5620
5712
  color
5621
5713
  };
5622
5714
  }
5623
- var COLUMN_RE2;
5715
+ var COLUMN_RE2, LEGACY_COLUMN_RE;
5624
5716
  var init_parser5 = __esm({
5625
5717
  "src/kanban/parser.ts"() {
5626
5718
  "use strict";
5627
5719
  init_diagnostics();
5720
+ init_colors();
5628
5721
  init_tag_groups();
5629
5722
  init_parsing();
5630
- COLUMN_RE2 = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
5723
+ COLUMN_RE2 = /^\[(.+?)\](?:\s*\(([^)]+)\))?\s*(?:\|\s*(.+))?$/;
5724
+ LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
5631
5725
  }
5632
5726
  });
5633
5727
 
@@ -7212,7 +7306,7 @@ var init_parser9 = __esm({
7212
7306
  TAG_GROUP_RE = /^tag\s*:\s*(\w[\w\s]*?)(?:\s+alias\s+(\w+))?\s*$/;
7213
7307
  TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?(\s+default)?\s*$/;
7214
7308
  COMPONENT_RE = /^([a-zA-Z_][\w]*)(.*)$/;
7215
- PIPE_META_RE = /\|\s*(\w+)\s*:\s*([^|]+)/g;
7309
+ PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
7216
7310
  PROPERTY_RE = /^([\w-]+)\s*:\s*(.+)$/;
7217
7311
  PERCENT_RE = /^([\d.]+)%$/;
7218
7312
  RANGE_RE = /^(\d+)-(\d+)$/;
@@ -9578,7 +9672,7 @@ function computeCardArchive(content, parsed, cardId) {
9578
9672
  const trimmedEnd = withoutCard.length > 0 && withoutCard[withoutCard.length - 1].trim() === "" ? withoutCard : [...withoutCard, ""];
9579
9673
  return [
9580
9674
  ...trimmedEnd,
9581
- "== Archive ==",
9675
+ "[Archive]",
9582
9676
  ...cardLines
9583
9677
  ].join("\n");
9584
9678
  }
@@ -10457,7 +10551,7 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10457
10551
  g.append("line").attr("x1", bx + px * spread).attr("y1", by + py * spread).attr("x2", bx - px * spread).attr("y2", by - py * spread).attr("stroke", color).attr("stroke-width", sw);
10458
10552
  }
10459
10553
  }
10460
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims) {
10554
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10461
10555
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10462
10556
  const width = exportDims?.width ?? container.clientWidth;
10463
10557
  const height = exportDims?.height ?? container.clientHeight;
@@ -10527,8 +10621,16 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10527
10621
  }
10528
10622
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10529
10623
  const node = layout.nodes[ni];
10530
- const nodeColor2 = node.color ?? seriesColors2[ni % seriesColors2.length];
10624
+ const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10625
+ const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
10531
10626
  const nodeG = contentG.append("g").attr("transform", `translate(${node.x}, ${node.y})`).attr("class", "er-table").attr("data-line-number", String(node.lineNumber)).attr("data-node-id", node.id);
10627
+ if (activeTagGroup) {
10628
+ const tagKey = activeTagGroup.toLowerCase();
10629
+ const tagValue = node.metadata[tagKey];
10630
+ if (tagValue) {
10631
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10632
+ }
10633
+ }
10532
10634
  if (onClickItem) {
10533
10635
  nodeG.style("cursor", "pointer").on("click", () => {
10534
10636
  onClickItem(node.lineNumber);
@@ -10560,6 +10662,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10560
10662
  }
10561
10663
  }
10562
10664
  }
10665
+ if (parsed.tagGroups.length > 0) {
10666
+ const LEGEND_Y_PAD = 16;
10667
+ const LEGEND_PILL_H = 22;
10668
+ const LEGEND_PILL_RX = 11;
10669
+ const LEGEND_PILL_PAD9 = 10;
10670
+ const LEGEND_GAP2 = 8;
10671
+ const LEGEND_FONT_SIZE2 = 11;
10672
+ const LEGEND_GROUP_GAP7 = 16;
10673
+ const legendG = svg.append("g").attr("class", "er-tag-legend");
10674
+ let legendX = DIAGRAM_PADDING5;
10675
+ let legendY = height - DIAGRAM_PADDING5;
10676
+ for (const group of parsed.tagGroups) {
10677
+ const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10678
+ const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
10679
+ const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
10680
+ legendX += labelWidth;
10681
+ for (const entry of group.entries) {
10682
+ const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
10683
+ const tmpText = legendG.append("text").attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
10684
+ const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
10685
+ tmpText.remove();
10686
+ const pillW = textW + LEGEND_PILL_PAD9 * 2;
10687
+ pillG.append("rect").attr("x", legendX).attr("y", legendY).attr("width", pillW).attr("height", LEGEND_PILL_H).attr("rx", LEGEND_PILL_RX).attr("ry", LEGEND_PILL_RX).attr("fill", mix(entry.color, isDark ? palette.surface : palette.bg, 25)).attr("stroke", entry.color).attr("stroke-width", 1);
10688
+ pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
10689
+ legendX += pillW + LEGEND_GAP2;
10690
+ }
10691
+ legendX += LEGEND_GROUP_GAP7;
10692
+ }
10693
+ }
10563
10694
  }
10564
10695
  function renderERDiagramForExport(content, theme, palette) {
10565
10696
  const parsed = parseERDiagram(content, palette);
@@ -10605,6 +10736,7 @@ var init_renderer5 = __esm({
10605
10736
  init_fonts();
10606
10737
  init_color_utils();
10607
10738
  init_palettes();
10739
+ init_tag_groups();
10608
10740
  init_parser3();
10609
10741
  init_layout4();
10610
10742
  DIAGRAM_PADDING5 = 20;
@@ -15036,7 +15168,7 @@ function formatUptime(fraction) {
15036
15168
  if (pct >= 99) return `${pct.toFixed(1)}%`;
15037
15169
  return `${pct.toFixed(1)}%`;
15038
15170
  }
15039
- function layoutInfra(computed, selectedNodeId) {
15171
+ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15040
15172
  if (computed.nodes.length === 0) {
15041
15173
  return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15042
15174
  }
@@ -15057,9 +15189,10 @@ function layoutInfra(computed, selectedNodeId) {
15057
15189
  const widthMap = /* @__PURE__ */ new Map();
15058
15190
  const heightMap = /* @__PURE__ */ new Map();
15059
15191
  for (const node of computed.nodes) {
15060
- const expanded = node.id === selectedNodeId;
15192
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15193
+ const expanded = !isNodeCollapsed && node.id === selectedNodeId;
15061
15194
  const width = computeNodeWidth2(node, expanded, computed.options);
15062
- const height = computeNodeHeight2(node, expanded, computed.options);
15195
+ const height = isNodeCollapsed ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM : computeNodeHeight2(node, expanded, computed.options);
15063
15196
  widthMap.set(node.id, width);
15064
15197
  heightMap.set(node.id, height);
15065
15198
  const inGroup = groupedNodeIds.has(node.id);
@@ -15632,10 +15765,27 @@ function renderEdgeLabels(svg, edges, palette, isDark, animate) {
15632
15765
  }
15633
15766
  }
15634
15767
  }
15635
- function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions) {
15768
+ function resolveActiveTagStroke(node, activeGroup, tagGroups, palette) {
15769
+ const tg = tagGroups.find((t) => t.name.toLowerCase() === activeGroup.toLowerCase());
15770
+ if (!tg) return null;
15771
+ const tagKey = (tg.alias ?? tg.name).toLowerCase();
15772
+ const tagVal = node.tags[tagKey];
15773
+ if (!tagVal) return null;
15774
+ const tv = tg.values.find((v) => v.name.toLowerCase() === tagVal.toLowerCase());
15775
+ if (!tv?.color) return null;
15776
+ return resolveColor(tv.color, palette);
15777
+ }
15778
+ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions, collapsedNodes, tagGroups) {
15636
15779
  const mutedColor = palette.textMuted;
15637
15780
  for (const node of nodes) {
15638
- const { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
15781
+ let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
15782
+ if (activeGroup && tagGroups && !node.isEdge) {
15783
+ const tagStroke = resolveActiveTagStroke(node, activeGroup, tagGroups, palette);
15784
+ if (tagStroke) {
15785
+ stroke2 = tagStroke;
15786
+ fill2 = mix(palette.bg, tagStroke, isDark ? 88 : 94);
15787
+ }
15788
+ }
15639
15789
  let cls = "infra-node";
15640
15790
  if (animate && node.isEdge) {
15641
15791
  cls += " infra-node-edge-throb";
@@ -15645,9 +15795,9 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15645
15795
  else if (severity === "overloaded") cls += " infra-node-overload";
15646
15796
  else if (severity === "warning") cls += " infra-node-warning";
15647
15797
  }
15648
- const g = svg.append("g").attr("class", cls).attr("data-line-number", node.lineNumber).attr("data-infra-node", node.id);
15798
+ const g = svg.append("g").attr("class", cls).attr("data-line-number", node.lineNumber).attr("data-infra-node", node.id).attr("data-node-collapse", node.id).style("cursor", "pointer");
15649
15799
  if (node.id.startsWith("[")) {
15650
- g.attr("data-node-toggle", node.id).style("cursor", "pointer");
15800
+ g.attr("data-node-toggle", node.id);
15651
15801
  }
15652
15802
  for (const [tagKey, tagVal] of Object.entries(node.tags)) {
15653
15803
  g.attr(`data-tag-${tagKey.toLowerCase()}`, tagVal.toLowerCase());
@@ -15665,7 +15815,12 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15665
15815
  g.append("rect").attr("x", x).attr("y", y).attr("width", node.width).attr("height", node.height).attr("rx", NODE_BORDER_RADIUS).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", strokeWidth);
15666
15816
  const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE3 * 0.35;
15667
15817
  g.append("text").attr("x", node.x).attr("y", headerCenterY).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", NODE_FONT_SIZE3).attr("font-weight", "600").attr("fill", textFill).text(node.label);
15668
- {
15818
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15819
+ if (isNodeCollapsed) {
15820
+ const chevronY = y + node.height - 6;
15821
+ g.append("text").attr("x", node.x).attr("y", chevronY).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", 8).attr("fill", textFill).attr("opacity", 0.5).text("\u25BC");
15822
+ }
15823
+ if (!isNodeCollapsed) {
15669
15824
  const expanded = node.id === selectedNodeId;
15670
15825
  const displayProps = !node.isEdge && expanded ? getDisplayProps(node, expanded, diagramOptions) : [];
15671
15826
  const computedRows = getComputedRows(node, expanded);
@@ -15988,7 +16143,7 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
15988
16143
  cursorX += fullW + LEGEND_GROUP_GAP5;
15989
16144
  }
15990
16145
  }
15991
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode) {
16146
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode, collapsedNodes) {
15992
16147
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
15993
16148
  const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette);
15994
16149
  const hasLegend = legendGroups.length > 0 || !!playback;
@@ -16047,7 +16202,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16047
16202
  }
16048
16203
  renderGroups(svg, layout.groups, palette, isDark);
16049
16204
  renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
16050
- renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options);
16205
+ renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options, collapsedNodes, tagGroups ?? []);
16051
16206
  if (shouldAnimate) {
16052
16207
  renderRejectParticles(svg, layout.nodes);
16053
16208
  }
@@ -17983,6 +18138,7 @@ function parseD3(content, palette) {
17983
18138
  timelineGroups: [],
17984
18139
  timelineEras: [],
17985
18140
  timelineMarkers: [],
18141
+ timelineTagGroups: [],
17986
18142
  timelineSort: "time",
17987
18143
  timelineScale: true,
17988
18144
  timelineSwimlanes: false,
@@ -18017,25 +18173,75 @@ function parseD3(content, palette) {
18017
18173
  const freeformLines = [];
18018
18174
  let currentArcGroup = null;
18019
18175
  let currentTimelineGroup = null;
18176
+ let currentTimelineTagGroup = null;
18177
+ const timelineAliasMap = /* @__PURE__ */ new Map();
18020
18178
  for (let i = 0; i < lines.length; i++) {
18021
- const line10 = lines[i].trim();
18179
+ const rawLine = lines[i];
18180
+ const line10 = rawLine.trim();
18181
+ const indent = rawLine.length - rawLine.trimStart().length;
18022
18182
  const lineNumber = i + 1;
18023
18183
  if (!line10) continue;
18024
- const sectionMatch = line10.match(/^#{2,}\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/);
18025
- if (sectionMatch) {
18184
+ if (result.type === "timeline" && indent === 0) {
18185
+ const tagBlockMatch = matchTagBlockHeading(line10);
18186
+ if (tagBlockMatch) {
18187
+ if (tagBlockMatch.deprecated) {
18188
+ result.diagnostics.push(makeDgmoError(
18189
+ lineNumber,
18190
+ `'## ${tagBlockMatch.name}' is deprecated for tag groups \u2014 use 'tag: ${tagBlockMatch.name}' instead`,
18191
+ "warning"
18192
+ ));
18193
+ }
18194
+ currentTimelineTagGroup = {
18195
+ name: tagBlockMatch.name,
18196
+ alias: tagBlockMatch.alias,
18197
+ entries: [],
18198
+ lineNumber
18199
+ };
18200
+ if (tagBlockMatch.alias) {
18201
+ timelineAliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
18202
+ }
18203
+ result.timelineTagGroups.push(currentTimelineTagGroup);
18204
+ continue;
18205
+ }
18206
+ }
18207
+ if (currentTimelineTagGroup && indent > 0) {
18208
+ const trimmedEntry = line10;
18209
+ const isDefault = /\bdefault\s*$/.test(trimmedEntry);
18210
+ const entryText = isDefault ? trimmedEntry.replace(/\s+default\s*$/, "").trim() : trimmedEntry;
18211
+ const { label, color } = extractColor(entryText, palette);
18212
+ if (color) {
18213
+ if (isDefault) currentTimelineTagGroup.defaultValue = label;
18214
+ currentTimelineTagGroup.entries.push({ value: label, color, lineNumber });
18215
+ continue;
18216
+ }
18217
+ }
18218
+ if (currentTimelineTagGroup && indent === 0) {
18219
+ currentTimelineTagGroup = null;
18220
+ }
18221
+ const groupMatch = line10.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
18222
+ if (groupMatch) {
18026
18223
  if (result.type === "arc") {
18027
- const name = sectionMatch[1].trim();
18028
- const color = sectionMatch[2] ? resolveColor(sectionMatch[2].trim(), palette) : null;
18224
+ const name = groupMatch[1].trim();
18225
+ const color = groupMatch[2] ? resolveColor(groupMatch[2].trim(), palette) : null;
18029
18226
  result.arcNodeGroups.push({ name, nodes: [], color, lineNumber });
18030
18227
  currentArcGroup = name;
18031
18228
  } else if (result.type === "timeline") {
18032
- const name = sectionMatch[1].trim();
18033
- const color = sectionMatch[2] ? resolveColor(sectionMatch[2].trim(), palette) : null;
18229
+ const name = groupMatch[1].trim();
18230
+ const color = groupMatch[2] ? resolveColor(groupMatch[2].trim(), palette) : null;
18034
18231
  result.timelineGroups.push({ name, color, lineNumber });
18035
18232
  currentTimelineGroup = name;
18036
18233
  }
18037
18234
  continue;
18038
18235
  }
18236
+ if (/^#{2,}\s+/.test(line10) && (result.type === "arc" || result.type === "timeline")) {
18237
+ const name = line10.replace(/^#{2,}\s+/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
18238
+ result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`, "warning"));
18239
+ continue;
18240
+ }
18241
+ if (indent === 0) {
18242
+ currentArcGroup = null;
18243
+ currentTimelineGroup = null;
18244
+ }
18039
18245
  if (line10.startsWith("//")) {
18040
18246
  continue;
18041
18247
  }
@@ -18107,11 +18313,14 @@ function parseD3(content, palette) {
18107
18313
  const amount = parseFloat(durationMatch[2]);
18108
18314
  const unit = durationMatch[3];
18109
18315
  const endDate = addDurationToDate(startDate, amount, unit);
18316
+ const segments = durationMatch[5].split("|");
18317
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18110
18318
  result.timelineEvents.push({
18111
18319
  date: startDate,
18112
18320
  endDate,
18113
- label: durationMatch[5].trim(),
18321
+ label: segments[0].trim(),
18114
18322
  group: currentTimelineGroup,
18323
+ metadata,
18115
18324
  lineNumber,
18116
18325
  uncertain
18117
18326
  });
@@ -18121,11 +18330,14 @@ function parseD3(content, palette) {
18121
18330
  /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\d{4}(?:-\d{2})?(?:-\d{2})?)(\?)?\s*:\s*(.+)$/
18122
18331
  );
18123
18332
  if (rangeMatch) {
18333
+ const segments = rangeMatch[4].split("|");
18334
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18124
18335
  result.timelineEvents.push({
18125
18336
  date: rangeMatch[1],
18126
18337
  endDate: rangeMatch[2],
18127
- label: rangeMatch[4].trim(),
18338
+ label: segments[0].trim(),
18128
18339
  group: currentTimelineGroup,
18340
+ metadata,
18129
18341
  lineNumber,
18130
18342
  uncertain: rangeMatch[3] === "?"
18131
18343
  });
@@ -18135,11 +18347,14 @@ function parseD3(content, palette) {
18135
18347
  /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*:\s*(.+)$/
18136
18348
  );
18137
18349
  if (pointMatch) {
18350
+ const segments = pointMatch[2].split("|");
18351
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18138
18352
  result.timelineEvents.push({
18139
18353
  date: pointMatch[1],
18140
18354
  endDate: null,
18141
- label: pointMatch[2].trim(),
18355
+ label: segments[0].trim(),
18142
18356
  group: currentTimelineGroup,
18357
+ metadata,
18143
18358
  lineNumber
18144
18359
  });
18145
18360
  continue;
@@ -18402,7 +18617,7 @@ function parseD3(content, palette) {
18402
18617
  }
18403
18618
  if (result.arcNodeGroups.length > 0) {
18404
18619
  if (result.arcOrder === "name" || result.arcOrder === "degree") {
18405
- warn(1, `Cannot use "order: ${result.arcOrder}" with ## section headers. Use "order: group" or remove section headers.`);
18620
+ warn(1, `Cannot use "order: ${result.arcOrder}" with [Group] headers. Use "order: group" or remove group headers.`);
18406
18621
  result.arcOrder = "group";
18407
18622
  }
18408
18623
  if (result.arcOrder === "appearance") {
@@ -18415,6 +18630,23 @@ function parseD3(content, palette) {
18415
18630
  if (result.timelineEvents.length === 0) {
18416
18631
  warn(1, 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"');
18417
18632
  }
18633
+ if (result.timelineTagGroups.length > 0) {
18634
+ validateTagValues(
18635
+ result.timelineEvents,
18636
+ result.timelineTagGroups,
18637
+ (line10, msg) => result.diagnostics.push(makeDgmoError(line10, msg, "warning")),
18638
+ suggest
18639
+ );
18640
+ for (const group of result.timelineTagGroups) {
18641
+ if (!group.defaultValue) continue;
18642
+ const key = group.name.toLowerCase();
18643
+ for (const event of result.timelineEvents) {
18644
+ if (!event.metadata[key]) {
18645
+ event.metadata[key] = group.defaultValue;
18646
+ }
18647
+ }
18648
+ }
18649
+ }
18418
18650
  return result;
18419
18651
  }
18420
18652
  if (result.type === "venn") {
@@ -19173,7 +19405,7 @@ function buildEventTooltipHtml(ev) {
19173
19405
  function buildEraTooltipHtml(era) {
19174
19406
  return `<strong>${era.label}</strong><br>${formatDateLabel(era.startDate)} \u2192 ${formatDateLabel(era.endDate)}`;
19175
19407
  }
19176
- function renderTimeline(container, parsed, palette, isDark, onClickItem, exportDims) {
19408
+ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportDims, activeTagGroup) {
19177
19409
  d3Selection12.select(container).selectAll(":not([data-d3-tooltip])").remove();
19178
19410
  const {
19179
19411
  timelineEvents,
@@ -19201,6 +19433,10 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19201
19433
  groupColorMap.set(grp.name, grp.color ?? colors[i % colors.length]);
19202
19434
  });
19203
19435
  function eventColor(ev) {
19436
+ if (activeTagGroup) {
19437
+ const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, activeTagGroup);
19438
+ if (tagColor) return tagColor;
19439
+ }
19204
19440
  if (ev.group && groupColorMap.has(ev.group)) {
19205
19441
  return groupColorMap.get(ev.group);
19206
19442
  }
@@ -19271,10 +19507,36 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19271
19507
  }
19272
19508
  function fadeReset(g) {
19273
19509
  g.selectAll(
19274
- ".tl-event, .tl-legend-item, .tl-lane-header, .tl-marker"
19510
+ ".tl-event, .tl-legend-item, .tl-lane-header, .tl-marker, .tl-tag-legend-entry"
19275
19511
  ).attr("opacity", 1);
19276
19512
  g.selectAll(".tl-era").attr("opacity", 1);
19277
19513
  }
19514
+ function fadeToTagValue(g, tagKey, tagValue) {
19515
+ const attrName = `data-tag-${tagKey}`;
19516
+ g.selectAll(".tl-event").each(function() {
19517
+ const el = d3Selection12.select(this);
19518
+ const val = el.attr(attrName);
19519
+ el.attr("opacity", val === tagValue ? 1 : FADE_OPACITY);
19520
+ });
19521
+ g.selectAll(".tl-legend-item, .tl-lane-header").attr(
19522
+ "opacity",
19523
+ FADE_OPACITY
19524
+ );
19525
+ g.selectAll(".tl-marker").attr("opacity", FADE_OPACITY);
19526
+ g.selectAll(".tl-tag-legend-entry").each(function() {
19527
+ const el = d3Selection12.select(this);
19528
+ const entryValue = el.attr("data-legend-entry");
19529
+ if (entryValue === "__group__") return;
19530
+ const entryGroup = el.attr("data-tag-group");
19531
+ el.attr("opacity", entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY);
19532
+ });
19533
+ }
19534
+ function setTagAttrs(evG, ev) {
19535
+ for (const [key, value] of Object.entries(ev.metadata)) {
19536
+ evG.attr(`data-tag-${key}`, value.toLowerCase());
19537
+ }
19538
+ }
19539
+ const tagLegendReserve = parsed.timelineTagGroups.length > 0 ? 36 : 0;
19278
19540
  if (isVertical) {
19279
19541
  if (timelineSort === "group" && timelineGroups.length > 0) {
19280
19542
  const groupNames = timelineGroups.map((gr) => gr.name);
@@ -19286,7 +19548,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19286
19548
  const scaleMargin = timelineScale ? 40 : 0;
19287
19549
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19288
19550
  const margin = {
19289
- top: 104 + markerMargin,
19551
+ top: 104 + markerMargin + tagLegendReserve,
19290
19552
  right: 40 + scaleMargin,
19291
19553
  bottom: 40,
19292
19554
  left: 60 + scaleMargin
@@ -19362,6 +19624,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19362
19624
  }).on("click", () => {
19363
19625
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19364
19626
  });
19627
+ setTagAttrs(evG, ev);
19365
19628
  if (ev.endDate) {
19366
19629
  const y2 = yScale(parseTimelineDate(ev.endDate));
19367
19630
  const rectH = Math.max(y2 - y, 4);
@@ -19388,7 +19651,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19388
19651
  const scaleMargin = timelineScale ? 40 : 0;
19389
19652
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19390
19653
  const margin = {
19391
- top: 104 + markerMargin,
19654
+ top: 104 + markerMargin + tagLegendReserve,
19392
19655
  right: 200,
19393
19656
  bottom: 40,
19394
19657
  left: 60 + scaleMargin
@@ -19468,6 +19731,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19468
19731
  }).on("click", () => {
19469
19732
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19470
19733
  });
19734
+ setTagAttrs(evG, ev);
19471
19735
  if (ev.endDate) {
19472
19736
  const y2 = yScale(parseTimelineDate(ev.endDate));
19473
19737
  const rectH = Math.max(y2 - y, 4);
@@ -19520,7 +19784,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19520
19784
  const dynamicLeftMargin = Math.max(120, maxGroupNameLen * 7 + 30);
19521
19785
  const baseTopMargin = title ? 50 : 20;
19522
19786
  const margin = {
19523
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
19787
+ top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
19524
19788
  right: 40,
19525
19789
  bottom: 40 + scaleMargin,
19526
19790
  left: dynamicLeftMargin
@@ -19623,6 +19887,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19623
19887
  }).on("click", () => {
19624
19888
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19625
19889
  });
19890
+ setTagAttrs(evG, ev);
19626
19891
  if (ev.endDate) {
19627
19892
  const x2 = xScale(parseTimelineDate(ev.endDate));
19628
19893
  const rectW = Math.max(x2 - x, 4);
@@ -19664,7 +19929,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19664
19929
  const scaleMargin = timelineScale ? 24 : 0;
19665
19930
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19666
19931
  const margin = {
19667
- top: 104 + (timelineScale ? 40 : 0) + markerMargin,
19932
+ top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
19668
19933
  right: 40,
19669
19934
  bottom: 40 + scaleMargin,
19670
19935
  left: 60
@@ -19760,6 +20025,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19760
20025
  }).on("click", () => {
19761
20026
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19762
20027
  });
20028
+ setTagAttrs(evG, ev);
19763
20029
  if (ev.endDate) {
19764
20030
  const x2 = xScale(parseTimelineDate(ev.endDate));
19765
20031
  const rectW = Math.max(x2 - x, 4);
@@ -19795,6 +20061,124 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19795
20061
  }
19796
20062
  });
19797
20063
  }
20064
+ if (parsed.timelineTagGroups.length > 0) {
20065
+ const LG_HEIGHT = 28;
20066
+ const LG_PILL_PAD = 16;
20067
+ const LG_PILL_FONT_SIZE = 11;
20068
+ const LG_PILL_FONT_W = LG_PILL_FONT_SIZE * 0.6;
20069
+ const LG_CAPSULE_PAD = 4;
20070
+ const LG_DOT_R = 4;
20071
+ const LG_ENTRY_FONT_SIZE = 10;
20072
+ const LG_ENTRY_FONT_W = LG_ENTRY_FONT_SIZE * 0.6;
20073
+ const LG_ENTRY_DOT_GAP = 4;
20074
+ const LG_ENTRY_TRAIL = 8;
20075
+ const LG_GROUP_GAP = 12;
20076
+ const mainSvg = d3Selection12.select(container).select("svg");
20077
+ const mainG = mainSvg.select("g");
20078
+ if (!mainSvg.empty() && !mainG.empty()) {
20079
+ let drawLegend2 = function() {
20080
+ mainSvg.selectAll(".tl-tag-legend-group").remove();
20081
+ const totalW = legendGroups.reduce((s, lg) => {
20082
+ const isActive = currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20083
+ return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
20084
+ }, 0) + (legendGroups.length - 1) * LG_GROUP_GAP;
20085
+ let cx = (width - totalW) / 2;
20086
+ for (const lg of legendGroups) {
20087
+ const isActive = currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20088
+ const pillLabel = lg.group.name;
20089
+ const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
20090
+ const gEl = mainSvg.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", lg.group.name.toLowerCase()).attr("data-tag-group", lg.group.name.toLowerCase()).attr("data-legend-entry", "__group__").style("cursor", "pointer").on("click", () => {
20091
+ const groupKey = lg.group.name.toLowerCase();
20092
+ currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
20093
+ drawLegend2();
20094
+ recolorEvents2();
20095
+ });
20096
+ if (isActive) {
20097
+ gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
20098
+ }
20099
+ const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
20100
+ const pillYOff = isActive ? LG_CAPSULE_PAD : 0;
20101
+ const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
20102
+ gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
20103
+ if (isActive) {
20104
+ gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
20105
+ }
20106
+ gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2).attr("font-size", LG_PILL_FONT_SIZE).attr("font-weight", "500").attr("font-family", FONT_FAMILY).attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
20107
+ if (isActive) {
20108
+ let entryX = pillXOff + pillWidth + 4;
20109
+ for (const entry of lg.group.entries) {
20110
+ const tagKey = lg.group.name.toLowerCase();
20111
+ const tagVal = entry.value.toLowerCase();
20112
+ const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal).style("cursor", "pointer").on("mouseenter", (event) => {
20113
+ event.stopPropagation();
20114
+ fadeToTagValue(mainG, tagKey, tagVal);
20115
+ mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
20116
+ const el = d3Selection12.select(this);
20117
+ const ev = el.attr("data-legend-entry");
20118
+ if (ev === "__group__") return;
20119
+ const eg = el.attr("data-tag-group");
20120
+ el.attr("opacity", eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY);
20121
+ });
20122
+ }).on("mouseleave", (event) => {
20123
+ event.stopPropagation();
20124
+ fadeReset(mainG);
20125
+ mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
20126
+ }).on("click", (event) => {
20127
+ event.stopPropagation();
20128
+ });
20129
+ entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
20130
+ const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
20131
+ entryG.append("text").attr("x", textX).attr("y", LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LG_ENTRY_FONT_SIZE).attr("font-family", FONT_FAMILY).attr("fill", palette.textMuted).text(entry.value);
20132
+ entryX = textX + entry.value.length * LG_ENTRY_FONT_W + LG_ENTRY_TRAIL;
20133
+ }
20134
+ }
20135
+ cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
20136
+ }
20137
+ }, recolorEvents2 = function() {
20138
+ mainG.selectAll(".tl-event").each(function() {
20139
+ const el = d3Selection12.select(this);
20140
+ const lineNum = el.attr("data-line-number");
20141
+ const ev = lineNum ? eventByLine.get(lineNum) : void 0;
20142
+ if (!ev) return;
20143
+ let color;
20144
+ if (currentActiveGroup) {
20145
+ const tagColor = resolveTagColor(
20146
+ ev.metadata,
20147
+ parsed.timelineTagGroups,
20148
+ currentActiveGroup
20149
+ );
20150
+ color = tagColor ?? (ev.group && groupColorMap.has(ev.group) ? groupColorMap.get(ev.group) : textColor);
20151
+ } else {
20152
+ color = ev.group && groupColorMap.has(ev.group) ? groupColorMap.get(ev.group) : textColor;
20153
+ }
20154
+ el.selectAll("rect").attr("fill", color);
20155
+ el.selectAll("circle:not(.tl-event-point-outline)").attr("fill", color);
20156
+ });
20157
+ };
20158
+ var drawLegend = drawLegend2, recolorEvents = recolorEvents2;
20159
+ const legendY = title ? 50 : 10;
20160
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20161
+ const legendGroups = parsed.timelineTagGroups.map((g) => {
20162
+ const pillW = g.name.length * LG_PILL_FONT_W + LG_PILL_PAD;
20163
+ let entryX = LG_CAPSULE_PAD + pillW + 4;
20164
+ for (const entry of g.entries) {
20165
+ const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
20166
+ entryX = textX + entry.value.length * LG_ENTRY_FONT_W + LG_ENTRY_TRAIL;
20167
+ }
20168
+ return {
20169
+ group: g,
20170
+ minifiedWidth: pillW,
20171
+ expandedWidth: entryX + LG_CAPSULE_PAD
20172
+ };
20173
+ });
20174
+ let currentActiveGroup = activeTagGroup ?? null;
20175
+ const eventByLine = /* @__PURE__ */ new Map();
20176
+ for (const ev of timelineEvents) {
20177
+ eventByLine.set(String(ev.lineNumber), ev);
20178
+ }
20179
+ drawLegend2();
20180
+ }
20181
+ }
19798
20182
  }
19799
20183
  function getRotateFn(mode) {
19800
20184
  if (mode === "mixed") return () => Math.random() > 0.5 ? 0 : 90;
@@ -20785,8 +21169,10 @@ var init_d3 = __esm({
20785
21169
  init_branding();
20786
21170
  init_colors();
20787
21171
  init_palettes();
21172
+ init_color_utils();
20788
21173
  init_diagnostics();
20789
21174
  init_parsing();
21175
+ init_tag_groups();
20790
21176
  DEFAULT_CLOUD_OPTIONS = {
20791
21177
  rotate: "none",
20792
21178
  max: 0,
@@ -21784,6 +22170,9 @@ function encodeDiagramUrl(dsl, options) {
21784
22170
  if (options?.viewState?.activeTagGroup) {
21785
22171
  hash += `&tag=${encodeURIComponent(options.viewState.activeTagGroup)}`;
21786
22172
  }
22173
+ if (options?.viewState?.collapsedGroups?.length) {
22174
+ hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(","))}`;
22175
+ }
21787
22176
  return { url: `${baseUrl}?${hash}#${hash}` };
21788
22177
  }
21789
22178
  function decodeDiagramUrl(hash) {
@@ -21804,6 +22193,9 @@ function decodeDiagramUrl(hash) {
21804
22193
  if (key === "tag" && val) {
21805
22194
  viewState.activeTagGroup = val;
21806
22195
  }
22196
+ if (key === "cg" && val) {
22197
+ viewState.collapsedGroups = val.split(",").filter(Boolean);
22198
+ }
21807
22199
  }
21808
22200
  if (payload.startsWith("dgmo=")) {
21809
22201
  payload = payload.slice(5);