@diagrammo/dgmo 0.8.2 → 0.8.3
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/cli.cjs +189 -194
- package/dist/index.cjs +450 -596
- 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 +450 -596
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +821 -1060
- package/package.json +1 -1
- package/src/c4/parser.ts +19 -13
- package/src/chart.ts +69 -47
- package/src/class/parser.ts +46 -19
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +11 -16
- package/src/completion.ts +29 -25
- package/src/d3.ts +173 -174
- package/src/dgmo-router.ts +1 -1
- package/src/echarts.ts +42 -22
- package/src/er/parser.ts +9 -17
- package/src/gantt/parser.ts +108 -40
- package/src/graph/flowchart-parser.ts +7 -55
- package/src/graph/state-parser.ts +7 -10
- package/src/infra/parser.ts +6 -126
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +7 -13
- package/src/kanban/parser.ts +4 -7
- package/src/org/parser.ts +5 -8
- package/src/org/resolver.ts +3 -3
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +22 -45
- package/src/sitemap/parser.ts +10 -17
- package/src/utils/parsing.ts +9 -43
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/d3.ts
CHANGED
|
@@ -182,7 +182,7 @@ import { getSeriesColors } from './palettes';
|
|
|
182
182
|
import { mix } from './palettes/color-utils';
|
|
183
183
|
import type { DgmoError } from './diagnostics';
|
|
184
184
|
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
185
|
-
import { collectIndentedValues, extractColor,
|
|
185
|
+
import { collectIndentedValues, extractColor, parseFirstLine, parsePipeMetadata, MULTIPLE_PIPE_ERROR } from './utils/parsing';
|
|
186
186
|
import { matchTagBlockHeading, validateTagValues, resolveTagColor } from './utils/tag-groups';
|
|
187
187
|
import type { TagGroup } from './utils/tag-groups';
|
|
188
188
|
import {
|
|
@@ -463,6 +463,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
463
463
|
let currentArcGroup: string | null = null;
|
|
464
464
|
let currentTimelineGroup: string | null = null;
|
|
465
465
|
let currentTimelineTagGroup: TagGroup | null = null;
|
|
466
|
+
let inTimelineEraBlock = false;
|
|
467
|
+
let timelineEraBlockIndent = 0;
|
|
468
|
+
let inTimelineMarkerBlock = false;
|
|
469
|
+
let timelineMarkerBlockIndent = 0;
|
|
466
470
|
const timelineAliasMap = new Map<string, string>();
|
|
467
471
|
const VALID_D3_TYPES = new Set(['slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant', 'sequence']);
|
|
468
472
|
let firstLineParsed = false;
|
|
@@ -494,14 +498,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
494
498
|
// Not a bare chart type — fall through to normal parsing
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
// Timeline tag group heading: `tag
|
|
501
|
+
// Timeline tag group heading: `tag Name [alias X]`
|
|
498
502
|
if (result.type === 'timeline' && indent === 0) {
|
|
499
503
|
const tagBlockMatch = matchTagBlockHeading(line);
|
|
500
504
|
if (tagBlockMatch) {
|
|
501
|
-
if (tagBlockMatch.deprecated) {
|
|
502
|
-
result.diagnostics.push(makeDgmoError(lineNumber,
|
|
503
|
-
`'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`, 'warning'));
|
|
504
|
-
}
|
|
505
505
|
currentTimelineTagGroup = {
|
|
506
506
|
name: tagBlockMatch.name,
|
|
507
507
|
alias: tagBlockMatch.alias,
|
|
@@ -605,10 +605,80 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
// Timeline era
|
|
608
|
+
// Timeline era block entries (indented under bare `era`)
|
|
609
|
+
if (result.type === 'timeline' && inTimelineEraBlock) {
|
|
610
|
+
if (indent <= timelineEraBlockIndent) {
|
|
611
|
+
inTimelineEraBlock = false;
|
|
612
|
+
// fall through to process this line normally
|
|
613
|
+
} else {
|
|
614
|
+
if (line.startsWith('//')) continue;
|
|
615
|
+
const eraEntryMatch = line.match(
|
|
616
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*:?\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
617
|
+
);
|
|
618
|
+
if (eraEntryMatch) {
|
|
619
|
+
const colorAnnotation = eraEntryMatch[4]?.trim() || null;
|
|
620
|
+
result.timelineEras.push({
|
|
621
|
+
startDate: eraEntryMatch[1],
|
|
622
|
+
endDate: eraEntryMatch[2],
|
|
623
|
+
label: eraEntryMatch[3].trim(),
|
|
624
|
+
color: colorAnnotation
|
|
625
|
+
? resolveColor(colorAnnotation, palette)
|
|
626
|
+
: null,
|
|
627
|
+
lineNumber,
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
warn(lineNumber, `Unrecognized era entry: "${line}"`);
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Timeline marker block entries (indented under bare `marker`)
|
|
637
|
+
if (result.type === 'timeline' && inTimelineMarkerBlock) {
|
|
638
|
+
if (indent <= timelineMarkerBlockIndent) {
|
|
639
|
+
inTimelineMarkerBlock = false;
|
|
640
|
+
// fall through to process this line normally
|
|
641
|
+
} else {
|
|
642
|
+
if (line.startsWith('//')) continue;
|
|
643
|
+
const markerEntryMatch = line.match(
|
|
644
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
645
|
+
);
|
|
646
|
+
if (markerEntryMatch) {
|
|
647
|
+
const colorAnnotation = markerEntryMatch[3]?.trim() || null;
|
|
648
|
+
result.timelineMarkers.push({
|
|
649
|
+
date: markerEntryMatch[1],
|
|
650
|
+
label: markerEntryMatch[2].trim(),
|
|
651
|
+
color: colorAnnotation
|
|
652
|
+
? resolveColor(colorAnnotation, palette)
|
|
653
|
+
: null,
|
|
654
|
+
lineNumber,
|
|
655
|
+
});
|
|
656
|
+
} else {
|
|
657
|
+
warn(lineNumber, `Unrecognized marker entry: "${line}"`);
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Timeline era/marker block starters and inline forms
|
|
609
664
|
if (result.type === 'timeline') {
|
|
665
|
+
// Bare `era` keyword starts a block
|
|
666
|
+
if (line.toLowerCase() === 'era') {
|
|
667
|
+
inTimelineEraBlock = true;
|
|
668
|
+
timelineEraBlockIndent = indent;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Bare `marker` keyword starts a block
|
|
673
|
+
if (line.toLowerCase() === 'marker') {
|
|
674
|
+
inTimelineMarkerBlock = true;
|
|
675
|
+
timelineMarkerBlockIndent = indent;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Timeline era lines (inline): era YYYY->YYYY Label (color)
|
|
610
680
|
const eraMatch = line.match(
|
|
611
|
-
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
681
|
+
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*:?\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
612
682
|
);
|
|
613
683
|
if (eraMatch) {
|
|
614
684
|
const colorAnnotation = eraMatch[4]?.trim() || null;
|
|
@@ -624,7 +694,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
624
694
|
continue;
|
|
625
695
|
}
|
|
626
696
|
|
|
627
|
-
// Timeline marker lines: marker YYYY Label (color)
|
|
697
|
+
// Timeline marker lines (inline): marker YYYY Label (color)
|
|
628
698
|
const markerMatch = line.match(
|
|
629
699
|
/^marker:?\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
630
700
|
);
|
|
@@ -647,8 +717,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
647
717
|
// Duration event: 2026-07-15->30d: description (d=days, w=weeks, m=months, y=years, h=hours, min=minutes)
|
|
648
718
|
// Supports decimals up to 2 places (e.g., 1.25y = 1 year 3 months)
|
|
649
719
|
// Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
|
|
720
|
+
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
650
721
|
const durationMatch = line.match(
|
|
651
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
722
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?(?:\s*:\s*|\s+)(.+)$/
|
|
652
723
|
);
|
|
653
724
|
if (durationMatch) {
|
|
654
725
|
const startDate = durationMatch[1];
|
|
@@ -658,7 +729,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
658
729
|
const endDate = addDurationToDate(startDate, amount, unit);
|
|
659
730
|
const segments = durationMatch[5].split('|');
|
|
660
731
|
const metadata = segments.length > 1
|
|
661
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber,
|
|
732
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
|
|
662
733
|
: {};
|
|
663
734
|
result.timelineEvents.push({
|
|
664
735
|
date: startDate,
|
|
@@ -673,13 +744,15 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
673
744
|
}
|
|
674
745
|
|
|
675
746
|
// Range event: 1655->1667 description (supports uncertain end: 1655->1667?)
|
|
747
|
+
// Also supports YYYY-MM-DD HH:MM in both start and end dates
|
|
748
|
+
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
676
749
|
const rangeMatch = line.match(
|
|
677
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s
|
|
750
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)(\?)?(?:\s*:\s*|\s+)(.+)$/
|
|
678
751
|
);
|
|
679
752
|
if (rangeMatch) {
|
|
680
753
|
const segments = rangeMatch[4].split('|');
|
|
681
754
|
const metadata = segments.length > 1
|
|
682
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber,
|
|
755
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
|
|
683
756
|
: {};
|
|
684
757
|
result.timelineEvents.push({
|
|
685
758
|
date: rangeMatch[1],
|
|
@@ -700,7 +773,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
700
773
|
if (pointMatch) {
|
|
701
774
|
const segments = pointMatch[2].split('|');
|
|
702
775
|
const metadata = segments.length > 1
|
|
703
|
-
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber,
|
|
776
|
+
? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
|
|
704
777
|
: {};
|
|
705
778
|
result.timelineEvents.push({
|
|
706
779
|
date: pointMatch[1],
|
|
@@ -716,20 +789,52 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
716
789
|
|
|
717
790
|
// Venn diagram DSL
|
|
718
791
|
if (result.type === 'venn') {
|
|
719
|
-
// Intersection line: "A + B
|
|
792
|
+
// Intersection line: "A + B Label" / "A + B" / "A + B + C Label"
|
|
793
|
+
// Also accepts deprecated colon syntax: "A + B: Label"
|
|
720
794
|
if (/\+/.test(line)) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
label = line.substring(colonIdx + 1).trim() || null;
|
|
727
|
-
} else {
|
|
728
|
-
setsPart = line.trim();
|
|
729
|
-
label = null;
|
|
795
|
+
// Build lookup of known set names and aliases for label extraction
|
|
796
|
+
const knownSetRefs = new Set<string>();
|
|
797
|
+
for (const s of result.vennSets) {
|
|
798
|
+
knownSetRefs.add(s.name.toLowerCase());
|
|
799
|
+
if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
|
|
730
800
|
}
|
|
731
|
-
|
|
732
|
-
|
|
801
|
+
|
|
802
|
+
const segments = line.split('+').map((s) => s.trim()).filter(Boolean);
|
|
803
|
+
if (segments.length >= 2) {
|
|
804
|
+
// All segments except the last are pure set references
|
|
805
|
+
const rawSets = segments.slice(0, -1);
|
|
806
|
+
const lastSeg = segments[segments.length - 1];
|
|
807
|
+
|
|
808
|
+
// For the last segment, extract set reference and optional label.
|
|
809
|
+
// Support deprecated colon: "SetRef: Label"
|
|
810
|
+
const colonIdx = lastSeg.indexOf(':');
|
|
811
|
+
let lastSetRef: string;
|
|
812
|
+
let label: string | null;
|
|
813
|
+
if (colonIdx >= 0) {
|
|
814
|
+
lastSetRef = lastSeg.substring(0, colonIdx).trim();
|
|
815
|
+
label = lastSeg.substring(colonIdx + 1).trim() || null;
|
|
816
|
+
} else {
|
|
817
|
+
// No colon — find where the set reference ends and label begins.
|
|
818
|
+
// Try progressively shorter prefixes against known set names/aliases.
|
|
819
|
+
const words = lastSeg.split(/\s+/);
|
|
820
|
+
let matchLen = 0;
|
|
821
|
+
for (let w = words.length; w >= 1; w--) {
|
|
822
|
+
const candidate = words.slice(0, w).join(' ');
|
|
823
|
+
if (knownSetRefs.has(candidate.toLowerCase())) {
|
|
824
|
+
matchLen = w;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (matchLen > 0) {
|
|
829
|
+
lastSetRef = words.slice(0, matchLen).join(' ');
|
|
830
|
+
label = words.length > matchLen ? words.slice(matchLen).join(' ') : null;
|
|
831
|
+
} else {
|
|
832
|
+
// No known set matched — assume first word is the set ref, rest is label
|
|
833
|
+
lastSetRef = words[0];
|
|
834
|
+
label = words.length > 1 ? words.slice(1).join(' ') : null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
rawSets.push(lastSetRef);
|
|
733
838
|
result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
|
|
734
839
|
continue;
|
|
735
840
|
}
|
|
@@ -866,20 +971,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
866
971
|
continue;
|
|
867
972
|
}
|
|
868
973
|
|
|
869
|
-
if (firstToken === 'orientation' || firstToken === 'direction') {
|
|
870
|
-
if (result.type === 'arc' || result.type === 'timeline') {
|
|
871
|
-
const vLower = restValue.toLowerCase();
|
|
872
|
-
if (vLower === 'horizontal' || vLower === 'vertical') {
|
|
873
|
-
result.orientation = vLower;
|
|
874
|
-
} else {
|
|
875
|
-
const dir = normalizeDirection(restValue);
|
|
876
|
-
if (dir === 'LR') result.orientation = 'horizontal';
|
|
877
|
-
else if (dir === 'TB') result.orientation = 'vertical';
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
974
|
if (firstToken === 'order') {
|
|
884
975
|
const v = restValue.toLowerCase();
|
|
885
976
|
if (v === 'name' || v === 'group' || v === 'degree') {
|
|
@@ -888,29 +979,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
888
979
|
continue;
|
|
889
980
|
}
|
|
890
981
|
|
|
891
|
-
if (firstToken === 'sort') {
|
|
892
|
-
const vLower = restValue.toLowerCase();
|
|
893
|
-
if (vLower === 'time' || vLower === 'group') {
|
|
894
|
-
result.timelineSort = vLower;
|
|
895
|
-
} else if (vLower === 'tag' || vLower.startsWith('tag:')) {
|
|
896
|
-
result.timelineSort = 'tag';
|
|
897
|
-
if (vLower.startsWith('tag:')) {
|
|
898
|
-
const groupRef = restValue.substring(4).trim();
|
|
899
|
-
if (groupRef) {
|
|
900
|
-
result.timelineDefaultSwimlaneTG = groupRef;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (firstToken === 'swimlanes') {
|
|
908
|
-
const v = restValue.toLowerCase();
|
|
909
|
-
if (v === 'on') result.timelineSwimlanes = true;
|
|
910
|
-
else if (v === 'off') result.timelineSwimlanes = false;
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
982
|
if (firstToken === 'rotate') {
|
|
915
983
|
const v = restValue.toLowerCase();
|
|
916
984
|
if (v === 'none' || v === 'mixed' || v === 'angled') {
|
|
@@ -951,23 +1019,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
951
1019
|
// Check for color annotation in raw key: "Label(color)"
|
|
952
1020
|
const colorMatch = rawKey.match(/^(.+?)\(([^)]+)\)\s*$/);
|
|
953
1021
|
|
|
954
|
-
if (key === 'chart') {
|
|
955
|
-
const value = line
|
|
956
|
-
.substring(colonIndex + 1)
|
|
957
|
-
.trim()
|
|
958
|
-
.toLowerCase();
|
|
959
|
-
if (VALID_D3_TYPES.has(value)) {
|
|
960
|
-
result.type = value as ParsedVisualization['type'];
|
|
961
|
-
} else {
|
|
962
|
-
const validD3Types = [...VALID_D3_TYPES];
|
|
963
|
-
let msg = `Unsupported chart type: ${value}. Supported types: ${validD3Types.join(', ')}`;
|
|
964
|
-
const hint = suggest(value, validD3Types);
|
|
965
|
-
if (hint) msg += `. ${hint}`;
|
|
966
|
-
return fail(lineNumber, msg);
|
|
967
|
-
}
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
1022
|
if (key === 'title') {
|
|
972
1023
|
result.title = line.substring(colonIndex + 1).trim();
|
|
973
1024
|
result.titleLineNumber = lineNumber;
|
|
@@ -977,23 +1028,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
977
1028
|
continue;
|
|
978
1029
|
}
|
|
979
1030
|
|
|
980
|
-
if (key === 'orientation' || key === 'direction') {
|
|
981
|
-
// Only arc and timeline support orientation
|
|
982
|
-
if (result.type === 'arc' || result.type === 'timeline') {
|
|
983
|
-
const raw = line.substring(colonIndex + 1).trim();
|
|
984
|
-
// Accept horizontal/vertical directly, or LR/TB via normalizeDirection
|
|
985
|
-
const vLower = raw.toLowerCase();
|
|
986
|
-
if (vLower === 'horizontal' || vLower === 'vertical') {
|
|
987
|
-
result.orientation = vLower;
|
|
988
|
-
} else {
|
|
989
|
-
const dir = normalizeDirection(raw);
|
|
990
|
-
if (dir === 'LR') result.orientation = 'horizontal';
|
|
991
|
-
else if (dir === 'TB') result.orientation = 'vertical';
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
1031
|
if (key === 'order') {
|
|
998
1032
|
const v = line
|
|
999
1033
|
.substring(colonIndex + 1)
|
|
@@ -1005,39 +1039,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1005
1039
|
continue;
|
|
1006
1040
|
}
|
|
1007
1041
|
|
|
1008
|
-
if (key === 'sort') {
|
|
1009
|
-
const v = line
|
|
1010
|
-
.substring(colonIndex + 1)
|
|
1011
|
-
.trim();
|
|
1012
|
-
const vLower = v.toLowerCase();
|
|
1013
|
-
if (vLower === 'time' || vLower === 'group') {
|
|
1014
|
-
result.timelineSort = vLower;
|
|
1015
|
-
} else if (vLower === 'tag' || vLower.startsWith('tag:')) {
|
|
1016
|
-
result.timelineSort = 'tag';
|
|
1017
|
-
if (vLower.startsWith('tag:')) {
|
|
1018
|
-
// Extract group name (preserving original case for display)
|
|
1019
|
-
const groupRef = v.substring(4).trim();
|
|
1020
|
-
if (groupRef) {
|
|
1021
|
-
result.timelineDefaultSwimlaneTG = groupRef;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (key === 'swimlanes') {
|
|
1029
|
-
const v = line
|
|
1030
|
-
.substring(colonIndex + 1)
|
|
1031
|
-
.trim()
|
|
1032
|
-
.toLowerCase();
|
|
1033
|
-
if (v === 'on') {
|
|
1034
|
-
result.timelineSwimlanes = true;
|
|
1035
|
-
} else if (v === 'off') {
|
|
1036
|
-
result.timelineSwimlanes = false;
|
|
1037
|
-
}
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
1042
|
if (key === 'rotate') {
|
|
1042
1043
|
const v = line
|
|
1043
1044
|
.substring(colonIndex + 1)
|
|
@@ -1092,22 +1093,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1092
1093
|
}
|
|
1093
1094
|
|
|
1094
1095
|
if (allNumeric && numericValues.length > 0) {
|
|
1095
|
-
//
|
|
1096
|
-
if (result.type
|
|
1097
|
-
result.words.push({
|
|
1098
|
-
text: labelPart,
|
|
1099
|
-
weight: numericValues[0],
|
|
1100
|
-
lineNumber,
|
|
1101
|
-
});
|
|
1102
|
-
} else {
|
|
1096
|
+
// Wordcloud does not use colon data format — skip to freeform handling
|
|
1097
|
+
if (result.type !== 'wordcloud') {
|
|
1103
1098
|
result.data.push({
|
|
1104
1099
|
label: labelPart,
|
|
1105
1100
|
values: numericValues,
|
|
1106
1101
|
color: colorPart,
|
|
1107
1102
|
lineNumber,
|
|
1108
1103
|
});
|
|
1104
|
+
continue;
|
|
1109
1105
|
}
|
|
1110
|
-
continue;
|
|
1111
1106
|
}
|
|
1112
1107
|
}
|
|
1113
1108
|
|
|
@@ -1171,7 +1166,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1171
1166
|
result.words = tokenizeFreeformText(freeformLines.join(' '));
|
|
1172
1167
|
}
|
|
1173
1168
|
if (result.words.length === 0) {
|
|
1174
|
-
warn(1, 'No words found. Add words as "word
|
|
1169
|
+
warn(1, 'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text');
|
|
1175
1170
|
}
|
|
1176
1171
|
// Apply max word limit (words are already sorted by weight desc for freeform)
|
|
1177
1172
|
if (
|
|
@@ -1226,29 +1221,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1226
1221
|
}
|
|
1227
1222
|
}
|
|
1228
1223
|
|
|
1229
|
-
// Resolve sort: tag default swimlane group
|
|
1230
|
-
if (result.timelineSort === 'tag') {
|
|
1231
|
-
if (result.timelineTagGroups.length === 0) {
|
|
1232
|
-
warn(1, '"sort: tag" requires at least one tag group definition');
|
|
1233
|
-
result.timelineSort = 'time';
|
|
1234
|
-
} else if (result.timelineDefaultSwimlaneTG) {
|
|
1235
|
-
// Resolve alias → full group name
|
|
1236
|
-
const ref = result.timelineDefaultSwimlaneTG.toLowerCase();
|
|
1237
|
-
const match = result.timelineTagGroups.find(
|
|
1238
|
-
(g) => g.name.toLowerCase() === ref || g.alias?.toLowerCase() === ref
|
|
1239
|
-
);
|
|
1240
|
-
if (match) {
|
|
1241
|
-
result.timelineDefaultSwimlaneTG = match.name;
|
|
1242
|
-
} else {
|
|
1243
|
-
warn(1, `"sort: tag:${result.timelineDefaultSwimlaneTG}" — no tag group matches "${result.timelineDefaultSwimlaneTG}"`);
|
|
1244
|
-
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1245
|
-
}
|
|
1246
|
-
} else {
|
|
1247
|
-
// Default to first tag group
|
|
1248
|
-
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
1224
|
return result;
|
|
1253
1225
|
}
|
|
1254
1226
|
|
|
@@ -2570,6 +2542,26 @@ export function formatDateLabel(dateStr: string): string {
|
|
|
2570
2542
|
return `${month} ${day}, ${year}${timeSuffix}`;
|
|
2571
2543
|
}
|
|
2572
2544
|
|
|
2545
|
+
/**
|
|
2546
|
+
* Formats a boundary label for the time axis.
|
|
2547
|
+
* When both boundaries fall on the same calendar day and have a time component,
|
|
2548
|
+
* returns just the time (e.g. "12:15") to avoid collisions with regular ticks.
|
|
2549
|
+
* Otherwise falls back to the full formatDateLabel.
|
|
2550
|
+
*/
|
|
2551
|
+
function formatBoundaryLabel(dateStr: string, otherDateStr: string): string {
|
|
2552
|
+
const spaceIdx = dateStr.indexOf(' ');
|
|
2553
|
+
const otherSpaceIdx = otherDateStr.indexOf(' ');
|
|
2554
|
+
// Both must have time components and share the same date portion
|
|
2555
|
+
if (spaceIdx !== -1 && otherSpaceIdx !== -1) {
|
|
2556
|
+
const datePart = dateStr.slice(0, spaceIdx);
|
|
2557
|
+
const otherDatePart = otherDateStr.slice(0, otherSpaceIdx);
|
|
2558
|
+
if (datePart === otherDatePart) {
|
|
2559
|
+
return dateStr.slice(spaceIdx + 1); // just "HH:MM"
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return formatDateLabel(dateStr);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2573
2565
|
/**
|
|
2574
2566
|
* Computes adaptive tick marks for a timeline scale.
|
|
2575
2567
|
* - Multi-year spans → year ticks
|
|
@@ -2662,6 +2654,9 @@ export function computeTimeTicks(
|
|
|
2662
2654
|
else if (spanHours > 24) stepHour = 3;
|
|
2663
2655
|
else if (spanHours > 12) stepHour = 2;
|
|
2664
2656
|
|
|
2657
|
+
// For single-day spans, just show HH:MM without the date prefix
|
|
2658
|
+
const singleDay = spanHours <= 24;
|
|
2659
|
+
|
|
2665
2660
|
const startDate = fractionalYearToDate(domainMin);
|
|
2666
2661
|
// Round down to nearest step boundary
|
|
2667
2662
|
startDate.setHours(Math.floor(startDate.getHours() / stepHour) * stepHour, 0, 0, 0);
|
|
@@ -2670,11 +2665,15 @@ export function computeTimeTicks(
|
|
|
2670
2665
|
const val = dateToFractionalYear(startDate);
|
|
2671
2666
|
if (val > domainMax) break;
|
|
2672
2667
|
if (val >= domainMin) {
|
|
2673
|
-
const mon = MONTH_ABBR[startDate.getMonth()];
|
|
2674
|
-
const d = startDate.getDate();
|
|
2675
2668
|
const hh = String(startDate.getHours()).padStart(2, '0');
|
|
2676
2669
|
const mm = String(startDate.getMinutes()).padStart(2, '0');
|
|
2677
|
-
|
|
2670
|
+
if (singleDay) {
|
|
2671
|
+
ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
|
|
2672
|
+
} else {
|
|
2673
|
+
const mon = MONTH_ABBR[startDate.getMonth()];
|
|
2674
|
+
const d = startDate.getDate();
|
|
2675
|
+
ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
|
|
2676
|
+
}
|
|
2678
2677
|
}
|
|
2679
2678
|
startDate.setHours(startDate.getHours() + stepHour);
|
|
2680
2679
|
}
|
|
@@ -3409,8 +3408,8 @@ export function renderTimeline(
|
|
|
3409
3408
|
textColor,
|
|
3410
3409
|
minDate,
|
|
3411
3410
|
maxDate,
|
|
3412
|
-
|
|
3413
|
-
|
|
3411
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
3412
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3414
3413
|
);
|
|
3415
3414
|
}
|
|
3416
3415
|
|
|
@@ -3659,8 +3658,8 @@ export function renderTimeline(
|
|
|
3659
3658
|
textColor,
|
|
3660
3659
|
minDate,
|
|
3661
3660
|
maxDate,
|
|
3662
|
-
|
|
3663
|
-
|
|
3661
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
3662
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3664
3663
|
);
|
|
3665
3664
|
}
|
|
3666
3665
|
|
|
@@ -3960,8 +3959,8 @@ export function renderTimeline(
|
|
|
3960
3959
|
textColor,
|
|
3961
3960
|
minDate,
|
|
3962
3961
|
maxDate,
|
|
3963
|
-
|
|
3964
|
-
|
|
3962
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
3963
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3965
3964
|
);
|
|
3966
3965
|
}
|
|
3967
3966
|
|
|
@@ -4261,8 +4260,8 @@ export function renderTimeline(
|
|
|
4261
4260
|
textColor,
|
|
4262
4261
|
minDate,
|
|
4263
4262
|
maxDate,
|
|
4264
|
-
|
|
4265
|
-
|
|
4263
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
4264
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
4266
4265
|
);
|
|
4267
4266
|
}
|
|
4268
4267
|
|
package/src/dgmo-router.ts
CHANGED
|
@@ -68,7 +68,7 @@ export function looksLikeC4(content: string): boolean {
|
|
|
68
68
|
/**
|
|
69
69
|
* Extracts the chart type from raw file content.
|
|
70
70
|
* First tries the first non-empty, non-comment line as a bare chart type name
|
|
71
|
-
* (e.g., `gantt Product Launch`).
|
|
71
|
+
* (e.g., `gantt Product Launch`).
|
|
72
72
|
* Falls back to inference when no explicit chart type is found.
|
|
73
73
|
*/
|
|
74
74
|
export function parseDgmoChartType(content: string): string | null {
|