@diagrammo/dgmo 0.8.3 → 0.8.5

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.
Files changed (122) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +452 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +188 -185
  9. package/dist/editor.cjs +338 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
@@ -6,24 +6,22 @@ import type { PaletteColors } from '../palettes';
6
6
  import { resolveColor } from '../colors';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
8
  import type { TagGroup } from '../utils/tag-groups';
9
- import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
9
+ import {
10
+ isTagBlockHeading,
11
+ matchTagBlockHeading,
12
+ validateTagValues,
13
+ } from '../utils/tag-groups';
10
14
  import {
11
15
  measureIndent,
12
16
  extractColor,
13
17
  parsePipeMetadata,
14
18
  inferArrowColor,
15
19
  MULTIPLE_PIPE_ERROR,
16
- TITLE_RE,
17
- OPTION_RE,
18
20
  parseFirstLine,
19
21
  OPTION_NOCOLON_RE,
20
22
  ALL_CHART_TYPES,
21
23
  } from '../utils/parsing';
22
- import type {
23
- SitemapNode,
24
- SitemapDirection,
25
- ParsedSitemap,
26
- } from './types';
24
+ import type { SitemapNode, ParsedSitemap } from './types';
27
25
 
28
26
  // ============================================================
29
27
  // Regexes
@@ -46,7 +44,7 @@ const BARE_ARROW_RE = /^->\s*(.+)$/;
46
44
 
