@diagrammo/dgmo 0.2.19 → 0.2.21
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/README.md +33 -33
- package/dist/cli.cjs +150 -144
- package/dist/index.cjs +9475 -8087
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -1
- package/dist/index.d.ts +124 -1
- package/dist/index.js +9345 -7965
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chart.ts +40 -9
- package/src/class/parser.ts +37 -6
- package/src/class/renderer.ts +11 -8
- package/src/class/types.ts +4 -0
- package/src/cli.ts +38 -3
- package/src/d3.ts +159 -48
- package/src/dgmo-mermaid.ts +7 -1
- package/src/dgmo-router.ts +74 -4
- package/src/diagnostics.ts +77 -0
- package/src/echarts.ts +23 -14
- package/src/er/layout.ts +49 -7
- package/src/er/parser.ts +31 -4
- package/src/er/renderer.ts +2 -1
- package/src/er/types.ts +3 -0
- package/src/graph/flowchart-parser.ts +34 -4
- package/src/graph/flowchart-renderer.ts +35 -32
- package/src/graph/types.ts +4 -0
- package/src/index.ts +22 -0
- package/src/kanban/mutations.ts +183 -0
- package/src/kanban/parser.ts +389 -0
- package/src/kanban/renderer.ts +564 -0
- package/src/kanban/types.ts +45 -0
- package/src/org/layout.ts +97 -66
- package/src/org/parser.ts +50 -15
- package/src/org/renderer.ts +91 -159
- package/src/org/resolver.ts +470 -0
- package/src/sequence/parser.ts +90 -33
- package/src/sequence/renderer.ts +13 -5
package/src/d3.ts
CHANGED
|
@@ -167,6 +167,7 @@ export interface ParsedD3 {
|
|
|
167
167
|
quadrantYAxis: [string, string] | null;
|
|
168
168
|
quadrantYAxisLineNumber: number | null;
|
|
169
169
|
quadrantTitleLineNumber: number | null;
|
|
170
|
+
diagnostics: DgmoError[];
|
|
170
171
|
error: string | null;
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -177,6 +178,8 @@ export interface ParsedD3 {
|
|
|
177
178
|
import { resolveColor } from './colors';
|
|
178
179
|
import type { PaletteColors } from './palettes';
|
|
179
180
|
import { getSeriesColors } from './palettes';
|
|
181
|
+
import type { DgmoError } from './diagnostics';
|
|
182
|
+
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
180
183
|
|
|
181
184
|
// ============================================================
|
|
182
185
|
// Timeline Date Helper
|
|
@@ -298,12 +301,19 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
298
301
|
quadrantYAxis: null,
|
|
299
302
|
quadrantYAxisLineNumber: null,
|
|
300
303
|
quadrantTitleLineNumber: null,
|
|
304
|
+
diagnostics: [],
|
|
301
305
|
error: null,
|
|
302
306
|
};
|
|
303
307
|
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
const fail = (line: number, message: string): ParsedD3 => {
|
|
309
|
+
const diag = makeDgmoError(line, message);
|
|
310
|
+
result.diagnostics.push(diag);
|
|
311
|
+
result.error = formatDgmoError(diag);
|
|
306
312
|
return result;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (!content || !content.trim()) {
|
|
316
|
+
return fail(0, 'Empty content');
|
|
307
317
|
}
|
|
308
318
|
|
|
309
319
|
const lines = content.split('\n');
|
|
@@ -604,8 +614,11 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
604
614
|
) {
|
|
605
615
|
result.type = value;
|
|
606
616
|
} else {
|
|
607
|
-
|
|
608
|
-
|
|
617
|
+
const validD3Types = ['slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant', 'sequence'];
|
|
618
|
+
let msg = `Unsupported chart type: ${value}. Supported types: ${validD3Types.join(', ')}`;
|
|
619
|
+
const hint = suggest(value, validD3Types);
|
|
620
|
+
if (hint) msg += `. ${hint}`;
|
|
621
|
+
return fail(lineNumber, msg);
|
|
609
622
|
}
|
|
610
623
|
continue;
|
|
611
624
|
}
|
|
@@ -620,12 +633,15 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
620
633
|
}
|
|
621
634
|
|
|
622
635
|
if (key === 'orientation') {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
636
|
+
// Only arc and timeline support orientation
|
|
637
|
+
if (result.type === 'arc' || result.type === 'timeline') {
|
|
638
|
+
const v = line
|
|
639
|
+
.substring(colonIndex + 1)
|
|
640
|
+
.trim()
|
|
641
|
+
.toLowerCase();
|
|
642
|
+
if (v === 'horizontal' || v === 'vertical') {
|
|
643
|
+
result.orientation = v;
|
|
644
|
+
}
|
|
629
645
|
}
|
|
630
646
|
continue;
|
|
631
647
|
}
|
|
@@ -783,8 +799,7 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
783
799
|
|
|
784
800
|
// Validation
|
|
785
801
|
if (!result.type) {
|
|
786
|
-
|
|
787
|
-
return result;
|
|
802
|
+
return fail(1, 'Missing required "chart:" line (e.g., "chart: slope")');
|
|
788
803
|
}
|
|
789
804
|
|
|
790
805
|
// Sequence diagrams are parsed by their own dedicated parser
|
|
@@ -792,15 +807,17 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
792
807
|
return result;
|
|
793
808
|
}
|
|
794
809
|
|
|
810
|
+
const warn = (line: number, message: string): void => {
|
|
811
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
812
|
+
};
|
|
813
|
+
|
|
795
814
|
if (result.type === 'wordcloud') {
|
|
796
815
|
// If no structured words were found, parse freeform text as word frequencies
|
|
797
816
|
if (result.words.length === 0 && freeformLines.length > 0) {
|
|
798
817
|
result.words = tokenizeFreeformText(freeformLines.join(' '));
|
|
799
818
|
}
|
|
800
819
|
if (result.words.length === 0) {
|
|
801
|
-
|
|
802
|
-
'No words found. Add words as "word: weight", one per line, or paste freeform text';
|
|
803
|
-
return result;
|
|
820
|
+
warn(1, 'No words found. Add words as "word: weight", one per line, or paste freeform text');
|
|
804
821
|
}
|
|
805
822
|
// Apply max word limit (words are already sorted by weight desc for freeform)
|
|
806
823
|
if (
|
|
@@ -817,15 +834,13 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
817
834
|
|
|
818
835
|
if (result.type === 'arc') {
|
|
819
836
|
if (result.links.length === 0) {
|
|
820
|
-
|
|
821
|
-
'No links found. Add links as "Source -> Target: weight" (e.g., "Alice -> Bob: 5")';
|
|
822
|
-
return result;
|
|
837
|
+
warn(1, 'No links found. Add links as "Source -> Target: weight" (e.g., "Alice -> Bob: 5")');
|
|
823
838
|
}
|
|
824
839
|
// Validate arc ordering vs groups
|
|
825
840
|
if (result.arcNodeGroups.length > 0) {
|
|
826
841
|
if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
|
|
827
|
-
|
|
828
|
-
|
|
842
|
+
warn(1, `Cannot use "order: ${result.arcOrder}" with ## section headers. Use "order: group" or remove section headers.`);
|
|
843
|
+
result.arcOrder = 'group';
|
|
829
844
|
}
|
|
830
845
|
if (result.arcOrder === 'appearance') {
|
|
831
846
|
result.arcOrder = 'group';
|
|
@@ -836,70 +851,67 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
836
851
|
|
|
837
852
|
if (result.type === 'timeline') {
|
|
838
853
|
if (result.timelineEvents.length === 0) {
|
|
839
|
-
|
|
840
|
-
'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"';
|
|
841
|
-
return result;
|
|
854
|
+
warn(1, 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"');
|
|
842
855
|
}
|
|
843
856
|
return result;
|
|
844
857
|
}
|
|
845
858
|
|
|
846
859
|
if (result.type === 'venn') {
|
|
847
860
|
if (result.vennSets.length < 2) {
|
|
848
|
-
|
|
849
|
-
'At least 2 sets are required. Add sets as "Name: size" (e.g., "Math: 100")';
|
|
850
|
-
return result;
|
|
861
|
+
return fail(1, 'At least 2 sets are required. Add sets as "Name: size" (e.g., "Math: 100")');
|
|
851
862
|
}
|
|
852
863
|
if (result.vennSets.length > 3) {
|
|
853
|
-
|
|
854
|
-
return result;
|
|
864
|
+
return fail(1, 'At most 3 sets are supported. Remove extra sets.');
|
|
855
865
|
}
|
|
856
|
-
// Validate overlap references and sizes
|
|
866
|
+
// Validate overlap references and sizes — skip invalid overlaps
|
|
857
867
|
const setMap = new Map(result.vennSets.map((s) => [s.name, s.size]));
|
|
868
|
+
const validOverlaps = [];
|
|
858
869
|
for (const ov of result.vennOverlaps) {
|
|
870
|
+
let valid = true;
|
|
859
871
|
for (const setName of ov.sets) {
|
|
860
872
|
if (!setMap.has(setName)) {
|
|
861
|
-
result.
|
|
862
|
-
|
|
873
|
+
result.diagnostics.push(makeDgmoError(ov.lineNumber, `Overlap references unknown set "${setName}". Define it first as "${setName}: <size>"`));
|
|
874
|
+
if (!result.error) result.error = formatDgmoError(result.diagnostics[result.diagnostics.length - 1]);
|
|
875
|
+
valid = false;
|
|
876
|
+
break;
|
|
863
877
|
}
|
|
864
878
|
}
|
|
879
|
+
if (!valid) continue;
|
|
865
880
|
const minSetSize = Math.min(...ov.sets.map((s) => setMap.get(s)!));
|
|
866
881
|
if (ov.size > minSetSize) {
|
|
867
|
-
|
|
868
|
-
return result;
|
|
882
|
+
warn(ov.lineNumber, `Overlap size ${ov.size} exceeds smallest constituent set size ${minSetSize}`);
|
|
869
883
|
}
|
|
884
|
+
validOverlaps.push(ov);
|
|
870
885
|
}
|
|
886
|
+
result.vennOverlaps = validOverlaps;
|
|
871
887
|
return result;
|
|
872
888
|
}
|
|
873
889
|
|
|
874
890
|
if (result.type === 'quadrant') {
|
|
875
891
|
if (result.quadrantPoints.length === 0) {
|
|
876
|
-
|
|
877
|
-
'No data points found. Add points as "Label: x, y" (e.g., "Item A: 0.5, 0.7")';
|
|
878
|
-
return result;
|
|
892
|
+
warn(1, 'No data points found. Add points as "Label: x, y" (e.g., "Item A: 0.5, 0.7")');
|
|
879
893
|
}
|
|
880
894
|
return result;
|
|
881
895
|
}
|
|
882
896
|
|
|
883
897
|
// Slope chart validation
|
|
884
898
|
if (result.periods.length < 2) {
|
|
885
|
-
|
|
886
|
-
'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")';
|
|
887
|
-
return result;
|
|
899
|
+
return fail(1, 'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")');
|
|
888
900
|
}
|
|
889
901
|
|
|
890
902
|
if (result.data.length === 0) {
|
|
891
|
-
|
|
892
|
-
'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")';
|
|
893
|
-
return result;
|
|
903
|
+
warn(1, 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")');
|
|
894
904
|
}
|
|
895
905
|
|
|
896
|
-
// Validate value counts match period count
|
|
906
|
+
// Validate value counts match period count — warn and skip mismatched items
|
|
897
907
|
for (const item of result.data) {
|
|
898
908
|
if (item.values.length !== result.periods.length) {
|
|
899
|
-
|
|
900
|
-
return result;
|
|
909
|
+
warn(item.lineNumber, `Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`);
|
|
901
910
|
}
|
|
902
911
|
}
|
|
912
|
+
result.data = result.data.filter(
|
|
913
|
+
(item) => item.values.length === result.periods.length
|
|
914
|
+
);
|
|
903
915
|
|
|
904
916
|
return result;
|
|
905
917
|
}
|
|
@@ -2916,6 +2928,34 @@ export function renderTimeline(
|
|
|
2916
2928
|
if (ev.endDate) {
|
|
2917
2929
|
const y2 = yScale(parseTimelineDate(ev.endDate));
|
|
2918
2930
|
const rectH = Math.max(y2 - y, 4);
|
|
2931
|
+
|
|
2932
|
+
let fill: string = laneColor;
|
|
2933
|
+
if (ev.uncertain) {
|
|
2934
|
+
const gradientId = `uncertain-vg-${ev.lineNumber}`;
|
|
2935
|
+
const defs =
|
|
2936
|
+
svg.select('defs').node() || svg.append('defs').node();
|
|
2937
|
+
d3Selection
|
|
2938
|
+
.select(defs as Element)
|
|
2939
|
+
.append('linearGradient')
|
|
2940
|
+
.attr('id', gradientId)
|
|
2941
|
+
.attr('x1', '0%')
|
|
2942
|
+
.attr('y1', '0%')
|
|
2943
|
+
.attr('x2', '0%')
|
|
2944
|
+
.attr('y2', '100%')
|
|
2945
|
+
.selectAll('stop')
|
|
2946
|
+
.data([
|
|
2947
|
+
{ offset: '0%', opacity: 1 },
|
|
2948
|
+
{ offset: '80%', opacity: 1 },
|
|
2949
|
+
{ offset: '100%', opacity: 0 },
|
|
2950
|
+
])
|
|
2951
|
+
.enter()
|
|
2952
|
+
.append('stop')
|
|
2953
|
+
.attr('offset', (d) => d.offset)
|
|
2954
|
+
.attr('stop-color', laneColor)
|
|
2955
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
2956
|
+
fill = `url(#${gradientId})`;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2919
2959
|
evG
|
|
2920
2960
|
.append('rect')
|
|
2921
2961
|
.attr('x', laneCenter - 6)
|
|
@@ -2923,7 +2963,7 @@ export function renderTimeline(
|
|
|
2923
2963
|
.attr('width', 12)
|
|
2924
2964
|
.attr('height', rectH)
|
|
2925
2965
|
.attr('rx', 4)
|
|
2926
|
-
.attr('fill',
|
|
2966
|
+
.attr('fill', fill);
|
|
2927
2967
|
evG
|
|
2928
2968
|
.append('text')
|
|
2929
2969
|
.attr('x', laneCenter + 14)
|
|
@@ -3127,6 +3167,34 @@ export function renderTimeline(
|
|
|
3127
3167
|
if (ev.endDate) {
|
|
3128
3168
|
const y2 = yScale(parseTimelineDate(ev.endDate));
|
|
3129
3169
|
const rectH = Math.max(y2 - y, 4);
|
|
3170
|
+
|
|
3171
|
+
let fill: string = color;
|
|
3172
|
+
if (ev.uncertain) {
|
|
3173
|
+
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3174
|
+
const defs =
|
|
3175
|
+
svg.select('defs').node() || svg.append('defs').node();
|
|
3176
|
+
d3Selection
|
|
3177
|
+
.select(defs as Element)
|
|
3178
|
+
.append('linearGradient')
|
|
3179
|
+
.attr('id', gradientId)
|
|
3180
|
+
.attr('x1', '0%')
|
|
3181
|
+
.attr('y1', '0%')
|
|
3182
|
+
.attr('x2', '0%')
|
|
3183
|
+
.attr('y2', '100%')
|
|
3184
|
+
.selectAll('stop')
|
|
3185
|
+
.data([
|
|
3186
|
+
{ offset: '0%', opacity: 1 },
|
|
3187
|
+
{ offset: '80%', opacity: 1 },
|
|
3188
|
+
{ offset: '100%', opacity: 0 },
|
|
3189
|
+
])
|
|
3190
|
+
.enter()
|
|
3191
|
+
.append('stop')
|
|
3192
|
+
.attr('offset', (d) => d.offset)
|
|
3193
|
+
.attr('stop-color', color)
|
|
3194
|
+
.attr('stop-opacity', (d) => d.opacity);
|
|
3195
|
+
fill = `url(#${gradientId})`;
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3130
3198
|
evG
|
|
3131
3199
|
.append('rect')
|
|
3132
3200
|
.attr('x', axisX - 6)
|
|
@@ -3134,7 +3202,7 @@ export function renderTimeline(
|
|
|
3134
3202
|
.attr('width', 12)
|
|
3135
3203
|
.attr('height', rectH)
|
|
3136
3204
|
.attr('rx', 4)
|
|
3137
|
-
.attr('fill',
|
|
3205
|
+
.attr('fill', fill);
|
|
3138
3206
|
evG
|
|
3139
3207
|
.append('text')
|
|
3140
3208
|
.attr('x', axisX + 16)
|
|
@@ -5213,7 +5281,7 @@ export async function renderD3ForExport(
|
|
|
5213
5281
|
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
5214
5282
|
|
|
5215
5283
|
const orgParsed = parseOrg(content, effectivePalette);
|
|
5216
|
-
if (orgParsed.error
|
|
5284
|
+
if (orgParsed.error) return '';
|
|
5217
5285
|
|
|
5218
5286
|
// Apply interactive collapse state when provided
|
|
5219
5287
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
@@ -5281,6 +5349,49 @@ export async function renderD3ForExport(
|
|
|
5281
5349
|
}
|
|
5282
5350
|
}
|
|
5283
5351
|
|
|
5352
|
+
if (detectedType === 'kanban') {
|
|
5353
|
+
const { parseKanban } = await import('./kanban/parser');
|
|
5354
|
+
const { renderKanban } = await import('./kanban/renderer');
|
|
5355
|
+
|
|
5356
|
+
const isDark = theme === 'dark';
|
|
5357
|
+
const { getPalette } = await import('./palettes');
|
|
5358
|
+
const effectivePalette =
|
|
5359
|
+
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
5360
|
+
|
|
5361
|
+
const kanbanParsed = parseKanban(content, effectivePalette);
|
|
5362
|
+
if (kanbanParsed.error || kanbanParsed.columns.length === 0) return '';
|
|
5363
|
+
|
|
5364
|
+
const container = document.createElement('div');
|
|
5365
|
+
container.style.position = 'absolute';
|
|
5366
|
+
container.style.left = '-9999px';
|
|
5367
|
+
document.body.appendChild(container);
|
|
5368
|
+
|
|
5369
|
+
try {
|
|
5370
|
+
renderKanban(container, kanbanParsed, effectivePalette, isDark);
|
|
5371
|
+
|
|
5372
|
+
const svgEl = container.querySelector('svg');
|
|
5373
|
+
if (!svgEl) return '';
|
|
5374
|
+
|
|
5375
|
+
if (theme === 'transparent') {
|
|
5376
|
+
svgEl.style.background = 'none';
|
|
5377
|
+
} else if (!svgEl.style.background) {
|
|
5378
|
+
svgEl.style.background = effectivePalette.bg;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
5382
|
+
svgEl.style.fontFamily = FONT_FAMILY;
|
|
5383
|
+
|
|
5384
|
+
const svgHtml = svgEl.outerHTML;
|
|
5385
|
+
if (options?.branding !== false) {
|
|
5386
|
+
const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
5387
|
+
return injectBranding(svgHtml, brandColor);
|
|
5388
|
+
}
|
|
5389
|
+
return svgHtml;
|
|
5390
|
+
} finally {
|
|
5391
|
+
document.body.removeChild(container);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5284
5395
|
if (detectedType === 'class') {
|
|
5285
5396
|
const { parseClassDiagram } = await import('./class/parser');
|
|
5286
5397
|
const { layoutClassDiagram } = await import('./class/layout');
|
package/src/dgmo-mermaid.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
|
|
6
6
|
import { resolveColor } from './colors';
|
|
7
|
+
import type { DgmoError } from './diagnostics';
|
|
8
|
+
import { makeDgmoError, formatDgmoError } from './diagnostics';
|
|
7
9
|
|
|
8
10
|
// ============================================================
|
|
9
11
|
// Types
|
|
@@ -29,6 +31,7 @@ export interface ParsedQuadrant {
|
|
|
29
31
|
bottomRight: QuadrantLabel | null;
|
|
30
32
|
};
|
|
31
33
|
points: { label: string; x: number; y: number; lineNumber: number }[];
|
|
34
|
+
diagnostics: DgmoError[];
|
|
32
35
|
error: string | null;
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -68,6 +71,7 @@ export function parseQuadrant(content: string): ParsedQuadrant {
|
|
|
68
71
|
bottomRight: null,
|
|
69
72
|
},
|
|
70
73
|
points: [],
|
|
74
|
+
diagnostics: [],
|
|
71
75
|
error: null,
|
|
72
76
|
};
|
|
73
77
|
|
|
@@ -154,7 +158,9 @@ export function parseQuadrant(content: string): ParsedQuadrant {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
if (result.points.length === 0) {
|
|
157
|
-
|
|
161
|
+
const diag = makeDgmoError(1, 'No data points found. Add lines like: Label: 0.5, 0.7');
|
|
162
|
+
result.diagnostics.push(diag);
|
|
163
|
+
result.error = formatDgmoError(diag);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
return result;
|
package/src/dgmo-router.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
// .dgmo Unified Format — Chart Type Router
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
-
import { looksLikeSequence } from './sequence/parser';
|
|
6
|
-
import { looksLikeFlowchart } from './graph/flowchart-parser';
|
|
7
|
-
import { looksLikeClassDiagram } from './class/parser';
|
|
8
|
-
import { looksLikeERDiagram } from './er/parser';
|
|
5
|
+
import { looksLikeSequence, parseSequenceDgmo } from './sequence/parser';
|
|
6
|
+
import { looksLikeFlowchart, parseFlowchart } from './graph/flowchart-parser';
|
|
7
|
+
import { looksLikeClassDiagram, parseClassDiagram } from './class/parser';
|
|
8
|
+
import { looksLikeERDiagram, parseERDiagram } from './er/parser';
|
|
9
|
+
import { parseChart } from './chart';
|
|
10
|
+
import { parseEChart } from './echarts';
|
|
11
|
+
import { parseD3 } from './d3';
|
|
12
|
+
import { parseOrg, looksLikeOrg } from './org/parser';
|
|
13
|
+
import { parseKanban } from './kanban/parser';
|
|
14
|
+
import type { DgmoError } from './diagnostics';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Framework identifiers used by the .dgmo router.
|
|
@@ -51,6 +57,7 @@ export const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
|
|
|
51
57
|
class: 'd3',
|
|
52
58
|
er: 'd3',
|
|
53
59
|
org: 'd3',
|
|
60
|
+
kanban: 'd3',
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
/**
|
|
@@ -82,6 +89,69 @@ export function parseDgmoChartType(content: string): string | null {
|
|
|
82
89
|
if (looksLikeFlowchart(content)) return 'flowchart';
|
|
83
90
|
if (looksLikeClassDiagram(content)) return 'class';
|
|
84
91
|
if (looksLikeERDiagram(content)) return 'er';
|
|
92
|
+
if (looksLikeOrg(content)) return 'org';
|
|
85
93
|
|
|
86
94
|
return null;
|
|
87
95
|
}
|
|
96
|
+
|
|
97
|
+
// Standard chart types parsed by parseChart (then rendered via ECharts)
|
|
98
|
+
const STANDARD_CHART_TYPES = new Set([
|
|
99
|
+
'bar', 'line', 'multi-line', 'area', 'pie', 'doughnut',
|
|
100
|
+
'radar', 'polar-area', 'bar-stacked',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// ECharts-native types parsed by parseEChart
|
|
104
|
+
const ECHART_TYPES = new Set([
|
|
105
|
+
'scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse DGMO content and return diagnostics without rendering.
|
|
110
|
+
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
111
|
+
*/
|
|
112
|
+
export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
113
|
+
const chartType = parseDgmoChartType(content);
|
|
114
|
+
|
|
115
|
+
if (!chartType) {
|
|
116
|
+
// No chart type detected — try D3 parser as fallback (it handles missing chart: line)
|
|
117
|
+
const parsed = parseD3(content);
|
|
118
|
+
return { diagnostics: parsed.diagnostics };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (chartType === 'sequence') {
|
|
122
|
+
const parsed = parseSequenceDgmo(content);
|
|
123
|
+
return { diagnostics: parsed.diagnostics };
|
|
124
|
+
}
|
|
125
|
+
if (chartType === 'flowchart') {
|
|
126
|
+
const parsed = parseFlowchart(content);
|
|
127
|
+
return { diagnostics: parsed.diagnostics };
|
|
128
|
+
}
|
|
129
|
+
if (chartType === 'class') {
|
|
130
|
+
const parsed = parseClassDiagram(content);
|
|
131
|
+
return { diagnostics: parsed.diagnostics };
|
|
132
|
+
}
|
|
133
|
+
if (chartType === 'er') {
|
|
134
|
+
const parsed = parseERDiagram(content);
|
|
135
|
+
return { diagnostics: parsed.diagnostics };
|
|
136
|
+
}
|
|
137
|
+
if (chartType === 'org') {
|
|
138
|
+
const parsed = parseOrg(content);
|
|
139
|
+
return { diagnostics: parsed.diagnostics };
|
|
140
|
+
}
|
|
141
|
+
if (chartType === 'kanban') {
|
|
142
|
+
const parsed = parseKanban(content);
|
|
143
|
+
return { diagnostics: parsed.diagnostics };
|
|
144
|
+
}
|
|
145
|
+
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
146
|
+
const parsed = parseChart(content);
|
|
147
|
+
return { diagnostics: parsed.diagnostics };
|
|
148
|
+
}
|
|
149
|
+
if (ECHART_TYPES.has(chartType)) {
|
|
150
|
+
const parsed = parseEChart(content);
|
|
151
|
+
return { diagnostics: parsed.diagnostics };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// D3 types (slope, wordcloud, arc, timeline, venn, quadrant)
|
|
155
|
+
const parsed = parseD3(content);
|
|
156
|
+
return { diagnostics: parsed.diagnostics };
|
|
157
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Structured Diagnostic Types
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export type DgmoSeverity = 'error' | 'warning';
|
|
6
|
+
|
|
7
|
+
export interface DgmoError {
|
|
8
|
+
line: number; // 1-based (0 = no line info)
|
|
9
|
+
column?: number; // optional 1-based column
|
|
10
|
+
message: string; // without "Line N:" prefix
|
|
11
|
+
severity: DgmoSeverity;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makeDgmoError(
|
|
15
|
+
line: number,
|
|
16
|
+
message: string,
|
|
17
|
+
severity: DgmoSeverity = 'error'
|
|
18
|
+
): DgmoError {
|
|
19
|
+
return { line, message, severity };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDgmoError(err: DgmoError): string {
|
|
23
|
+
return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// "Did you mean?" Suggestions
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple Levenshtein distance between two strings.
|
|
32
|
+
*/
|
|
33
|
+
function levenshtein(a: string, b: string): number {
|
|
34
|
+
const m = a.length;
|
|
35
|
+
const n = b.length;
|
|
36
|
+
const dp: number[] = Array(n + 1)
|
|
37
|
+
.fill(0)
|
|
38
|
+
.map((_, i) => i);
|
|
39
|
+
|
|
40
|
+
for (let i = 1; i <= m; i++) {
|
|
41
|
+
let prev = dp[0];
|
|
42
|
+
dp[0] = i;
|
|
43
|
+
for (let j = 1; j <= n; j++) {
|
|
44
|
+
const tmp = dp[j];
|
|
45
|
+
dp[j] =
|
|
46
|
+
a[i - 1] === b[j - 1]
|
|
47
|
+
? prev
|
|
48
|
+
: 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
49
|
+
prev = tmp;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return dp[n];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a "did you mean 'X'?" suggestion if the input is close to one of the candidates.
|
|
57
|
+
* Returns null if no good match is found.
|
|
58
|
+
* Threshold: distance ≤ max(2, floor(input.length / 3))
|
|
59
|
+
*/
|
|
60
|
+
export function suggest(input: string, candidates: readonly string[]): string | null {
|
|
61
|
+
if (!input || candidates.length === 0) return null;
|
|
62
|
+
const lower = input.toLowerCase();
|
|
63
|
+
const threshold = Math.max(2, Math.floor(lower.length / 3));
|
|
64
|
+
|
|
65
|
+
let best: string | null = null;
|
|
66
|
+
let bestDist = Infinity;
|
|
67
|
+
|
|
68
|
+
for (const c of candidates) {
|
|
69
|
+
const dist = levenshtein(lower, c.toLowerCase());
|
|
70
|
+
if (dist < bestDist && dist <= threshold && dist > 0) {
|
|
71
|
+
bestDist = dist;
|
|
72
|
+
best = c;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return best ? `Did you mean '${best}'?` : null;
|
|
77
|
+
}
|
package/src/echarts.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface ParsedHeatmapRow {
|
|
|
52
52
|
lineNumber: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
import type { DgmoError } from './diagnostics';
|
|
56
|
+
|
|
55
57
|
export interface ParsedEChart {
|
|
56
58
|
type: EChartsChartType;
|
|
57
59
|
title?: string;
|
|
@@ -72,6 +74,7 @@ export interface ParsedEChart {
|
|
|
72
74
|
sizelabel?: string;
|
|
73
75
|
showLabels?: boolean;
|
|
74
76
|
categoryColors?: Record<string, string>;
|
|
77
|
+
diagnostics: DgmoError[];
|
|
75
78
|
error?: string;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -84,6 +87,7 @@ import type { PaletteColors } from './palettes';
|
|
|
84
87
|
import { getSeriesColors, getSegmentColors } from './palettes';
|
|
85
88
|
import { parseChart } from './chart';
|
|
86
89
|
import type { ParsedChart } from './chart';
|
|
90
|
+
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
87
91
|
|
|
88
92
|
// ============================================================
|
|
89
93
|
// Parser
|
|
@@ -111,6 +115,7 @@ export function parseEChart(
|
|
|
111
115
|
const result: ParsedEChart = {
|
|
112
116
|
type: 'scatter',
|
|
113
117
|
data: [],
|
|
118
|
+
diagnostics: [],
|
|
114
119
|
};
|
|
115
120
|
|
|
116
121
|
// Track current category for grouped scatter charts
|
|
@@ -168,7 +173,13 @@ export function parseEChart(
|
|
|
168
173
|
) {
|
|
169
174
|
result.type = chartType;
|
|
170
175
|
} else {
|
|
171
|
-
|
|
176
|
+
const validTypes = ['scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel'];
|
|
177
|
+
let msg = `Unsupported chart type: ${value}. Supported types: ${validTypes.join(', ')}.`;
|
|
178
|
+
const hint = suggest(chartType, validTypes);
|
|
179
|
+
if (hint) msg += ` ${hint}`;
|
|
180
|
+
const diag = makeDgmoError(lineNumber, msg);
|
|
181
|
+
result.diagnostics.push(diag);
|
|
182
|
+
result.error = formatDgmoError(diag);
|
|
172
183
|
return result;
|
|
173
184
|
}
|
|
174
185
|
continue;
|
|
@@ -341,42 +352,40 @@ export function parseEChart(
|
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
|
|
355
|
+
const warn = (line: number, message: string): void => {
|
|
356
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
357
|
+
};
|
|
358
|
+
|
|
344
359
|
if (!result.error) {
|
|
345
360
|
if (result.type === 'sankey') {
|
|
346
361
|
if (!result.links || result.links.length === 0) {
|
|
347
|
-
|
|
348
|
-
'No links found. Add links in format: Source -> Target: 123';
|
|
362
|
+
warn(1, 'No links found. Add links in format: Source -> Target: 123');
|
|
349
363
|
}
|
|
350
364
|
} else if (result.type === 'chord') {
|
|
351
365
|
if (!result.links || result.links.length === 0) {
|
|
352
|
-
|
|
353
|
-
'No links found. Add links in format: Source -> Target: 123';
|
|
366
|
+
warn(1, 'No links found. Add links in format: Source -> Target: 123');
|
|
354
367
|
}
|
|
355
368
|
} else if (result.type === 'function') {
|
|
356
369
|
if (!result.functions || result.functions.length === 0) {
|
|
357
|
-
|
|
358
|
-
'No functions found. Add functions in format: Name: expression';
|
|
370
|
+
warn(1, 'No functions found. Add functions in format: Name: expression');
|
|
359
371
|
}
|
|
360
372
|
if (!result.xRange) {
|
|
361
373
|
result.xRange = { min: -10, max: 10 }; // Default range
|
|
362
374
|
}
|
|
363
375
|
} else if (result.type === 'scatter') {
|
|
364
376
|
if (!result.scatterPoints || result.scatterPoints.length === 0) {
|
|
365
|
-
|
|
366
|
-
'No scatter points found. Add points in format: Name: x, y or Name: x, y, size';
|
|
377
|
+
warn(1, 'No scatter points found. Add points in format: Name: x, y or Name: x, y, size');
|
|
367
378
|
}
|
|
368
379
|
} else if (result.type === 'heatmap') {
|
|
369
380
|
if (!result.heatmapRows || result.heatmapRows.length === 0) {
|
|
370
|
-
|
|
371
|
-
'No heatmap data found. Add data in format: RowLabel: val1, val2, val3';
|
|
381
|
+
warn(1, 'No heatmap data found. Add data in format: RowLabel: val1, val2, val3');
|
|
372
382
|
}
|
|
373
383
|
if (!result.columns || result.columns.length === 0) {
|
|
374
|
-
|
|
375
|
-
'No columns defined. Add columns in format: columns: Col1, Col2, Col3';
|
|
384
|
+
warn(1, 'No columns defined. Add columns in format: columns: Col1, Col2, Col3');
|
|
376
385
|
}
|
|
377
386
|
} else if (result.type === 'funnel') {
|
|
378
387
|
if (result.data.length === 0) {
|
|
379
|
-
|
|
388
|
+
warn(1, 'No data found. Add data in format: Label: value');
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
}
|