@diagrammo/dgmo 0.8.3 → 0.8.4
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/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +153 -153
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3336 -1055
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3336 -1055
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +30 -29
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +168 -61
- package/src/completion.ts +378 -183
- package/src/d3.ts +887 -288
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/sitemap/parser.ts
CHANGED
|
@@ -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 {
|
|
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)
|
|
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]*[
|
|
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(
|
|
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 (
|
|
243
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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 (
|
|
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) {
|
package/src/utils/arrows.ts
CHANGED
|
@@ -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
|
-
}
|
package/src/utils/duration.ts
CHANGED
|
@@ -2,12 +2,26 @@
|
|
|
2
2
|
// Duration & Business Day Arithmetic
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
-
import type {
|
|
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[] = [
|
|
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(
|
|
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 =
|
|
123
|
-
|
|
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 =
|
|
135
|
-
|
|
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 =
|
|
146
|
-
|
|
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;
|
package/src/utils/parsing.ts
CHANGED
|
@@ -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',
|
|
15
|
-
'
|
|
16
|
-
'
|
|
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',
|
|
30
|
+
'slope',
|
|
31
|
+
'wordcloud',
|
|
32
|
+
'arc',
|
|
33
|
+
'timeline',
|
|
34
|
+
'venn',
|
|
35
|
+
'quadrant',
|
|
19
36
|
// diagrams
|
|
20
|
-
'sequence',
|
|
21
|
-
'
|
|
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 {
|
|
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 =
|
|
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 = (
|
|
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 (
|
|
189
|
-
|
|
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') {
|
|
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
|
|
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 (
|
|
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 (
|
|
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();
|