47
45
  function parseArrowLine(
48
46
  trimmed: string,
49
- palette?: PaletteColors,
47
+ palette?: PaletteColors
50
48
  ): { label?: string; color?: string; target: string } | null {
51
49
  // Bare arrow: -> Target
52
50
  const bareMatch = trimmed.match(BARE_ARROW_RE);
@@ -59,7 +57,7 @@ function parseArrowLine(
59
57
  if (arrowMatch) {
60
58
  const label = arrowMatch[1]?.trim() || undefined;
61
59
  let color = arrowMatch[2]
62
- ? resolveColor(arrowMatch[2].trim(), palette) ?? undefined
60
+ ? (resolveColor(arrowMatch[2].trim(), palette) ?? undefined)
63
61
  : undefined;
64
62
  if (label && !color) {
65
63
  color = inferArrowColor(label);
@@ -90,7 +88,7 @@ export function looksLikeSitemap(content: string): boolean {
90
88
  if (!trimmed || trimmed.startsWith('//')) continue;
91
89
 
92
90
  // Skip header lines
93
- if (parseFirstLine(trimmed) || TITLE_RE.test(trimmed)) continue;
91
+ if (parseFirstLine(trimmed)) continue;
94
92
  if (isTagBlockHeading(trimmed)) continue;
95
93
 
96
94
  if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
@@ -106,7 +104,7 @@ export function looksLikeSitemap(content: string): boolean {
106
104
  // Exclude flowchart: flowchart arrows connect shaped nodes like (X) -> [Y]
107
105
  // Sitemap arrows are indented under a parent node, target is plain text
108
106
  const hasFlowchartShapes =
109
- /[\])][ \t]*-.*->/.test(content) || /->[ \t]*[\[(<\/]/.test(content);
107
+ /[\])][ \t]*-.*->/.test(content) || /->[ \t]*[[(</]/.test(content);
110
108
 
111
109
  return !hasFlowchartShapes;
112
110
  }
@@ -117,7 +115,7 @@ export function looksLikeSitemap(content: string): boolean {
117
115
 
118
116
  export function parseSitemap(
119
117
  content: string,
120
- palette?: PaletteColors,
118
+ palette?: PaletteColors
121
119
  ): ParsedSitemap {
122
120
  const result: ParsedSitemap = {
123
121
  title: null,
@@ -231,7 +229,10 @@ export function parseSitemap(
231
229
  lineNumber,
232
230
  };
233
231
  if (tagBlockMatch.alias) {
234
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
232
+ aliasMap.set(
233
+ tagBlockMatch.alias.toLowerCase(),
234
+ tagBlockMatch.name.toLowerCase()
235
+ );
235
236
  }
236
237
  result.tagGroups.push(currentTagGroup);
237
238
  continue;
@@ -239,8 +240,13 @@ export function parseSitemap(
239
240
 
240
241
  // Generic header options (space-separated, before content/tag groups)
241
242
  // Skip lines with `|` (pipe metadata) or `->` (arrows) — those are content
242
- if (!contentStarted && !currentTagGroup && measureIndent(line) === 0
243
- && !trimmed.includes('|') && !trimmed.includes('->')) {
243
+ if (
244
+ !contentStarted &&
245
+ !currentTagGroup &&
246
+ measureIndent(line) === 0 &&
247
+ !trimmed.includes('|') &&
248
+ !trimmed.includes('->')
249
+ ) {
244
250
  // Bare boolean: direction-tb
245
251
  if (/^direction-tb$/i.test(trimmed)) {
246
252
  result.direction = 'TB';
@@ -263,7 +269,7 @@ export function parseSitemap(
263
269
  if (!color) {
264
270
  pushError(
265
271
  lineNumber,
266
- `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
272
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
267
273
  );
268
274
  continue;
269
275
  }
@@ -279,7 +285,7 @@ export function parseSitemap(
279
285
  continue;
280
286
  }
281
287
  // Non-indented line after tag group — fall through to content
282
- currentTagGroup = null;
288
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
283
289
  }
284
290
 
285
291
  // --- Content phase ---
@@ -313,8 +319,9 @@ export function parseSitemap(
313
319
  const containerMatch = trimmed.match(CONTAINER_RE);
314
320
 
315
321
  // Check for metadata syntax: key: value
316
- const metadataMatch =
317
- trimmed.includes('|') ? null : trimmed.match(METADATA_RE);
322
+ const metadataMatch = trimmed.includes('|')
323
+ ? null
324
+ : trimmed.match(METADATA_RE);
318
325
 
319
326
  if (containerMatch) {
320
327
  const rawLabel = containerMatch[1].trim();
@@ -326,7 +333,10 @@ export function parseSitemap(
326
333
  if (pipeStr) {
327
334
  // Build segments array compatible with parsePipeMetadata (first element is label, rest are pipe parts)
328
335
  const pipeSegments = ['', pipeStr];
329
- Object.assign(containerMetadata, parsePipeMetadata(pipeSegments, aliasMap));
336
+ Object.assign(
337
+ containerMetadata,
338
+ parsePipeMetadata(pipeSegments, aliasMap)
339
+ );
330
340
  }
331
341
 
332
342
  containerCounter++;
@@ -358,7 +368,14 @@ export function parseSitemap(
358
368
  } else if (metadataMatch && indentStack.length === 0) {
359
369
  // Could be a node label containing ':'
360
370
  if (indent === 0) {
361
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
371
+ const node = parseNodeLabel(
372
+ trimmed,
373
+ lineNumber,
374
+ palette,
375
+ ++nodeCounter,
376
+ aliasMap,
377
+ pushWarning
378
+ );
362
379
  attachNode(node, indent, indentStack, result);
363
380
  labelToNode.set(node.label.toLowerCase(), node);
364
381
  } else {
@@ -366,7 +383,14 @@ export function parseSitemap(
366
383
  }
367
384
  } else {
368
385
  // Node label — possibly with pipe-delimited metadata
369
- const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
386
+ const node = parseNodeLabel(
387
+ trimmed,
388
+ lineNumber,
389
+ palette,
390
+ ++nodeCounter,
391
+ aliasMap,
392
+ pushWarning
393
+ );
370
394
  attachNode(node, indent, indentStack, result);
371
395
  labelToNode.set(node.label.toLowerCase(), node);
372
396
  }
@@ -409,7 +433,11 @@ export function parseSitemap(
409
433
  validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
410
434
  }
411
435
 
412
- if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
436
+ if (
437
+ result.roots.length === 0 &&
438
+ result.tagGroups.length === 0 &&
439
+ !result.error
440
+ ) {
413
441
  const diag = makeDgmoError(1, 'No pages found in sitemap');
414
442
  result.diagnostics.push(diag);
415
443
  result.error = formatDgmoError(diag);
@@ -428,12 +456,16 @@ function parseNodeLabel(
428
456
  palette: PaletteColors | undefined,
429
457
  counter: number,
430
458
  aliasMap: Map<string, string> = new Map(),
431
- warnFn?: (line: number, msg: string) => void,
459
+ warnFn?: (line: number, msg: string) => void
432
460
  ): SitemapNode {
433
461
  const segments = trimmed.split('|').map((s) => s.trim());
434
462
  const rawLabel = segments[0];
435
463
  const { label, color } = extractColor(rawLabel, palette);
436
- const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined);
464
+ const metadata = parsePipeMetadata(
465
+ segments,
466
+ aliasMap,
467
+ warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined
468
+ );
437
469
 
438
470
  return {
439
471
  id: `node-${counter}`,
@@ -451,7 +483,7 @@ function attachNode(
451
483
  node: SitemapNode,
452
484
  indent: number,
453
485
  indentStack: { node: SitemapNode; indent: number }[],
454
- result: ParsedSitemap,
486
+ result: ParsedSitemap
455
487
  ): void {
456
488
  // Pop stack entries with indent >= current indent
457
489
  while (indentStack.length > 0) {
@@ -464,7 +496,11 @@ function attachNode(
464
496
  const parent = indentStack[indentStack.length - 1].node;
465
497
  node.parentId = parent.id;
466
498
  // Cascade container metadata to child nodes (child overrides on conflict)
467
- if (parent.isContainer && Object.keys(parent.metadata).length > 0 && !node.isContainer) {
499
+ if (
500
+ parent.isContainer &&
501
+ Object.keys(parent.metadata).length > 0 &&
502
+ !node.isContainer
503
+ ) {
468
504
  node.metadata = { ...parent.metadata, ...node.metadata };
469
505
  }
470
506
  parent.children.push(node);
@@ -477,7 +513,7 @@ function attachNode(
477
513
 
478
514
  function findParentNode(
479
515
  indent: number,
480
- indentStack: { node: SitemapNode; indent: number }[],
516
+ indentStack: { node: SitemapNode; indent: number }[]
481
517
  ): SitemapNode | null {
482
518
  for (let i = indentStack.length - 1; i >= 0; i--) {
483
519
  if (indentStack[i].indent < indent) {
@@ -34,7 +34,7 @@ const ARROW_CHARS = ['->', '~>'];
34
34
  * - `null` if not a labeled arrow (caller should fall through to bare patterns)
35
35
  */
36
36
  export function parseArrow(
37
- line: string,
37
+ line: string
38
38
  ): ParsedArrow | { error: string } | null {
39
39
  // Check bidi patterns first — return error
40
40
  if (BIDI_SYNC_RE.test(line) || BIDI_ASYNC_RE.test(line)) {
@@ -47,8 +47,7 @@ export function parseArrow(
47
47
  // Check deprecated return arrow patterns — return error
48
48
  if (RETURN_SYNC_LABELED_RE.test(line) || RETURN_ASYNC_LABELED_RE.test(line)) {
49
49
  const m =
50
- line.match(RETURN_SYNC_LABELED_RE) ??
51
- line.match(RETURN_ASYNC_LABELED_RE);
50
+ line.match(RETURN_SYNC_LABELED_RE) ?? line.match(RETURN_ASYNC_LABELED_RE);
52
51
  const from = m![3];
53
52
  const to = m![1];
54
53
  const label = m![2].trim();
@@ -93,22 +92,3 @@ export function parseArrow(
93
92
 
94
93
  return null;
95
94
  }
96
-
97
- /**
98
- * Match an arrow segment and extract label + async flag.
99
- * Handles: `->`, `-label->`, `~>`, `~label~>`.
100
- * Returns null if no arrow pattern matched.
101
- */
102
- export function matchArrowLabel(segment: string): { label: string; async: boolean } | null {
103
- // Async labeled: ~label~>
104
- const asyncLabeled = segment.match(/^~(.+?)~>$/);
105
- if (asyncLabeled) return { label: asyncLabeled[1].trim(), async: true };
106
- // Async bare: ~>
107
- if (segment.trim() === '~>') return { label: '', async: true };
108
- // Sync labeled: -label->
109
- const syncLabeled = segment.match(/^-(.+?)->$/);
110
- if (syncLabeled) return { label: syncLabeled[1].trim(), async: false };
111
- // Sync bare: ->
112
- if (segment.trim() === '->') return { label: '', async: false };
113
- return null;
114
- }
@@ -2,12 +2,26 @@
2
2
  // Duration & Business Day Arithmetic
3
3
  // ============================================================
4
4
 
5
- import type { Duration, DurationUnit, GanttHolidays, Offset, Weekday } from '../gantt/types';
5
+ import type {
6
+ Duration,
7
+ DurationUnit,
8
+ GanttHolidays,
9
+ Offset,
10
+ Weekday,
11
+ } from '../gantt/types';
6
12
 
7
13
  // ── Weekday constants ─────────────────────────────────────
8
14
 
9
15
  /** JS Date.getDay() → Weekday mapping (0=Sun, 1=Mon, ..., 6=Sat) */
10
- const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
16
+ const JS_DAY_TO_WEEKDAY: Weekday[] = [
17
+ 'sun',
18
+ 'mon',
19
+ 'tue',
20
+ 'wed',
21
+ 'thu',
22
+ 'fri',
23
+ 'sat',
24
+ ];
11
25
 
12
26
  /**
13
27
  * Check if a date is a workday (not a weekend and not a holiday).
@@ -15,7 +29,7 @@ const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri',
15
29
  export function isWorkday(
16
30
  date: Date,
17
31
  workweek: Weekday[],
18
- holidaySet: Set<string>,
32
+ holidaySet: Set<string>
19
33
  ): boolean {
20
34
  const dayName = JS_DAY_TO_WEEKDAY[date.getDay()];
21
35
  if (!workweek.includes(dayName)) return false;
@@ -68,7 +82,7 @@ export function addBusinessDays(
68
82
  count: number,
69
83
  workweek: Weekday[],
70
84
  holidaySet: Set<string>,
71
- direction: 1 | -1 = 1,
85
+ direction: 1 | -1 = 1
72
86
  ): Date {
73
87
  const days = Math.round(Math.abs(count));
74
88
  if (days === 0) return new Date(startDate);
@@ -97,13 +111,19 @@ export function addGanttDuration(
97
111
  duration: Duration,
98
112
  holidays: GanttHolidays,
99
113
  holidaySet: Set<string>,
100
- direction: 1 | -1 = 1,
114
+ direction: 1 | -1 = 1
101
115
  ): Date {
102
116
  const { amount, unit } = duration;
103
117
 
104
118
  switch (unit) {
105
119
  case 'bd':
106
- return addBusinessDays(startDate, amount, holidays.workweek, holidaySet, direction);
120
+ return addBusinessDays(
121
+ startDate,
122
+ amount,
123
+ holidays.workweek,
124
+ holidaySet,
125
+ direction
126
+ );
107
127
 
108
128
  case 'd': {
109
129
  const result = new Date(startDate);
@@ -119,8 +139,10 @@ export function addGanttDuration(
119
139
 
120
140
  case 'm': {
121
141
  const result = new Date(startDate);
122
- const wholeMonths = direction === -1 ? Math.round(amount) : Math.floor(amount);
123
- const fractionalDays = direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
142
+ const wholeMonths =
143
+ direction === -1 ? Math.round(amount) : Math.floor(amount);
144
+ const fractionalDays =
145
+ direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
124
146
  result.setMonth(result.getMonth() + wholeMonths * direction);
125
147
  if (fractionalDays > 0) {
126
148
  result.setDate(result.getDate() + fractionalDays * direction);
@@ -131,8 +153,10 @@ export function addGanttDuration(
131
153
  case 'q': {
132
154
  const result = new Date(startDate);
133
155
  const totalMonths = amount * 3;
134
- const wholeMonths = direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
135
- const fractionalDays = direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
156
+ const wholeMonths =
157
+ direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
158
+ const fractionalDays =
159
+ direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
136
160
  result.setMonth(result.getMonth() + wholeMonths * direction);
137
161
  if (fractionalDays > 0) {
138
162
  result.setDate(result.getDate() + fractionalDays * direction);
@@ -142,8 +166,10 @@ export function addGanttDuration(
142
166
 
143
167
  case 'y': {
144
168
  const result = new Date(startDate);
145
- const wholeYears = direction === -1 ? Math.round(amount) : Math.floor(amount);
146
- const fractionalMonths = direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
169
+ const wholeYears =
170
+ direction === -1 ? Math.round(amount) : Math.floor(amount);
171
+ const fractionalMonths =
172
+ direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
147
173
  result.setFullYear(result.getFullYear() + wholeYears * direction);
148
174
  if (fractionalMonths > 0) {
149
175
  result.setMonth(result.getMonth() + fractionalMonths * direction);
@@ -221,7 +247,7 @@ export function parseGanttDate(s: string): Date {
221
247
  }
222
248
  }
223
249
 
224
- const parts = datePart.split('-').map(p => parseInt(p, 10));
250
+ const parts = datePart.split('-').map((p) => parseInt(p, 10));
225
251
  const year = parts[0];
226
252
  const month = parts.length >= 2 ? parts[1] - 1 : 0; // JS months are 0-based
227
253
  const day = parts.length >= 3 ? parts[2] : 1;
@@ -238,11 +264,3 @@ export function formatGanttDate(date: Date): string {
238
264
  if (h === 0 && m === 0) return dateStr;
239
265
  return `${dateStr} ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
240
266
  }
241
-
242
- /**
243
- * Calculate the difference in calendar days between two dates.
244
- */
245
- export function daysBetween(a: Date, b: Date): number {
246
- const msPerDay = 86400000;
247
- return Math.round((b.getTime() - a.getTime()) / msPerDay);
248
- }
@@ -6,11 +6,9 @@
6
6
  export const LEGEND_HEIGHT = 28;
7
7
  export const LEGEND_PILL_PAD = 16;
8
8
  export const LEGEND_PILL_FONT_SIZE = 11;
9
- export const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
10
9
  export const LEGEND_CAPSULE_PAD = 4;
11
10
  export const LEGEND_DOT_R = 4;
12
11
  export const LEGEND_ENTRY_FONT_SIZE = 10;
13
- export const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
14
12
  export const LEGEND_ENTRY_DOT_GAP = 4;
15
13
  export const LEGEND_ENTRY_TRAIL = 8;
16
14
  export const LEGEND_GROUP_GAP = 12;
@@ -11,14 +11,41 @@ import type { PaletteColors } from '../palettes';
11
11
  /** Complete set of recognized chart type identifiers. */
12
12
  export const ALL_CHART_TYPES = new Set([
13
13
  // data charts
14
- 'bar', 'line', 'pie', 'doughnut', 'area', 'polar-area', 'radar',
15
- 'bar-stacked', 'multi-line', 'scatter', 'sankey', 'chord', 'function',
16
- 'heatmap', 'funnel',
14
+ 'bar',
15
+ 'line',
16
+ 'pie',
17
+ 'doughnut',
18
+ 'area',
19
+ 'polar-area',
20
+ 'radar',
21
+ 'bar-stacked',
22
+ 'multi-line',
23
+ 'scatter',
24
+ 'sankey',
25
+ 'chord',
26
+ 'function',
27
+ 'heatmap',
28
+ 'funnel',
17
29
  // visualizations
18
- 'slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant',
30
+ 'slope',
31
+ 'wordcloud',
32
+ 'arc',
33
+ 'timeline',
34
+ 'venn',
35
+ 'quadrant',
19
36
  // diagrams
20
- 'sequence', 'flowchart', 'class', 'er', 'org', 'kanban', 'c4',
21
- 'initiative-status', 'state', 'sitemap', 'infra', 'gantt',
37
+ 'sequence',
38
+ 'flowchart',
39
+ 'class',
40
+ 'er',
41
+ 'org',
42
+ 'kanban',
43
+ 'c4',
44
+ 'initiative-status',
45
+ 'state',
46
+ 'sitemap',
47
+ 'infra',
48
+ 'gantt',
22
49
  ]);
23
50
 
24
51
  /** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */
@@ -38,7 +65,7 @@ export const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
38
65
  /** Extract an optional trailing color suffix from a label, resolving via palette. */
39
66
  export function extractColor(
40
67
  label: string,
41
- palette?: PaletteColors,
68
+ palette?: PaletteColors
42
69
  ): { label: string; color?: string } {
43
70
  const m = label.match(COLOR_SUFFIX_RE);
44
71
  if (!m) return { label };
@@ -49,16 +76,9 @@ export function extractColor(
49
76
  };
50
77
  }
51
78
 
52
- /** @deprecated Matches `title: <text>` header lines. Remove after all parsers migrate. */
53
- export const TITLE_RE = /^title\s*:\s*(.+)/i;
54
-
55
- /** @deprecated Matches `option: value` header lines. Remove after all parsers migrate. */
56
- export const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
57
-
58
79
  /** Matches `option value` header lines (space-separated, no colon). */
59
80
  export const OPTION_NOCOLON_RE = /^([a-z][a-z0-9-]*)\s+(.+)$/i;
60
81
 
61
-
62
82
  // ── New shared utilities ─────────────────────────────────────
63
83
 
64
84
  /**
@@ -68,7 +88,7 @@ export const OPTION_NOCOLON_RE = /^([a-z][a-z0-9-]*)\s+(.+)$/i;
68
88
  * Returns `null` if the first token is not a recognized chart type.
69
89
  */
70
90
  export function parseFirstLine(
71
- line: string,
91
+ line: string
72
92
  ): { chartType: string; title: string | undefined } | null {
73
93
  const trimmed = line.trim();
74
94
  if (!trimmed || trimmed.startsWith('//')) return null;
@@ -81,7 +101,10 @@ export function parseFirstLine(
81
101
  }
82
102
  const firstToken = trimmed.substring(0, spaceIdx).toLowerCase();
83
103
  if (!ALL_CHART_TYPES.has(firstToken)) return null;
84
- return { chartType: firstToken, title: trimmed.substring(spaceIdx + 1).trim() || undefined };
104
+ return {
105
+ chartType: firstToken,
106
+ title: trimmed.substring(spaceIdx + 1).trim() || undefined,
107
+ };
85
108
  }
86
109
 
87
110
  /** Result of `prescanOptions()` — options collected from a two-pass scan. */
@@ -112,7 +135,7 @@ export interface PrescanResult {
112
135
  export function prescanOptions(
113
136
  lines: string[],
114
137
  knownOptions: Set<string>,
115
- knownBooleans: Set<string> = new Set(),
138
+ knownBooleans: Set<string> = new Set()
116
139
  ): PrescanResult {
117
140
  const options: Record<string, string> = {};
118
141
  const booleans = new Set<string>();
@@ -127,12 +150,15 @@ export function prescanOptions(
127
150
 
128
151
  // Strip inline comments
129
152
  const commentIdx = trimmed.indexOf(' //');
130
- const effective = commentIdx >= 0 ? trimmed.substring(0, commentIdx).trim() : trimmed;
153
+ const effective =
154
+ commentIdx >= 0 ? trimmed.substring(0, commentIdx).trim() : trimmed;
131
155
  if (!effective) continue;
132
156
 
133
157
  // Extract first token
134
158
  const spaceIdx = effective.indexOf(' ');
135
- const firstToken = (spaceIdx === -1 ? effective : effective.substring(0, spaceIdx)).toLowerCase();
159
+ const firstToken = (
160
+ spaceIdx === -1 ? effective : effective.substring(0, spaceIdx)
161
+ ).toLowerCase();
136
162
 
137
163
  // Check for bare boolean (presence = on)
138
164
  if (spaceIdx === -1 && knownBooleans.has(firstToken)) {
@@ -185,8 +211,10 @@ export function normalizeGroupedNumber(token: string): string | null {
185
211
  */
186
212
  export function stripQuotes(token: string): string {
187
213
  if (token.length >= 2) {
188
- if ((token[0] === '"' && token[token.length - 1] === '"') ||
189
- (token[0] === "'" && token[token.length - 1] === "'")) {
214
+ if (
215
+ (token[0] === '"' && token[token.length - 1] === '"') ||
216
+ (token[0] === "'" && token[token.length - 1] === "'")
217
+ ) {
190
218
  return token.substring(1, token.length - 1);
191
219
  }
192
220
  }
@@ -203,7 +231,10 @@ export function tokenizeQuoteAware(input: string): string[] {
203
231
  let i = 0;
204
232
  while (i < input.length) {
205
233
  // Skip whitespace
206
- if (input[i] === ' ' || input[i] === '\t') { i++; continue; }
234
+ if (input[i] === ' ' || input[i] === '\t') {
235
+ i++;
236
+ continue;
237
+ }
207
238
 
208
239
  // Quoted token
209
240
  if (input[i] === '"' || input[i] === "'") {
@@ -236,7 +267,7 @@ export function tokenizeQuoteAware(input: string): string[] {
236
267
  */
237
268
  export function collectIndentedValues(
238
269
  lines: string[],
239
- startIndex: number,
270
+ startIndex: number
240
271
  ): { values: string[]; lineNumbers: number[]; newIndex: number } {
241
272
  const values: string[] = [];
242
273
  const lineNumbers: number[] = [];
@@ -268,7 +299,7 @@ export function parseSeriesNames(
268
299
  value: string,
269
300
  lines: string[],
270
301
  lineIndex: number,
271
- palette?: PaletteColors,
302
+ palette?: PaletteColors
272
303
  ): {
273
304
  series: string;
274
305
  names: string[];
@@ -279,10 +310,13 @@ export function parseSeriesNames(
279
310
  let rawNames: string[];
280
311
  let series: string;
281
312
  let newIndex = lineIndex;
282
- let nameLineNumbers: number[] = [];
313
+ let nameLineNumbers: number[] = []; // eslint-disable-line no-useless-assignment
283
314
  if (value) {
284
315
  series = value;
285
- rawNames = value.split(',').map((s) => s.trim()).filter(Boolean);
316
+ rawNames = value
317
+ .split(',')
318
+ .map((s) => s.trim())
319
+ .filter(Boolean);
286
320
  // Inline series names all share the same line number
287
321
  nameLineNumbers = rawNames.map(() => lineIndex + 1);
288
322
  } else {
@@ -305,8 +339,6 @@ export function parseSeriesNames(
305
339
  return { series, names, nameColors, nameLineNumbers, newIndex };
306
340
  }
307
341
 
308
-
309
-
310
342
  /**
311
343
  * Infer arrow color from label text.
312
344
  * Returns a named palette color or undefined if no inference applies.
@@ -315,9 +347,21 @@ export function parseSeriesNames(
315
347
  export function inferArrowColor(label: string): string | undefined {
316
348
  const lower = label.toLowerCase();
317
349
  // Green: positive/affirmative
318
- if (lower === 'yes' || lower === 'success' || lower === 'ok' || lower === 'true') return 'green';
350
+ if (
351
+ lower === 'yes' ||
352
+ lower === 'success' ||
353
+ lower === 'ok' ||
354
+ lower === 'true'
355
+ )
356
+ return 'green';
319
357
  // Red: negative/failure
320
- if (lower === 'no' || lower === 'fail' || lower === 'error' || lower === 'false') return 'red';
358
+ if (
359
+ lower === 'no' ||
360
+ lower === 'fail' ||
361
+ lower === 'error' ||
362
+ lower === 'false'
363
+ )
364
+ return 'red';
321
365
  // Orange: uncertain/warning
322
366
  if (lower === 'maybe' || lower === 'warning') return 'orange';
323
367
  return undefined;
@@ -335,7 +379,7 @@ export const MULTIPLE_PIPE_ERROR =
335
379
  export function parsePipeMetadata(
336
380
  segments: string[],
337
381
  aliasMap: Map<string, string> = new Map(),
338
- errorMultiplePipes?: () => void,
382
+ errorMultiplePipes?: () => void
339
383
  ): Record<string, string> {
340
384
  if (segments.length > 2) {
341
385
  if (errorMultiplePipes) errorMultiplePipes();