@diagrammo/dgmo 0.8.2 → 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 +189 -194
- 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 +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- 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 +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- 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 +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- 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 +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/sitemap/parser.ts
CHANGED
|
@@ -6,26 +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
|
-
normalizeDirection,
|
|
15
18
|
inferArrowColor,
|
|
16
|
-
|
|
17
|
-
CHART_TYPE_RE,
|
|
18
|
-
TITLE_RE,
|
|
19
|
-
OPTION_RE,
|
|
19
|
+
MULTIPLE_PIPE_ERROR,
|
|
20
20
|
parseFirstLine,
|
|
21
21
|
OPTION_NOCOLON_RE,
|
|
22
22
|
ALL_CHART_TYPES,
|
|
23
23
|
} from '../utils/parsing';
|
|
24
|
-
import type {
|
|
25
|
-
SitemapNode,
|
|
26
|
-
SitemapDirection,
|
|
27
|
-
ParsedSitemap,
|
|
28
|
-
} from './types';
|
|
24
|
+
import type { SitemapNode, ParsedSitemap } from './types';
|
|
29
25
|
|
|
30
26
|
// ============================================================
|
|
31
27
|
// Regexes
|
|
@@ -48,7 +44,7 @@ const BARE_ARROW_RE = /^->\s*(.+)$/;
|
|
|
48
44
|
|
|
49
45
|
function parseArrowLine(
|
|
50
46
|
trimmed: string,
|
|
51
|
-
palette?: PaletteColors
|
|
47
|
+
palette?: PaletteColors
|
|
52
48
|
): { label?: string; color?: string; target: string } | null {
|
|
53
49
|
// Bare arrow: -> Target
|
|
54
50
|
const bareMatch = trimmed.match(BARE_ARROW_RE);
|
|
@@ -61,7 +57,7 @@ function parseArrowLine(
|
|
|
61
57
|
if (arrowMatch) {
|
|
62
58
|
const label = arrowMatch[1]?.trim() || undefined;
|
|
63
59
|
let color = arrowMatch[2]
|
|
64
|
-
? resolveColor(arrowMatch[2].trim(), palette) ?? undefined
|
|
60
|
+
? (resolveColor(arrowMatch[2].trim(), palette) ?? undefined)
|
|
65
61
|
: undefined;
|
|
66
62
|
if (label && !color) {
|
|
67
63
|
color = inferArrowColor(label);
|
|
@@ -92,7 +88,7 @@ export function looksLikeSitemap(content: string): boolean {
|
|
|
92
88
|
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
93
89
|
|
|
94
90
|
// Skip header lines
|
|
95
|
-
if (
|
|
91
|
+
if (parseFirstLine(trimmed)) continue;
|
|
96
92
|
if (isTagBlockHeading(trimmed)) continue;
|
|
97
93
|
|
|
98
94
|
if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
|
|
@@ -108,7 +104,7 @@ export function looksLikeSitemap(content: string): boolean {
|
|
|
108
104
|
// Exclude flowchart: flowchart arrows connect shaped nodes like (X) -> [Y]
|
|
109
105
|
// Sitemap arrows are indented under a parent node, target is plain text
|
|
110
106
|
const hasFlowchartShapes =
|
|
111
|
-
/[\])][ \t]*-.*->/.test(content) || /->[ \t]*[
|
|
107
|
+
/[\])][ \t]*-.*->/.test(content) || /->[ \t]*[[(</]/.test(content);
|
|
112
108
|
|
|
113
109
|
return !hasFlowchartShapes;
|
|
114
110
|
}
|
|
@@ -119,12 +115,12 @@ export function looksLikeSitemap(content: string): boolean {
|
|
|
119
115
|
|
|
120
116
|
export function parseSitemap(
|
|
121
117
|
content: string,
|
|
122
|
-
palette?: PaletteColors
|
|
118
|
+
palette?: PaletteColors
|
|
123
119
|
): ParsedSitemap {
|
|
124
120
|
const result: ParsedSitemap = {
|
|
125
121
|
title: null,
|
|
126
122
|
titleLineNumber: null,
|
|
127
|
-
direction: '
|
|
123
|
+
direction: 'LR',
|
|
128
124
|
roots: [],
|
|
129
125
|
edges: [],
|
|
130
126
|
tagGroups: [],
|
|
@@ -226,10 +222,6 @@ export function parseSitemap(
|
|
|
226
222
|
pushError(lineNumber, 'Tag groups must appear before sitemap content');
|
|
227
223
|
continue;
|
|
228
224
|
}
|
|
229
|
-
if (tagBlockMatch.deprecated) {
|
|
230
|
-
pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
225
|
currentTagGroup = {
|
|
234
226
|
name: tagBlockMatch.name,
|
|
235
227
|
alias: tagBlockMatch.alias,
|
|
@@ -237,7 +229,10 @@ export function parseSitemap(
|
|
|
237
229
|
lineNumber,
|
|
238
230
|
};
|
|
239
231
|
if (tagBlockMatch.alias) {
|
|
240
|
-
aliasMap.set(
|
|
232
|
+
aliasMap.set(
|
|
233
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
234
|
+
tagBlockMatch.name.toLowerCase()
|
|
235
|
+
);
|
|
241
236
|
}
|
|
242
237
|
result.tagGroups.push(currentTagGroup);
|
|
243
238
|
continue;
|
|
@@ -245,18 +240,22 @@ export function parseSitemap(
|
|
|
245
240
|
|
|
246
241
|
// Generic header options (space-separated, before content/tag groups)
|
|
247
242
|
// Skip lines with `|` (pipe metadata) or `->` (arrows) — those are content
|
|
248
|
-
if (
|
|
249
|
-
|
|
243
|
+
if (
|
|
244
|
+
!contentStarted &&
|
|
245
|
+
!currentTagGroup &&
|
|
246
|
+
measureIndent(line) === 0 &&
|
|
247
|
+
!trimmed.includes('|') &&
|
|
248
|
+
!trimmed.includes('->')
|
|
249
|
+
) {
|
|
250
|
+
// Bare boolean: direction-tb
|
|
251
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
252
|
+
result.direction = 'TB';
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
250
256
|
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
251
257
|
if (optMatch) {
|
|
252
258
|
const key = optMatch[1].trim().toLowerCase();
|
|
253
|
-
if (key === 'direction' || key === 'orientation') {
|
|
254
|
-
const dir = normalizeDirection(optMatch[2]);
|
|
255
|
-
if (dir) {
|
|
256
|
-
result.direction = dir as SitemapDirection;
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
259
|
result.options[key] = optMatch[2].trim();
|
|
261
260
|
continue;
|
|
262
261
|
}
|
|
@@ -270,7 +269,7 @@ export function parseSitemap(
|
|
|
270
269
|
if (!color) {
|
|
271
270
|
pushError(
|
|
272
271
|
lineNumber,
|
|
273
|
-
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'
|
|
272
|
+
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
|
|
274
273
|
);
|
|
275
274
|
continue;
|
|
276
275
|
}
|
|
@@ -286,7 +285,7 @@ export function parseSitemap(
|
|
|
286
285
|
continue;
|
|
287
286
|
}
|
|
288
287
|
// Non-indented line after tag group — fall through to content
|
|
289
|
-
currentTagGroup = null;
|
|
288
|
+
currentTagGroup = null; // eslint-disable-line no-useless-assignment
|
|
290
289
|
}
|
|
291
290
|
|
|
292
291
|
// --- Content phase ---
|
|
@@ -320,8 +319,9 @@ export function parseSitemap(
|
|
|
320
319
|
const containerMatch = trimmed.match(CONTAINER_RE);
|
|
321
320
|
|
|
322
321
|
// Check for metadata syntax: key: value
|
|
323
|
-
const metadataMatch =
|
|
324
|
-
|
|
322
|
+
const metadataMatch = trimmed.includes('|')
|
|
323
|
+
? null
|
|
324
|
+
: trimmed.match(METADATA_RE);
|
|
325
325
|
|
|
326
326
|
if (containerMatch) {
|
|
327
327
|
const rawLabel = containerMatch[1].trim();
|
|
@@ -333,7 +333,10 @@ export function parseSitemap(
|
|
|
333
333
|
if (pipeStr) {
|
|
334
334
|
// Build segments array compatible with parsePipeMetadata (first element is label, rest are pipe parts)
|
|
335
335
|
const pipeSegments = ['', pipeStr];
|
|
336
|
-
Object.assign(
|
|
336
|
+
Object.assign(
|
|
337
|
+
containerMetadata,
|
|
338
|
+
parsePipeMetadata(pipeSegments, aliasMap)
|
|
339
|
+
);
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
containerCounter++;
|
|
@@ -365,7 +368,14 @@ export function parseSitemap(
|
|
|
365
368
|
} else if (metadataMatch && indentStack.length === 0) {
|
|
366
369
|
// Could be a node label containing ':'
|
|
367
370
|
if (indent === 0) {
|
|
368
|
-
const node = parseNodeLabel(
|
|
371
|
+
const node = parseNodeLabel(
|
|
372
|
+
trimmed,
|
|
373
|
+
lineNumber,
|
|
374
|
+
palette,
|
|
375
|
+
++nodeCounter,
|
|
376
|
+
aliasMap,
|
|
377
|
+
pushWarning
|
|
378
|
+
);
|
|
369
379
|
attachNode(node, indent, indentStack, result);
|
|
370
380
|
labelToNode.set(node.label.toLowerCase(), node);
|
|
371
381
|
} else {
|
|
@@ -373,7 +383,14 @@ export function parseSitemap(
|
|
|
373
383
|
}
|
|
374
384
|
} else {
|
|
375
385
|
// Node label — possibly with pipe-delimited metadata
|
|
376
|
-
const node = parseNodeLabel(
|
|
386
|
+
const node = parseNodeLabel(
|
|
387
|
+
trimmed,
|
|
388
|
+
lineNumber,
|
|
389
|
+
palette,
|
|
390
|
+
++nodeCounter,
|
|
391
|
+
aliasMap,
|
|
392
|
+
pushWarning
|
|
393
|
+
);
|
|
377
394
|
attachNode(node, indent, indentStack, result);
|
|
378
395
|
labelToNode.set(node.label.toLowerCase(), node);
|
|
379
396
|
}
|
|
@@ -416,7 +433,11 @@ export function parseSitemap(
|
|
|
416
433
|
validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
|
|
417
434
|
}
|
|
418
435
|
|
|
419
|
-
if (
|
|
436
|
+
if (
|
|
437
|
+
result.roots.length === 0 &&
|
|
438
|
+
result.tagGroups.length === 0 &&
|
|
439
|
+
!result.error
|
|
440
|
+
) {
|
|
420
441
|
const diag = makeDgmoError(1, 'No pages found in sitemap');
|
|
421
442
|
result.diagnostics.push(diag);
|
|
422
443
|
result.error = formatDgmoError(diag);
|
|
@@ -435,12 +456,16 @@ function parseNodeLabel(
|
|
|
435
456
|
palette: PaletteColors | undefined,
|
|
436
457
|
counter: number,
|
|
437
458
|
aliasMap: Map<string, string> = new Map(),
|
|
438
|
-
warnFn?: (line: number, msg: string) => void
|
|
459
|
+
warnFn?: (line: number, msg: string) => void
|
|
439
460
|
): SitemapNode {
|
|
440
461
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
441
462
|
const rawLabel = segments[0];
|
|
442
463
|
const { label, color } = extractColor(rawLabel, palette);
|
|
443
|
-
const metadata = parsePipeMetadata(
|
|
464
|
+
const metadata = parsePipeMetadata(
|
|
465
|
+
segments,
|
|
466
|
+
aliasMap,
|
|
467
|
+
warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined
|
|
468
|
+
);
|
|
444
469
|
|
|
445
470
|
return {
|
|
446
471
|
id: `node-${counter}`,
|
|
@@ -458,7 +483,7 @@ function attachNode(
|
|
|
458
483
|
node: SitemapNode,
|
|
459
484
|
indent: number,
|
|
460
485
|
indentStack: { node: SitemapNode; indent: number }[],
|
|
461
|
-
result: ParsedSitemap
|
|
486
|
+
result: ParsedSitemap
|
|
462
487
|
): void {
|
|
463
488
|
// Pop stack entries with indent >= current indent
|
|
464
489
|
while (indentStack.length > 0) {
|
|
@@ -471,7 +496,11 @@ function attachNode(
|
|
|
471
496
|
const parent = indentStack[indentStack.length - 1].node;
|
|
472
497
|
node.parentId = parent.id;
|
|
473
498
|
// Cascade container metadata to child nodes (child overrides on conflict)
|
|
474
|
-
if (
|
|
499
|
+
if (
|
|
500
|
+
parent.isContainer &&
|
|
501
|
+
Object.keys(parent.metadata).length > 0 &&
|
|
502
|
+
!node.isContainer
|
|
503
|
+
) {
|
|
475
504
|
node.metadata = { ...parent.metadata, ...node.metadata };
|
|
476
505
|
}
|
|
477
506
|
parent.children.push(node);
|
|
@@ -484,7 +513,7 @@ function attachNode(
|
|
|
484
513
|
|
|
485
514
|
function findParentNode(
|
|
486
515
|
indent: number,
|
|
487
|
-
indentStack: { node: SitemapNode; indent: number }[]
|
|
516
|
+
indentStack: { node: SitemapNode; indent: number }[]
|
|
488
517
|
): SitemapNode | null {
|
|
489
518
|
for (let i = indentStack.length - 1; i >= 0; i--) {
|
|
490
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;
|