@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.js CHANGED
@@ -1357,6 +1357,94 @@ var init_parsing = __esm({
1357
1357
  }
1358
1358
  });
1359
1359
 
1360
+ // src/utils/tag-groups.ts
1361
+ function isTagBlockHeading(trimmed) {
1362
+ return TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
1363
+ }
1364
+ function resolveTagColor(metadata, tagGroups, activeGroupName, isContainer) {
1365
+ if (!activeGroupName) return void 0;
1366
+ const group = tagGroups.find(
1367
+ (g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
1368
+ );
1369
+ if (!group) return void 0;
1370
+ const metaValue = metadata[group.name.toLowerCase()] ?? (isContainer ? void 0 : group.defaultValue);
1371
+ if (!metaValue) return "#999999";
1372
+ return group.entries.find(
1373
+ (e) => e.value.toLowerCase() === metaValue.toLowerCase()
1374
+ )?.color ?? "#999999";
1375
+ }
1376
+ function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1377
+ if (tagGroups.length === 0) return;
1378
+ const groupMap = /* @__PURE__ */ new Map();
1379
+ for (const g of tagGroups) groupMap.set(g.name.toLowerCase(), g);
1380
+ for (const entity of entities) {
1381
+ for (const [key, value] of Object.entries(entity.metadata)) {
1382
+ const group = groupMap.get(key);
1383
+ if (!group) continue;
1384
+ const match = group.entries.some(
1385
+ (e) => e.value.toLowerCase() === value.toLowerCase()
1386
+ );
1387
+ if (!match) {
1388
+ const defined = group.entries.map((e) => e.value);
1389
+ let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1390
+ const hint = suggestFn?.(value, defined);
1391
+ if (hint) {
1392
+ msg += `. ${hint}`;
1393
+ } else {
1394
+ msg += ` \u2014 defined values: ${defined.join(", ")}`;
1395
+ }
1396
+ pushWarning(entity.lineNumber, msg);
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+ function injectDefaultTagMetadata(entities, tagGroups, skip) {
1402
+ const defaults = [];
1403
+ for (const group of tagGroups) {
1404
+ if (group.defaultValue) {
1405
+ defaults.push({ key: group.name.toLowerCase(), value: group.defaultValue });
1406
+ }
1407
+ }
1408
+ if (defaults.length === 0) return;
1409
+ for (const entity of entities) {
1410
+ if (skip?.(entity)) continue;
1411
+ for (const { key, value } of defaults) {
1412
+ if (!(key in entity.metadata)) {
1413
+ entity.metadata[key] = value;
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ function matchTagBlockHeading(trimmed) {
1419
+ const tagMatch = trimmed.match(TAG_BLOCK_RE);
1420
+ if (tagMatch) {
1421
+ return {
1422
+ name: tagMatch[1].trim(),
1423
+ alias: tagMatch[2] || void 0,
1424
+ colorHint: tagMatch[3] || void 0,
1425
+ deprecated: false
1426
+ };
1427
+ }
1428
+ const groupMatch = trimmed.match(GROUP_HEADING_RE);
1429
+ if (groupMatch) {
1430
+ return {
1431
+ name: groupMatch[1].trim(),
1432
+ alias: groupMatch[2] || void 0,
1433
+ colorHint: groupMatch[3] || void 0,
1434
+ deprecated: true
1435
+ };
1436
+ }
1437
+ return null;
1438
+ }
1439
+ var TAG_BLOCK_RE, GROUP_HEADING_RE;
1440
+ var init_tag_groups = __esm({
1441
+ "src/utils/tag-groups.ts"() {
1442
+ "use strict";
1443
+ TAG_BLOCK_RE = /^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
1444
+ GROUP_HEADING_RE = /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
1445
+ }
1446
+ });
1447
+
1360
1448
  // src/sequence/participant-inference.ts
1361
1449
  function inferParticipantType(name) {
1362
1450
  for (const rule of PARTICIPANT_RULES) {
@@ -1686,94 +1774,6 @@ var init_arrows = __esm({
1686
1774
  }
1687
1775
  });
1688
1776
 
1689
- // src/utils/tag-groups.ts
1690
- function isTagBlockHeading(trimmed) {
1691
- return TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
1692
- }
1693
- function resolveTagColor(metadata, tagGroups, activeGroupName, isContainer) {
1694
- if (!activeGroupName) return void 0;
1695
- const group = tagGroups.find(
1696
- (g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
1697
- );
1698
- if (!group) return void 0;
1699
- const metaValue = metadata[group.name.toLowerCase()] ?? (isContainer ? void 0 : group.defaultValue);
1700
- if (!metaValue) return "#999999";
1701
- return group.entries.find(
1702
- (e) => e.value.toLowerCase() === metaValue.toLowerCase()
1703
- )?.color ?? "#999999";
1704
- }
1705
- function validateTagValues(entities, tagGroups, pushWarning, suggestFn) {
1706
- if (tagGroups.length === 0) return;
1707
- const groupMap = /* @__PURE__ */ new Map();
1708
- for (const g of tagGroups) groupMap.set(g.name.toLowerCase(), g);
1709
- for (const entity of entities) {
1710
- for (const [key, value] of Object.entries(entity.metadata)) {
1711
- const group = groupMap.get(key);
1712
- if (!group) continue;
1713
- const match = group.entries.some(
1714
- (e) => e.value.toLowerCase() === value.toLowerCase()
1715
- );
1716
- if (!match) {
1717
- const defined = group.entries.map((e) => e.value);
1718
- let msg = `Unknown value '${value}' for tag group '${group.name}'`;
1719
- const hint = suggestFn?.(value, defined);
1720
- if (hint) {
1721
- msg += `. ${hint}`;
1722
- } else {
1723
- msg += ` \u2014 defined values: ${defined.join(", ")}`;
1724
- }
1725
- pushWarning(entity.lineNumber, msg);
1726
- }
1727
- }
1728
- }
1729
- }
1730
- function injectDefaultTagMetadata(entities, tagGroups, skip) {
1731
- const defaults = [];
1732
- for (const group of tagGroups) {
1733
- if (group.defaultValue) {
1734
- defaults.push({ key: group.name.toLowerCase(), value: group.defaultValue });
1735
- }
1736
- }
1737
- if (defaults.length === 0) return;
1738
- for (const entity of entities) {
1739
- if (skip?.(entity)) continue;
1740
- for (const { key, value } of defaults) {
1741
- if (!(key in entity.metadata)) {
1742
- entity.metadata[key] = value;
1743
- }
1744
- }
1745
- }
1746
- }
1747
- function matchTagBlockHeading(trimmed) {
1748
- const tagMatch = trimmed.match(TAG_BLOCK_RE);
1749
- if (tagMatch) {
1750
- return {
1751
- name: tagMatch[1].trim(),
1752
- alias: tagMatch[2] || void 0,
1753
- colorHint: tagMatch[3] || void 0,
1754
- deprecated: false
1755
- };
1756
- }
1757
- const groupMatch = trimmed.match(GROUP_HEADING_RE);
1758
- if (groupMatch) {
1759
- return {
1760
- name: groupMatch[1].trim(),
1761
- alias: groupMatch[2] || void 0,
1762
- colorHint: groupMatch[3] || void 0,
1763
- deprecated: true
1764
- };
1765
- }
1766
- return null;
1767
- }
1768
- var TAG_BLOCK_RE, GROUP_HEADING_RE;
1769
- var init_tag_groups = __esm({
1770
- "src/utils/tag-groups.ts"() {
1771
- "use strict";
1772
- TAG_BLOCK_RE = /^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
1773
- GROUP_HEADING_RE = /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
1774
- }
1775
- });
1776
-
1777
1777
  // src/sequence/parser.ts
1778
1778
  var parser_exports = {};
1779
1779
  __export(parser_exports, {
@@ -3361,6 +3361,7 @@ function parseERDiagram(content, palette) {
3361
3361
  options: {},
3362
3362
  tables: [],
3363
3363
  relationships: [],
3364
+ tagGroups: [],
3364
3365
  diagnostics: [],
3365
3366
  error: null
3366
3367
  };
@@ -3378,6 +3379,8 @@ function parseERDiagram(content, palette) {
3378
3379
  const tableMap = /* @__PURE__ */ new Map();
3379
3380
  let currentTable = null;
3380
3381
  let contentStarted = false;
3382
+ let currentTagGroup = null;
3383
+ const aliasMap = /* @__PURE__ */ new Map();
3381
3384
  function getOrCreateTable(name, lineNumber) {
3382
3385
  const id = tableId(name);
3383
3386
  const existing = tableMap.get(id);
@@ -3386,6 +3389,7 @@ function parseERDiagram(content, palette) {
3386
3389
  id,
3387
3390
  name,
3388
3391
  columns: [],
3392
+ metadata: {},
3389
3393
  lineNumber
3390
3394
  };
3391
3395
  tableMap.set(id, table);
@@ -3402,6 +3406,50 @@ function parseERDiagram(content, palette) {
3402
3406
  continue;
3403
3407
  }
3404
3408
  if (trimmed.startsWith("//")) continue;
3409
+ if (!contentStarted && indent === 0) {
3410
+ const tagBlockMatch = matchTagBlockHeading(trimmed);
3411
+ if (tagBlockMatch) {
3412
+ if (tagBlockMatch.deprecated) {
3413
+ result.diagnostics.push(makeDgmoError(
3414
+ lineNumber,
3415
+ `'## ${tagBlockMatch.name}' is deprecated for tag groups \u2014 use 'tag: ${tagBlockMatch.name}' instead`,
3416
+ "warning"
3417
+ ));
3418
+ }
3419
+ currentTagGroup = {
3420
+ name: tagBlockMatch.name,
3421
+ alias: tagBlockMatch.alias,
3422
+ entries: [],
3423
+ lineNumber
3424
+ };
3425
+ if (tagBlockMatch.alias) {
3426
+ aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
3427
+ }
3428
+ result.tagGroups.push(currentTagGroup);
3429
+ continue;
3430
+ }
3431
+ }
3432
+ if (currentTagGroup && !contentStarted && indent > 0) {
3433
+ const isDefault = /\bdefault\s*$/.test(trimmed);
3434
+ const entryText = isDefault ? trimmed.replace(/\s+default\s*$/, "").trim() : trimmed;
3435
+ const { label, color } = extractColor(entryText, palette);
3436
+ if (!color) {
3437
+ result.diagnostics.push(makeDgmoError(
3438
+ lineNumber,
3439
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
3440
+ "warning"
3441
+ ));
3442
+ continue;
3443
+ }
3444
+ if (isDefault) {
3445
+ currentTagGroup.defaultValue = label;
3446
+ }
3447
+ currentTagGroup.entries.push({ value: label, color, lineNumber });
3448
+ continue;
3449
+ }
3450
+ if (currentTagGroup && indent === 0) {
3451
+ currentTagGroup = null;
3452
+ }
3405
3453
  if (!contentStarted && indent === 0 && /^[a-z][a-z0-9-]*\s*:/i.test(trimmed)) {
3406
3454
  const colonIdx = trimmed.indexOf(":");
3407
3455
  const key = trimmed.substring(0, colonIdx).trim().toLowerCase();
@@ -3483,6 +3531,11 @@ function parseERDiagram(content, palette) {
3483
3531
  const table = getOrCreateTable(name, lineNumber);
3484
3532
  if (color) table.color = color;
3485
3533
  table.lineNumber = lineNumber;
3534
+ const pipeStr = tableDecl[3]?.trim();
3535
+ if (pipeStr) {
3536
+ const meta = parsePipeMetadata(["", pipeStr], aliasMap);
3537
+ Object.assign(table.metadata, meta);
3538
+ }
3486
3539
  currentTable = table;
3487
3540
  continue;
3488
3541
  }
@@ -3492,6 +3545,27 @@ function parseERDiagram(content, palette) {
3492
3545
  result.diagnostics.push(diag);
3493
3546
  result.error = formatDgmoError(diag);
3494
3547
  }
3548
+ if (result.tagGroups.length > 0) {
3549
+ const tagEntities = result.tables.map((t) => ({
3550
+ metadata: t.metadata,
3551
+ lineNumber: t.lineNumber
3552
+ }));
3553
+ validateTagValues(
3554
+ tagEntities,
3555
+ result.tagGroups,
3556
+ (line10, msg) => result.diagnostics.push(makeDgmoError(line10, msg, "warning")),
3557
+ suggest
3558
+ );
3559
+ for (const group of result.tagGroups) {
3560
+ if (!group.defaultValue) continue;
3561
+ const key = group.name.toLowerCase();
3562
+ for (const table of result.tables) {
3563
+ if (!table.metadata[key]) {
3564
+ table.metadata[key] = group.defaultValue;
3565
+ }
3566
+ }
3567
+ }
3568
+ }
3495
3569
  if (result.tables.length >= 2 && result.relationships.length >= 1 && !result.error) {
3496
3570
  const connectedIds = /* @__PURE__ */ new Set();
3497
3571
  for (const rel of result.relationships) {
@@ -3543,7 +3617,8 @@ var init_parser3 = __esm({
3543
3617
  init_colors();
3544
3618
  init_diagnostics();
3545
3619
  init_parsing();
3546
- TABLE_DECL_RE = /^([a-zA-Z_]\w*)(?:\s+\(([^)]+)\))?\s*$/;
3620
+ init_tag_groups();
3621
+ TABLE_DECL_RE = /^([a-zA-Z_]\w*)(?:\s*\(([^)]+)\))?(?:\s*\|(.+))?$/;
3547
3622
  COLUMN_RE = /^(\w+)(?:\s*:\s*(\w[\w()]*(?:\s*\[\])?))?(?:\s+\[([^\]]+)\])?\s*$/;
3548
3623
  INDENT_REL_RE = /^([1*?])-(?:(.+)-)?([1*?])\s+([a-zA-Z_]\w*)\s*$/;
3549
3624
  CONSTRAINT_MAP = {
@@ -3581,7 +3656,10 @@ function parseChart(content, palette) {
3581
3656
  const trimmed = lines[i].trim();
3582
3657
  const lineNumber = i + 1;
3583
3658
  if (!trimmed) continue;
3584
- if (/^#{2,}\s+/.test(trimmed)) continue;
3659
+ if (/^#{2,}\s+/.test(trimmed)) {
3660
+ result.diagnostics.push(makeDgmoError(lineNumber, `'${trimmed}' \u2014 ## syntax is no longer supported. Use [Group] containers instead`));
3661
+ continue;
3662
+ }
3585
3663
  if (trimmed.startsWith("//")) continue;
3586
3664
  const colonIndex = trimmed.indexOf(":");
3587
3665
  if (colonIndex === -1) continue;
@@ -3729,9 +3807,16 @@ function parseEChart(content, palette) {
3729
3807
  const trimmed = lines[i].trim();
3730
3808
  const lineNumber = i + 1;
3731
3809
  if (!trimmed) continue;
3732
- const mdCategoryMatch = trimmed.match(/^#{2,}\s+(.+)$/);
3733
- if (mdCategoryMatch) {
3734
- const { label: catName, color: catColor } = extractColor(mdCategoryMatch[1].trim(), palette);
3810
+ if (/^#{2,}\s+/.test(trimmed)) {
3811
+ const name = trimmed.replace(/^#{2,}\s+/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
3812
+ result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`));
3813
+ continue;
3814
+ }
3815
+ if (trimmed.startsWith("//")) continue;
3816
+ const categoryMatch = trimmed.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
3817
+ if (categoryMatch) {
3818
+ const catName = categoryMatch[1].trim();
3819
+ const catColor = categoryMatch[2] ? resolveColor(categoryMatch[2].trim(), palette) : null;
3735
3820
  if (catColor) {
3736
3821
  if (!result.categoryColors) result.categoryColors = {};
3737
3822
  result.categoryColors[catName] = catColor;
@@ -3739,12 +3824,6 @@ function parseEChart(content, palette) {
3739
3824
  currentCategory = catName;
3740
3825
  continue;
3741
3826
  }
3742
- if (trimmed.startsWith("//")) continue;
3743
- const categoryMatch = trimmed.match(/^\[(.+)\]$/);
3744
- if (categoryMatch) {
3745
- currentCategory = categoryMatch[1].trim();
3746
- continue;
3747
- }
3748
3827
  const colonIndex = trimmed.indexOf(":");
3749
3828
  if (result.type === "sankey" && colonIndex === -1) {
3750
3829
  const indent = measureIndent(lines[i]);
@@ -5375,6 +5454,7 @@ function parseKanban(content, palette) {
5375
5454
  let currentTagGroup = null;
5376
5455
  let currentColumn = null;
5377
5456
  let currentCard = null;
5457
+ let cardBaseIndent = 0;
5378
5458
  let columnCounter = 0;
5379
5459
  let cardCounter = 0;
5380
5460
  const aliasMap = /* @__PURE__ */ new Map();
@@ -5474,7 +5554,14 @@ function parseKanban(content, palette) {
5474
5554
  }
5475
5555
  currentTagGroup = null;
5476
5556
  }
5477
- const columnMatch = trimmed.match(COLUMN_RE2);
5557
+ const indent = measureIndent(line10);
5558
+ if (LEGACY_COLUMN_RE.test(trimmed)) {
5559
+ const legacyMatch = trimmed.match(LEGACY_COLUMN_RE);
5560
+ const name = legacyMatch[1].replace(/\s*\(.*\)\s*$/, "").trim();
5561
+ warn(lineNumber, `'== ${name} ==' is no longer supported. Use '[${name}]' instead`);
5562
+ continue;
5563
+ }
5564
+ const columnMatch = indent === 0 ? trimmed.match(COLUMN_RE2) : null;
5478
5565
  if (columnMatch) {
5479
5566
  contentStarted = true;
5480
5567
  currentTagGroup = null;
@@ -5486,16 +5573,20 @@ function parseKanban(content, palette) {
5486
5573
  }
5487
5574
  currentCard = null;
5488
5575
  columnCounter++;
5489
- const rawColName = columnMatch[1].trim();
5490
- const wipStr = columnMatch[2];
5491
- const { label: colName, color: colColor } = extractColor(
5492
- rawColName,
5493
- palette
5494
- );
5576
+ const colName = columnMatch[1].trim();
5577
+ const colColor = columnMatch[2] ? resolveColor(columnMatch[2].trim(), palette) : void 0;
5578
+ let wipLimit;
5579
+ const pipeStr = columnMatch[3];
5580
+ if (pipeStr) {
5581
+ const wipMatch = pipeStr.match(/\bwip\s*:\s*(\d+)\b/i);
5582
+ if (wipMatch) {
5583
+ wipLimit = parseInt(wipMatch[1], 10);
5584
+ }
5585
+ }
5495
5586
  currentColumn = {
5496
5587
  id: `col-${columnCounter}`,
5497
5588
  name: colName,
5498
- wipLimit: wipStr ? parseInt(wipStr, 10) : void 0,
5589
+ wipLimit,
5499
5590
  color: colColor,
5500
5591
  cards: [],
5501
5592
  lineNumber
@@ -5510,24 +5601,25 @@ function parseKanban(content, palette) {
5510
5601
  warn(lineNumber, "Card line found before any column");
5511
5602
  continue;
5512
5603
  }
5513
- const indent = measureIndent(line10);
5514
- if (indent > 0 && currentCard) {
5604
+ if (currentCard && indent > cardBaseIndent) {
5515
5605
  currentCard.details.push(trimmed);
5516
5606
  currentCard.endLineNumber = lineNumber;
5517
5607
  continue;
5518
5608
  }
5519
- if (currentCard) {
5609
+ if (indent > 0) {
5610
+ cardCounter++;
5611
+ const card = parseCardLine(
5612
+ trimmed,
5613
+ lineNumber,
5614
+ cardCounter,
5615
+ aliasMap,
5616
+ palette
5617
+ );
5618
+ cardBaseIndent = indent;
5619
+ currentCard = card;
5620
+ currentColumn.cards.push(card);
5621
+ continue;
5520
5622
  }
5521
- cardCounter++;
5522
- const card = parseCardLine(
5523
- trimmed,
5524
- lineNumber,
5525
- cardCounter,
5526
- aliasMap,
5527
- palette
5528
- );
5529
- currentCard = card;
5530
- currentColumn.cards.push(card);
5531
5623
  }
5532
5624
  if (currentCard) {
5533
5625
  }
@@ -5561,7 +5653,7 @@ function parseKanban(content, palette) {
5561
5653
  }
5562
5654
  }
5563
5655
  if (result.columns.length === 0 && !result.error) {
5564
- return fail(1, "No columns found. Use == Column Name == to define columns");
5656
+ return fail(1, "No columns found. Use [Column Name] to define columns");
5565
5657
  }
5566
5658
  return result;
5567
5659
  }
@@ -5598,14 +5690,16 @@ function parseCardLine(trimmed, lineNumber, counter, aliasMap, palette) {
5598
5690
  color
5599
5691
  };
5600
5692
  }
5601
- var COLUMN_RE2;
5693
+ var COLUMN_RE2, LEGACY_COLUMN_RE;
5602
5694
  var init_parser5 = __esm({
5603
5695
  "src/kanban/parser.ts"() {
5604
5696
  "use strict";
5605
5697
  init_diagnostics();
5698
+ init_colors();
5606
5699
  init_tag_groups();
5607
5700
  init_parsing();
5608
- COLUMN_RE2 = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
5701
+ COLUMN_RE2 = /^\[(.+?)\](?:\s*\(([^)]+)\))?\s*(?:\|\s*(.+))?$/;
5702
+ LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
5609
5703
  }
5610
5704
  });
5611
5705
 
@@ -7190,7 +7284,7 @@ var init_parser9 = __esm({
7190
7284
  TAG_GROUP_RE = /^tag\s*:\s*(\w[\w\s]*?)(?:\s+alias\s+(\w+))?\s*$/;
7191
7285
  TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?(\s+default)?\s*$/;
7192
7286
  COMPONENT_RE = /^([a-zA-Z_][\w]*)(.*)$/;
7193
- PIPE_META_RE = /\|\s*(\w+)\s*:\s*([^|]+)/g;
7287
+ PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
7194
7288
  PROPERTY_RE = /^([\w-]+)\s*:\s*(.+)$/;
7195
7289
  PERCENT_RE = /^([\d.]+)%$/;
7196
7290
  RANGE_RE = /^(\d+)-(\d+)$/;
@@ -9556,7 +9650,7 @@ function computeCardArchive(content, parsed, cardId) {
9556
9650
  const trimmedEnd = withoutCard.length > 0 && withoutCard[withoutCard.length - 1].trim() === "" ? withoutCard : [...withoutCard, ""];
9557
9651
  return [
9558
9652
  ...trimmedEnd,
9559
- "== Archive ==",
9653
+ "[Archive]",
9560
9654
  ...cardLines
9561
9655
  ].join("\n");
9562
9656
  }
@@ -10437,7 +10531,7 @@ function drawCardinality(g, point, prevPoint, cardinality, color, useLabels) {
10437
10531
  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);
10438
10532
  }
10439
10533
  }
10440
- function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims) {
10534
+ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
10441
10535
  d3Selection5.select(container).selectAll(":not([data-d3-tooltip])").remove();
10442
10536
  const width = exportDims?.width ?? container.clientWidth;
10443
10537
  const height = exportDims?.height ?? container.clientHeight;
@@ -10507,8 +10601,16 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10507
10601
  }
10508
10602
  for (let ni = 0; ni < layout.nodes.length; ni++) {
10509
10603
  const node = layout.nodes[ni];
10510
- const nodeColor2 = node.color ?? seriesColors2[ni % seriesColors2.length];
10604
+ const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
10605
+ const nodeColor2 = node.color ?? tagColor ?? seriesColors2[ni % seriesColors2.length];
10511
10606
  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);
10607
+ if (activeTagGroup) {
10608
+ const tagKey = activeTagGroup.toLowerCase();
10609
+ const tagValue = node.metadata[tagKey];
10610
+ if (tagValue) {
10611
+ nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
10612
+ }
10613
+ }
10512
10614
  if (onClickItem) {
10513
10615
  nodeG.style("cursor", "pointer").on("click", () => {
10514
10616
  onClickItem(node.lineNumber);
@@ -10540,6 +10642,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
10540
10642
  }
10541
10643
  }
10542
10644
  }
10645
+ if (parsed.tagGroups.length > 0) {
10646
+ const LEGEND_Y_PAD = 16;
10647
+ const LEGEND_PILL_H = 22;
10648
+ const LEGEND_PILL_RX = 11;
10649
+ const LEGEND_PILL_PAD9 = 10;
10650
+ const LEGEND_GAP2 = 8;
10651
+ const LEGEND_FONT_SIZE2 = 11;
10652
+ const LEGEND_GROUP_GAP7 = 16;
10653
+ const legendG = svg.append("g").attr("class", "er-tag-legend");
10654
+ let legendX = DIAGRAM_PADDING5;
10655
+ let legendY = height - DIAGRAM_PADDING5;
10656
+ for (const group of parsed.tagGroups) {
10657
+ const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
10658
+ 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}:`);
10659
+ const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
10660
+ legendX += labelWidth;
10661
+ for (const entry of group.entries) {
10662
+ const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
10663
+ const tmpText = legendG.append("text").attr("font-size", LEGEND_FONT_SIZE2).attr("font-family", FONT_FAMILY).text(entry.value);
10664
+ const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
10665
+ tmpText.remove();
10666
+ const pillW = textW + LEGEND_PILL_PAD9 * 2;
10667
+ 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);
10668
+ 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);
10669
+ legendX += pillW + LEGEND_GAP2;
10670
+ }
10671
+ legendX += LEGEND_GROUP_GAP7;
10672
+ }
10673
+ }
10543
10674
  }
10544
10675
  function renderERDiagramForExport(content, theme, palette) {
10545
10676
  const parsed = parseERDiagram(content, palette);
@@ -10583,6 +10714,7 @@ var init_renderer5 = __esm({
10583
10714
  init_fonts();
10584
10715
  init_color_utils();
10585
10716
  init_palettes();
10717
+ init_tag_groups();
10586
10718
  init_parser3();
10587
10719
  init_layout4();
10588
10720
  DIAGRAM_PADDING5 = 20;
@@ -15015,7 +15147,7 @@ function formatUptime(fraction) {
15015
15147
  if (pct >= 99) return `${pct.toFixed(1)}%`;
15016
15148
  return `${pct.toFixed(1)}%`;
15017
15149
  }
15018
- function layoutInfra(computed, selectedNodeId) {
15150
+ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15019
15151
  if (computed.nodes.length === 0) {
15020
15152
  return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15021
15153
  }
@@ -15036,9 +15168,10 @@ function layoutInfra(computed, selectedNodeId) {
15036
15168
  const widthMap = /* @__PURE__ */ new Map();
15037
15169
  const heightMap = /* @__PURE__ */ new Map();
15038
15170
  for (const node of computed.nodes) {
15039
- const expanded = node.id === selectedNodeId;
15171
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15172
+ const expanded = !isNodeCollapsed && node.id === selectedNodeId;
15040
15173
  const width = computeNodeWidth2(node, expanded, computed.options);
15041
- const height = computeNodeHeight2(node, expanded, computed.options);
15174
+ const height = isNodeCollapsed ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM : computeNodeHeight2(node, expanded, computed.options);
15042
15175
  widthMap.set(node.id, width);
15043
15176
  heightMap.set(node.id, height);
15044
15177
  const inGroup = groupedNodeIds.has(node.id);
@@ -15612,10 +15745,27 @@ function renderEdgeLabels(svg, edges, palette, isDark, animate) {
15612
15745
  }
15613
15746
  }
15614
15747
  }
15615
- function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions) {
15748
+ function resolveActiveTagStroke(node, activeGroup, tagGroups, palette) {
15749
+ const tg = tagGroups.find((t) => t.name.toLowerCase() === activeGroup.toLowerCase());
15750
+ if (!tg) return null;
15751
+ const tagKey = (tg.alias ?? tg.name).toLowerCase();
15752
+ const tagVal = node.tags[tagKey];
15753
+ if (!tagVal) return null;
15754
+ const tv = tg.values.find((v) => v.name.toLowerCase() === tagVal.toLowerCase());
15755
+ if (!tv?.color) return null;
15756
+ return resolveColor(tv.color, palette);
15757
+ }
15758
+ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions, collapsedNodes, tagGroups) {
15616
15759
  const mutedColor = palette.textMuted;
15617
15760
  for (const node of nodes) {
15618
- const { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
15761
+ let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
15762
+ if (activeGroup && tagGroups && !node.isEdge) {
15763
+ const tagStroke = resolveActiveTagStroke(node, activeGroup, tagGroups, palette);
15764
+ if (tagStroke) {
15765
+ stroke2 = tagStroke;
15766
+ fill2 = mix(palette.bg, tagStroke, isDark ? 88 : 94);
15767
+ }
15768
+ }
15619
15769
  let cls = "infra-node";
15620
15770
  if (animate && node.isEdge) {
15621
15771
  cls += " infra-node-edge-throb";
@@ -15625,9 +15775,9 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15625
15775
  else if (severity === "overloaded") cls += " infra-node-overload";
15626
15776
  else if (severity === "warning") cls += " infra-node-warning";
15627
15777
  }
15628
- const g = svg.append("g").attr("class", cls).attr("data-line-number", node.lineNumber).attr("data-infra-node", node.id);
15778
+ 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");
15629
15779
  if (node.id.startsWith("[")) {
15630
- g.attr("data-node-toggle", node.id).style("cursor", "pointer");
15780
+ g.attr("data-node-toggle", node.id);
15631
15781
  }
15632
15782
  for (const [tagKey, tagVal] of Object.entries(node.tags)) {
15633
15783
  g.attr(`data-tag-${tagKey.toLowerCase()}`, tagVal.toLowerCase());
@@ -15645,7 +15795,12 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15645
15795
  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);
15646
15796
  const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE3 * 0.35;
15647
15797
  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);
15648
- {
15798
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15799
+ if (isNodeCollapsed) {
15800
+ const chevronY = y + node.height - 6;
15801
+ 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");
15802
+ }
15803
+ if (!isNodeCollapsed) {
15649
15804
  const expanded = node.id === selectedNodeId;
15650
15805
  const displayProps = !node.isEdge && expanded ? getDisplayProps(node, expanded, diagramOptions) : [];
15651
15806
  const computedRows = getComputedRows(node, expanded);
@@ -15968,7 +16123,7 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
15968
16123
  cursorX += fullW + LEGEND_GROUP_GAP5;
15969
16124
  }
15970
16125
  }
15971
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode) {
16126
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode, collapsedNodes) {
15972
16127
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
15973
16128
  const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette);
15974
16129
  const hasLegend = legendGroups.length > 0 || !!playback;
@@ -16027,7 +16182,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16027
16182
  }
16028
16183
  renderGroups(svg, layout.groups, palette, isDark);
16029
16184
  renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
16030
- renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options);
16185
+ renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options, collapsedNodes, tagGroups ?? []);
16031
16186
  if (shouldAnimate) {
16032
16187
  renderRejectParticles(svg, layout.nodes);
16033
16188
  }
@@ -17966,6 +18121,7 @@ function parseD3(content, palette) {
17966
18121
  timelineGroups: [],
17967
18122
  timelineEras: [],
17968
18123
  timelineMarkers: [],
18124
+ timelineTagGroups: [],
17969
18125
  timelineSort: "time",
17970
18126
  timelineScale: true,
17971
18127
  timelineSwimlanes: false,
@@ -18000,25 +18156,75 @@ function parseD3(content, palette) {
18000
18156
  const freeformLines = [];
18001
18157
  let currentArcGroup = null;
18002
18158
  let currentTimelineGroup = null;
18159
+ let currentTimelineTagGroup = null;
18160
+ const timelineAliasMap = /* @__PURE__ */ new Map();
18003
18161
  for (let i = 0; i < lines.length; i++) {
18004
- const line10 = lines[i].trim();
18162
+ const rawLine = lines[i];
18163
+ const line10 = rawLine.trim();
18164
+ const indent = rawLine.length - rawLine.trimStart().length;
18005
18165
  const lineNumber = i + 1;
18006
18166
  if (!line10) continue;
18007
- const sectionMatch = line10.match(/^#{2,}\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/);
18008
- if (sectionMatch) {
18167
+ if (result.type === "timeline" && indent === 0) {
18168
+ const tagBlockMatch = matchTagBlockHeading(line10);
18169
+ if (tagBlockMatch) {
18170
+ if (tagBlockMatch.deprecated) {
18171
+ result.diagnostics.push(makeDgmoError(
18172
+ lineNumber,
18173
+ `'## ${tagBlockMatch.name}' is deprecated for tag groups \u2014 use 'tag: ${tagBlockMatch.name}' instead`,
18174
+ "warning"
18175
+ ));
18176
+ }
18177
+ currentTimelineTagGroup = {
18178
+ name: tagBlockMatch.name,
18179
+ alias: tagBlockMatch.alias,
18180
+ entries: [],
18181
+ lineNumber
18182
+ };
18183
+ if (tagBlockMatch.alias) {
18184
+ timelineAliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
18185
+ }
18186
+ result.timelineTagGroups.push(currentTimelineTagGroup);
18187
+ continue;
18188
+ }
18189
+ }
18190
+ if (currentTimelineTagGroup && indent > 0) {
18191
+ const trimmedEntry = line10;
18192
+ const isDefault = /\bdefault\s*$/.test(trimmedEntry);
18193
+ const entryText = isDefault ? trimmedEntry.replace(/\s+default\s*$/, "").trim() : trimmedEntry;
18194
+ const { label, color } = extractColor(entryText, palette);
18195
+ if (color) {
18196
+ if (isDefault) currentTimelineTagGroup.defaultValue = label;
18197
+ currentTimelineTagGroup.entries.push({ value: label, color, lineNumber });
18198
+ continue;
18199
+ }
18200
+ }
18201
+ if (currentTimelineTagGroup && indent === 0) {
18202
+ currentTimelineTagGroup = null;
18203
+ }
18204
+ const groupMatch = line10.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
18205
+ if (groupMatch) {
18009
18206
  if (result.type === "arc") {
18010
- const name = sectionMatch[1].trim();
18011
- const color = sectionMatch[2] ? resolveColor(sectionMatch[2].trim(), palette) : null;
18207
+ const name = groupMatch[1].trim();
18208
+ const color = groupMatch[2] ? resolveColor(groupMatch[2].trim(), palette) : null;
18012
18209
  result.arcNodeGroups.push({ name, nodes: [], color, lineNumber });
18013
18210
  currentArcGroup = name;
18014
18211
  } else if (result.type === "timeline") {
18015
- const name = sectionMatch[1].trim();
18016
- const color = sectionMatch[2] ? resolveColor(sectionMatch[2].trim(), palette) : null;
18212
+ const name = groupMatch[1].trim();
18213
+ const color = groupMatch[2] ? resolveColor(groupMatch[2].trim(), palette) : null;
18017
18214
  result.timelineGroups.push({ name, color, lineNumber });
18018
18215
  currentTimelineGroup = name;
18019
18216
  }
18020
18217
  continue;
18021
18218
  }
18219
+ if (/^#{2,}\s+/.test(line10) && (result.type === "arc" || result.type === "timeline")) {
18220
+ const name = line10.replace(/^#{2,}\s+/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
18221
+ result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`, "warning"));
18222
+ continue;
18223
+ }
18224
+ if (indent === 0) {
18225
+ currentArcGroup = null;
18226
+ currentTimelineGroup = null;
18227
+ }
18022
18228
  if (line10.startsWith("//")) {
18023
18229
  continue;
18024
18230
  }
@@ -18090,11 +18296,14 @@ function parseD3(content, palette) {
18090
18296
  const amount = parseFloat(durationMatch[2]);
18091
18297
  const unit = durationMatch[3];
18092
18298
  const endDate = addDurationToDate(startDate, amount, unit);
18299
+ const segments = durationMatch[5].split("|");
18300
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18093
18301
  result.timelineEvents.push({
18094
18302
  date: startDate,
18095
18303
  endDate,
18096
- label: durationMatch[5].trim(),
18304
+ label: segments[0].trim(),
18097
18305
  group: currentTimelineGroup,
18306
+ metadata,
18098
18307
  lineNumber,
18099
18308
  uncertain
18100
18309
  });
@@ -18104,11 +18313,14 @@ function parseD3(content, palette) {
18104
18313
  /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\d{4}(?:-\d{2})?(?:-\d{2})?)(\?)?\s*:\s*(.+)$/
18105
18314
  );
18106
18315
  if (rangeMatch) {
18316
+ const segments = rangeMatch[4].split("|");
18317
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18107
18318
  result.timelineEvents.push({
18108
18319
  date: rangeMatch[1],
18109
18320
  endDate: rangeMatch[2],
18110
- label: rangeMatch[4].trim(),
18321
+ label: segments[0].trim(),
18111
18322
  group: currentTimelineGroup,
18323
+ metadata,
18112
18324
  lineNumber,
18113
18325
  uncertain: rangeMatch[3] === "?"
18114
18326
  });
@@ -18118,11 +18330,14 @@ function parseD3(content, palette) {
18118
18330
  /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*:\s*(.+)$/
18119
18331
  );
18120
18332
  if (pointMatch) {
18333
+ const segments = pointMatch[2].split("|");
18334
+ const metadata = segments.length > 1 ? parsePipeMetadata(["", ...segments.slice(1)], timelineAliasMap) : {};
18121
18335
  result.timelineEvents.push({
18122
18336
  date: pointMatch[1],
18123
18337
  endDate: null,
18124
- label: pointMatch[2].trim(),
18338
+ label: segments[0].trim(),
18125
18339
  group: currentTimelineGroup,
18340
+ metadata,
18126
18341
  lineNumber
18127
18342
  });
18128
18343
  continue;
@@ -18385,7 +18600,7 @@ function parseD3(content, palette) {
18385
18600
  }
18386
18601
  if (result.arcNodeGroups.length > 0) {
18387
18602
  if (result.arcOrder === "name" || result.arcOrder === "degree") {
18388
- warn(1, `Cannot use "order: ${result.arcOrder}" with ## section headers. Use "order: group" or remove section headers.`);
18603
+ warn(1, `Cannot use "order: ${result.arcOrder}" with [Group] headers. Use "order: group" or remove group headers.`);
18389
18604
  result.arcOrder = "group";
18390
18605
  }
18391
18606
  if (result.arcOrder === "appearance") {
@@ -18398,6 +18613,23 @@ function parseD3(content, palette) {
18398
18613
  if (result.timelineEvents.length === 0) {
18399
18614
  warn(1, 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"');
18400
18615
  }
18616
+ if (result.timelineTagGroups.length > 0) {
18617
+ validateTagValues(
18618
+ result.timelineEvents,
18619
+ result.timelineTagGroups,
18620
+ (line10, msg) => result.diagnostics.push(makeDgmoError(line10, msg, "warning")),
18621
+ suggest
18622
+ );
18623
+ for (const group of result.timelineTagGroups) {
18624
+ if (!group.defaultValue) continue;
18625
+ const key = group.name.toLowerCase();
18626
+ for (const event of result.timelineEvents) {
18627
+ if (!event.metadata[key]) {
18628
+ event.metadata[key] = group.defaultValue;
18629
+ }
18630
+ }
18631
+ }
18632
+ }
18401
18633
  return result;
18402
18634
  }
18403
18635
  if (result.type === "venn") {
@@ -19156,7 +19388,7 @@ function buildEventTooltipHtml(ev) {
19156
19388
  function buildEraTooltipHtml(era) {
19157
19389
  return `<strong>${era.label}</strong><br>${formatDateLabel(era.startDate)} \u2192 ${formatDateLabel(era.endDate)}`;
19158
19390
  }
19159
- function renderTimeline(container, parsed, palette, isDark, onClickItem, exportDims) {
19391
+ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportDims, activeTagGroup) {
19160
19392
  d3Selection12.select(container).selectAll(":not([data-d3-tooltip])").remove();
19161
19393
  const {
19162
19394
  timelineEvents,
@@ -19184,6 +19416,10 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19184
19416
  groupColorMap.set(grp.name, grp.color ?? colors[i % colors.length]);
19185
19417
  });
19186
19418
  function eventColor(ev) {
19419
+ if (activeTagGroup) {
19420
+ const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, activeTagGroup);
19421
+ if (tagColor) return tagColor;
19422
+ }
19187
19423
  if (ev.group && groupColorMap.has(ev.group)) {
19188
19424
  return groupColorMap.get(ev.group);
19189
19425
  }
@@ -19254,10 +19490,36 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19254
19490
  }
19255
19491
  function fadeReset(g) {
19256
19492
  g.selectAll(
19257
- ".tl-event, .tl-legend-item, .tl-lane-header, .tl-marker"
19493
+ ".tl-event, .tl-legend-item, .tl-lane-header, .tl-marker, .tl-tag-legend-entry"
19258
19494
  ).attr("opacity", 1);
19259
19495
  g.selectAll(".tl-era").attr("opacity", 1);
19260
19496
  }
19497
+ function fadeToTagValue(g, tagKey, tagValue) {
19498
+ const attrName = `data-tag-${tagKey}`;
19499
+ g.selectAll(".tl-event").each(function() {
19500
+ const el = d3Selection12.select(this);
19501
+ const val = el.attr(attrName);
19502
+ el.attr("opacity", val === tagValue ? 1 : FADE_OPACITY);
19503
+ });
19504
+ g.selectAll(".tl-legend-item, .tl-lane-header").attr(
19505
+ "opacity",
19506
+ FADE_OPACITY
19507
+ );
19508
+ g.selectAll(".tl-marker").attr("opacity", FADE_OPACITY);
19509
+ g.selectAll(".tl-tag-legend-entry").each(function() {
19510
+ const el = d3Selection12.select(this);
19511
+ const entryValue = el.attr("data-legend-entry");
19512
+ if (entryValue === "__group__") return;
19513
+ const entryGroup = el.attr("data-tag-group");
19514
+ el.attr("opacity", entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY);
19515
+ });
19516
+ }
19517
+ function setTagAttrs(evG, ev) {
19518
+ for (const [key, value] of Object.entries(ev.metadata)) {
19519
+ evG.attr(`data-tag-${key}`, value.toLowerCase());
19520
+ }
19521
+ }
19522
+ const tagLegendReserve = parsed.timelineTagGroups.length > 0 ? 36 : 0;
19261
19523
  if (isVertical) {
19262
19524
  if (timelineSort === "group" && timelineGroups.length > 0) {
19263
19525
  const groupNames = timelineGroups.map((gr) => gr.name);
@@ -19269,7 +19531,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19269
19531
  const scaleMargin = timelineScale ? 40 : 0;
19270
19532
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19271
19533
  const margin = {
19272
- top: 104 + markerMargin,
19534
+ top: 104 + markerMargin + tagLegendReserve,
19273
19535
  right: 40 + scaleMargin,
19274
19536
  bottom: 40,
19275
19537
  left: 60 + scaleMargin
@@ -19345,6 +19607,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19345
19607
  }).on("click", () => {
19346
19608
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19347
19609
  });
19610
+ setTagAttrs(evG, ev);
19348
19611
  if (ev.endDate) {
19349
19612
  const y2 = yScale(parseTimelineDate(ev.endDate));
19350
19613
  const rectH = Math.max(y2 - y, 4);
@@ -19371,7 +19634,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19371
19634
  const scaleMargin = timelineScale ? 40 : 0;
19372
19635
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19373
19636
  const margin = {
19374
- top: 104 + markerMargin,
19637
+ top: 104 + markerMargin + tagLegendReserve,
19375
19638
  right: 200,
19376
19639
  bottom: 40,
19377
19640
  left: 60 + scaleMargin
@@ -19451,6 +19714,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19451
19714
  }).on("click", () => {
19452
19715
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19453
19716
  });
19717
+ setTagAttrs(evG, ev);
19454
19718
  if (ev.endDate) {
19455
19719
  const y2 = yScale(parseTimelineDate(ev.endDate));
19456
19720
  const rectH = Math.max(y2 - y, 4);
@@ -19503,7 +19767,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19503
19767
  const dynamicLeftMargin = Math.max(120, maxGroupNameLen * 7 + 30);
19504
19768
  const baseTopMargin = title ? 50 : 20;
19505
19769
  const margin = {
19506
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin,
19770
+ top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
19507
19771
  right: 40,
19508
19772
  bottom: 40 + scaleMargin,
19509
19773
  left: dynamicLeftMargin
@@ -19606,6 +19870,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19606
19870
  }).on("click", () => {
19607
19871
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19608
19872
  });
19873
+ setTagAttrs(evG, ev);
19609
19874
  if (ev.endDate) {
19610
19875
  const x2 = xScale(parseTimelineDate(ev.endDate));
19611
19876
  const rectW = Math.max(x2 - x, 4);
@@ -19647,7 +19912,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19647
19912
  const scaleMargin = timelineScale ? 24 : 0;
19648
19913
  const markerMargin = timelineMarkers.length > 0 ? 30 : 0;
19649
19914
  const margin = {
19650
- top: 104 + (timelineScale ? 40 : 0) + markerMargin,
19915
+ top: 104 + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
19651
19916
  right: 40,
19652
19917
  bottom: 40 + scaleMargin,
19653
19918
  left: 60
@@ -19743,6 +20008,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19743
20008
  }).on("click", () => {
19744
20009
  if (onClickItem && ev.lineNumber) onClickItem(ev.lineNumber);
19745
20010
  });
20011
+ setTagAttrs(evG, ev);
19746
20012
  if (ev.endDate) {
19747
20013
  const x2 = xScale(parseTimelineDate(ev.endDate));
19748
20014
  const rectW = Math.max(x2 - x, 4);
@@ -19778,6 +20044,124 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19778
20044
  }
19779
20045
  });
19780
20046
  }
20047
+ if (parsed.timelineTagGroups.length > 0) {
20048
+ const LG_HEIGHT = 28;
20049
+ const LG_PILL_PAD = 16;
20050
+ const LG_PILL_FONT_SIZE = 11;
20051
+ const LG_PILL_FONT_W = LG_PILL_FONT_SIZE * 0.6;
20052
+ const LG_CAPSULE_PAD = 4;
20053
+ const LG_DOT_R = 4;
20054
+ const LG_ENTRY_FONT_SIZE = 10;
20055
+ const LG_ENTRY_FONT_W = LG_ENTRY_FONT_SIZE * 0.6;
20056
+ const LG_ENTRY_DOT_GAP = 4;
20057
+ const LG_ENTRY_TRAIL = 8;
20058
+ const LG_GROUP_GAP = 12;
20059
+ const mainSvg = d3Selection12.select(container).select("svg");
20060
+ const mainG = mainSvg.select("g");
20061
+ if (!mainSvg.empty() && !mainG.empty()) {
20062
+ let drawLegend2 = function() {
20063
+ mainSvg.selectAll(".tl-tag-legend-group").remove();
20064
+ const totalW = legendGroups.reduce((s, lg) => {
20065
+ const isActive = currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20066
+ return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
20067
+ }, 0) + (legendGroups.length - 1) * LG_GROUP_GAP;
20068
+ let cx = (width - totalW) / 2;
20069
+ for (const lg of legendGroups) {
20070
+ const isActive = currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20071
+ const pillLabel = lg.group.name;
20072
+ const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
20073
+ 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", () => {
20074
+ const groupKey = lg.group.name.toLowerCase();
20075
+ currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
20076
+ drawLegend2();
20077
+ recolorEvents2();
20078
+ });
20079
+ if (isActive) {
20080
+ gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
20081
+ }
20082
+ const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
20083
+ const pillYOff = isActive ? LG_CAPSULE_PAD : 0;
20084
+ const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
20085
+ 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);
20086
+ if (isActive) {
20087
+ 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);
20088
+ }
20089
+ 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);
20090
+ if (isActive) {
20091
+ let entryX = pillXOff + pillWidth + 4;
20092
+ for (const entry of lg.group.entries) {
20093
+ const tagKey = lg.group.name.toLowerCase();
20094
+ const tagVal = entry.value.toLowerCase();
20095
+ 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) => {
20096
+ event.stopPropagation();
20097
+ fadeToTagValue(mainG, tagKey, tagVal);
20098
+ mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
20099
+ const el = d3Selection12.select(this);
20100
+ const ev = el.attr("data-legend-entry");
20101
+ if (ev === "__group__") return;
20102
+ const eg = el.attr("data-tag-group");
20103
+ el.attr("opacity", eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY);
20104
+ });
20105
+ }).on("mouseleave", (event) => {
20106
+ event.stopPropagation();
20107
+ fadeReset(mainG);
20108
+ mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
20109
+ }).on("click", (event) => {
20110
+ event.stopPropagation();
20111
+ });
20112
+ entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
20113
+ const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
20114
+ 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);
20115
+ entryX = textX + entry.value.length * LG_ENTRY_FONT_W + LG_ENTRY_TRAIL;
20116
+ }
20117
+ }
20118
+ cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
20119
+ }
20120
+ }, recolorEvents2 = function() {
20121
+ mainG.selectAll(".tl-event").each(function() {
20122
+ const el = d3Selection12.select(this);
20123
+ const lineNum = el.attr("data-line-number");
20124
+ const ev = lineNum ? eventByLine.get(lineNum) : void 0;
20125
+ if (!ev) return;
20126
+ let color;
20127
+ if (currentActiveGroup) {
20128
+ const tagColor = resolveTagColor(
20129
+ ev.metadata,
20130
+ parsed.timelineTagGroups,
20131
+ currentActiveGroup
20132
+ );
20133
+ color = tagColor ?? (ev.group && groupColorMap.has(ev.group) ? groupColorMap.get(ev.group) : textColor);
20134
+ } else {
20135
+ color = ev.group && groupColorMap.has(ev.group) ? groupColorMap.get(ev.group) : textColor;
20136
+ }
20137
+ el.selectAll("rect").attr("fill", color);
20138
+ el.selectAll("circle:not(.tl-event-point-outline)").attr("fill", color);
20139
+ });
20140
+ };
20141
+ var drawLegend = drawLegend2, recolorEvents = recolorEvents2;
20142
+ const legendY = title ? 50 : 10;
20143
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20144
+ const legendGroups = parsed.timelineTagGroups.map((g) => {
20145
+ const pillW = g.name.length * LG_PILL_FONT_W + LG_PILL_PAD;
20146
+ let entryX = LG_CAPSULE_PAD + pillW + 4;
20147
+ for (const entry of g.entries) {
20148
+ const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
20149
+ entryX = textX + entry.value.length * LG_ENTRY_FONT_W + LG_ENTRY_TRAIL;
20150
+ }
20151
+ return {
20152
+ group: g,
20153
+ minifiedWidth: pillW,
20154
+ expandedWidth: entryX + LG_CAPSULE_PAD
20155
+ };
20156
+ });
20157
+ let currentActiveGroup = activeTagGroup ?? null;
20158
+ const eventByLine = /* @__PURE__ */ new Map();
20159
+ for (const ev of timelineEvents) {
20160
+ eventByLine.set(String(ev.lineNumber), ev);
20161
+ }
20162
+ drawLegend2();
20163
+ }
20164
+ }
19781
20165
  }
19782
20166
  function getRotateFn(mode) {
19783
20167
  if (mode === "mixed") return () => Math.random() > 0.5 ? 0 : 90;
@@ -20763,8 +21147,10 @@ var init_d3 = __esm({
20763
21147
  init_branding();
20764
21148
  init_colors();
20765
21149
  init_palettes();
21150
+ init_color_utils();
20766
21151
  init_diagnostics();
20767
21152
  init_parsing();
21153
+ init_tag_groups();
20768
21154
  DEFAULT_CLOUD_OPTIONS = {
20769
21155
  rotate: "none",
20770
21156
  max: 0,
@@ -21623,6 +22009,9 @@ function encodeDiagramUrl(dsl, options) {
21623
22009
  if (options?.viewState?.activeTagGroup) {
21624
22010
  hash += `&tag=${encodeURIComponent(options.viewState.activeTagGroup)}`;
21625
22011
  }
22012
+ if (options?.viewState?.collapsedGroups?.length) {
22013
+ hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(","))}`;
22014
+ }
21626
22015
  return { url: `${baseUrl}?${hash}#${hash}` };
21627
22016
  }
21628
22017
  function decodeDiagramUrl(hash) {
@@ -21643,6 +22032,9 @@ function decodeDiagramUrl(hash) {
21643
22032
  if (key === "tag" && val) {
21644
22033
  viewState.activeTagGroup = val;
21645
22034
  }
22035
+ if (key === "cg" && val) {
22036
+ viewState.collapsedGroups = val.split(",").filter(Boolean);
22037
+ }
21646
22038
  }
21647
22039
  if (payload.startsWith("dgmo=")) {
21648
22040
  payload = payload.slice(5);