@diagrammo/dgmo 0.2.20 → 0.2.22
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 +101 -99
- package/dist/index.cjs +983 -282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +976 -281
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/d3.ts +44 -1
- package/src/dgmo-router.ts +8 -1
- package/src/echarts.ts +7 -8
- package/src/index.ts +11 -0
- package/src/kanban/mutations.ts +183 -0
- package/src/kanban/parser.ts +389 -0
- package/src/kanban/renderer.ts +566 -0
- package/src/kanban/types.ts +45 -0
- package/src/org/layout.ts +92 -66
- package/src/org/parser.ts +15 -1
- package/src/org/renderer.ts +94 -167
- package/src/sequence/renderer.ts +7 -5
package/package.json
CHANGED
package/src/d3.ts
CHANGED
|
@@ -5281,7 +5281,7 @@ export async function renderD3ForExport(
|
|
|
5281
5281
|
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
5282
5282
|
|
|
5283
5283
|
const orgParsed = parseOrg(content, effectivePalette);
|
|
5284
|
-
if (orgParsed.error
|
|
5284
|
+
if (orgParsed.error) return '';
|
|
5285
5285
|
|
|
5286
5286
|
// Apply interactive collapse state when provided
|
|
5287
5287
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
@@ -5349,6 +5349,49 @@ export async function renderD3ForExport(
|
|
|
5349
5349
|
}
|
|
5350
5350
|
}
|
|
5351
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
|
+
|
|
5352
5395
|
if (detectedType === 'class') {
|
|
5353
5396
|
const { parseClassDiagram } = await import('./class/parser');
|
|
5354
5397
|
const { layoutClassDiagram } = await import('./class/layout');
|
package/src/dgmo-router.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { looksLikeERDiagram, parseERDiagram } from './er/parser';
|
|
|
9
9
|
import { parseChart } from './chart';
|
|
10
10
|
import { parseEChart } from './echarts';
|
|
11
11
|
import { parseD3 } from './d3';
|
|
12
|
-
import { parseOrg } from './org/parser';
|
|
12
|
+
import { parseOrg, looksLikeOrg } from './org/parser';
|
|
13
|
+
import { parseKanban } from './kanban/parser';
|
|
13
14
|
import type { DgmoError } from './diagnostics';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -56,6 +57,7 @@ export const DGMO_CHART_TYPE_MAP: Record<string, DgmoFramework> = {
|
|
|
56
57
|
class: 'd3',
|
|
57
58
|
er: 'd3',
|
|
58
59
|
org: 'd3',
|
|
60
|
+
kanban: 'd3',
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -87,6 +89,7 @@ export function parseDgmoChartType(content: string): string | null {
|
|
|
87
89
|
if (looksLikeFlowchart(content)) return 'flowchart';
|
|
88
90
|
if (looksLikeClassDiagram(content)) return 'class';
|
|
89
91
|
if (looksLikeERDiagram(content)) return 'er';
|
|
92
|
+
if (looksLikeOrg(content)) return 'org';
|
|
90
93
|
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
@@ -135,6 +138,10 @@ export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
|
135
138
|
const parsed = parseOrg(content);
|
|
136
139
|
return { diagnostics: parsed.diagnostics };
|
|
137
140
|
}
|
|
141
|
+
if (chartType === 'kanban') {
|
|
142
|
+
const parsed = parseKanban(content);
|
|
143
|
+
return { diagnostics: parsed.diagnostics };
|
|
144
|
+
}
|
|
138
145
|
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
139
146
|
const parsed = parseChart(content);
|
|
140
147
|
return { diagnostics: parsed.diagnostics };
|
package/src/echarts.ts
CHANGED
|
@@ -1198,16 +1198,15 @@ function buildFunnelOption(
|
|
|
1198
1198
|
const val = p.value;
|
|
1199
1199
|
const prev = prevValueMap.get(p.name) ?? val;
|
|
1200
1200
|
const isFirst = p.dataIndex === 0;
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!isFirst && topValue > 0) {
|
|
1201
|
+
if (isFirst) return '';
|
|
1202
|
+
const parts: string[] = [];
|
|
1203
|
+
const stepDrop = ((1 - val / prev) * 100).toFixed(1);
|
|
1204
|
+
parts.push(`Step drop-off: ${stepDrop}%`);
|
|
1205
|
+
if (topValue > 0) {
|
|
1207
1206
|
const totalDrop = ((1 - val / topValue) * 100).toFixed(1);
|
|
1208
|
-
|
|
1207
|
+
parts.push(`Overall drop-off: ${totalDrop}%`);
|
|
1209
1208
|
}
|
|
1210
|
-
return
|
|
1209
|
+
return parts.join('<br/>');
|
|
1211
1210
|
},
|
|
1212
1211
|
},
|
|
1213
1212
|
series: [
|
package/src/index.ts
CHANGED
|
@@ -144,6 +144,17 @@ export type {
|
|
|
144
144
|
|
|
145
145
|
export { renderOrg, renderOrgForExport } from './org/renderer';
|
|
146
146
|
|
|
147
|
+
export { parseKanban } from './kanban/parser';
|
|
148
|
+
export type {
|
|
149
|
+
ParsedKanban,
|
|
150
|
+
KanbanColumn,
|
|
151
|
+
KanbanCard,
|
|
152
|
+
KanbanTagGroup,
|
|
153
|
+
KanbanTagEntry,
|
|
154
|
+
} from './kanban/types';
|
|
155
|
+
export { computeCardMove, computeCardArchive, isArchiveColumn } from './kanban/mutations';
|
|
156
|
+
export { renderKanban, renderKanbanForExport } from './kanban/renderer';
|
|
157
|
+
|
|
147
158
|
export { collapseOrgTree } from './org/collapse';
|
|
148
159
|
export type { CollapsedOrgResult } from './org/collapse';
|
|
149
160
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ParsedKanban, KanbanCard, KanbanColumn } from './types';
|
|
2
|
+
|
|
3
|
+
const ARCHIVE_COLUMN_NAME = 'archive';
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// computeCardMove — pure function for source text mutation
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute new file content after moving a card to a different position.
|
|
11
|
+
*
|
|
12
|
+
* @param content - original file content string
|
|
13
|
+
* @param parsed - parsed kanban board
|
|
14
|
+
* @param cardId - id of the card to move
|
|
15
|
+
* @param targetColumnId - id of the destination column
|
|
16
|
+
* @param targetIndex - position within target column (0 = first card)
|
|
17
|
+
* @returns new content string, or null if move is invalid
|
|
18
|
+
*/
|
|
19
|
+
export function computeCardMove(
|
|
20
|
+
content: string,
|
|
21
|
+
parsed: ParsedKanban,
|
|
22
|
+
cardId: string,
|
|
23
|
+
targetColumnId: string,
|
|
24
|
+
targetIndex: number
|
|
25
|
+
): string | null {
|
|
26
|
+
// Find source card and column
|
|
27
|
+
let sourceCard: KanbanCard | null = null;
|
|
28
|
+
let sourceColumn: KanbanColumn | null = null;
|
|
29
|
+
|
|
30
|
+
for (const col of parsed.columns) {
|
|
31
|
+
for (const card of col.cards) {
|
|
32
|
+
if (card.id === cardId) {
|
|
33
|
+
sourceCard = card;
|
|
34
|
+
sourceColumn = col;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (sourceCard) break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!sourceCard || !sourceColumn) return null;
|
|
42
|
+
|
|
43
|
+
const targetColumn = parsed.columns.find((c) => c.id === targetColumnId);
|
|
44
|
+
if (!targetColumn) return null;
|
|
45
|
+
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
|
|
48
|
+
// Extract the card's lines (0-based indices)
|
|
49
|
+
const startIdx = sourceCard.lineNumber - 1;
|
|
50
|
+
const endIdx = sourceCard.endLineNumber - 1;
|
|
51
|
+
const cardLines = lines.slice(startIdx, endIdx + 1);
|
|
52
|
+
|
|
53
|
+
// Remove the card lines from content
|
|
54
|
+
const withoutCard = [
|
|
55
|
+
...lines.slice(0, startIdx),
|
|
56
|
+
...lines.slice(endIdx + 1),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Compute insertion point (0-based index in withoutCard)
|
|
60
|
+
let insertIdx: number;
|
|
61
|
+
|
|
62
|
+
// Adjust target column and card line numbers after removal
|
|
63
|
+
// Lines after the removed range shift up by the number of removed lines
|
|
64
|
+
const removedCount = endIdx - startIdx + 1;
|
|
65
|
+
const adjustLine = (ln: number): number => {
|
|
66
|
+
// ln is 1-based
|
|
67
|
+
if (ln > endIdx + 1) return ln - removedCount;
|
|
68
|
+
return ln;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (targetIndex === 0) {
|
|
72
|
+
// Insert right after column header line
|
|
73
|
+
const adjColLine = adjustLine(targetColumn.lineNumber);
|
|
74
|
+
insertIdx = adjColLine; // 0-based: insert after the column header line
|
|
75
|
+
} else {
|
|
76
|
+
// Insert after the preceding card's last line
|
|
77
|
+
// Get the cards in the target column, excluding the moved card
|
|
78
|
+
const targetCards = targetColumn.cards.filter((c) => c.id !== cardId);
|
|
79
|
+
const clampedIdx = Math.min(targetIndex, targetCards.length);
|
|
80
|
+
const precedingCard = targetCards[clampedIdx - 1];
|
|
81
|
+
if (!precedingCard) {
|
|
82
|
+
// Fallback: after column header
|
|
83
|
+
const adjColLine = adjustLine(targetColumn.lineNumber);
|
|
84
|
+
insertIdx = adjColLine;
|
|
85
|
+
} else {
|
|
86
|
+
const adjEndLine = adjustLine(precedingCard.endLineNumber);
|
|
87
|
+
insertIdx = adjEndLine; // 0-based: insert after this line
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Splice the card lines into the new position
|
|
92
|
+
const result = [
|
|
93
|
+
...withoutCard.slice(0, insertIdx),
|
|
94
|
+
...cardLines,
|
|
95
|
+
...withoutCard.slice(insertIdx),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return result.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================
|
|
102
|
+
// computeCardArchive — move card to an Archive section
|
|
103
|
+
// ============================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Move a card to the Archive section at the end of the file.
|
|
107
|
+
* Creates `== Archive ==` if it doesn't exist.
|
|
108
|
+
*
|
|
109
|
+
* @returns new content string, or null if the card is not found
|
|
110
|
+
*/
|
|
111
|
+
export function computeCardArchive(
|
|
112
|
+
content: string,
|
|
113
|
+
parsed: ParsedKanban,
|
|
114
|
+
cardId: string
|
|
115
|
+
): string | null {
|
|
116
|
+
let sourceCard: KanbanCard | null = null;
|
|
117
|
+
for (const col of parsed.columns) {
|
|
118
|
+
for (const card of col.cards) {
|
|
119
|
+
if (card.id === cardId) {
|
|
120
|
+
sourceCard = card;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (sourceCard) break;
|
|
125
|
+
}
|
|
126
|
+
if (!sourceCard) return null;
|
|
127
|
+
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
|
|
130
|
+
// Extract card lines
|
|
131
|
+
const startIdx = sourceCard.lineNumber - 1;
|
|
132
|
+
const endIdx = sourceCard.endLineNumber - 1;
|
|
133
|
+
const cardLines = lines.slice(startIdx, endIdx + 1);
|
|
134
|
+
|
|
135
|
+
// Remove card from its current position
|
|
136
|
+
const withoutCard = [
|
|
137
|
+
...lines.slice(0, startIdx),
|
|
138
|
+
...lines.slice(endIdx + 1),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// Check if an Archive column already exists
|
|
142
|
+
const archiveCol = parsed.columns.find(
|
|
143
|
+
(c) => c.name.toLowerCase() === ARCHIVE_COLUMN_NAME
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (archiveCol) {
|
|
147
|
+
// Append to existing archive column
|
|
148
|
+
// Find the last line of the archive column (after removal adjustment)
|
|
149
|
+
const removedCount = endIdx - startIdx + 1;
|
|
150
|
+
let archiveEndLine = archiveCol.lineNumber;
|
|
151
|
+
if (archiveCol.cards.length > 0) {
|
|
152
|
+
const lastCard = archiveCol.cards[archiveCol.cards.length - 1];
|
|
153
|
+
archiveEndLine = lastCard.endLineNumber;
|
|
154
|
+
}
|
|
155
|
+
// Adjust for removed lines
|
|
156
|
+
if (archiveEndLine > endIdx + 1) {
|
|
157
|
+
archiveEndLine -= removedCount;
|
|
158
|
+
}
|
|
159
|
+
// Insert after the archive end (0-based in withoutCard)
|
|
160
|
+
const insertIdx = archiveEndLine; // archiveEndLine is 1-based, so this is after that line
|
|
161
|
+
return [
|
|
162
|
+
...withoutCard.slice(0, insertIdx),
|
|
163
|
+
...cardLines,
|
|
164
|
+
...withoutCard.slice(insertIdx),
|
|
165
|
+
].join('\n');
|
|
166
|
+
} else {
|
|
167
|
+
// Create archive section at end of file
|
|
168
|
+
// Ensure trailing newline before the new section
|
|
169
|
+
const trimmedEnd = withoutCard.length > 0 && withoutCard[withoutCard.length - 1].trim() === ''
|
|
170
|
+
? withoutCard
|
|
171
|
+
: [...withoutCard, ''];
|
|
172
|
+
return [
|
|
173
|
+
...trimmedEnd,
|
|
174
|
+
'== Archive ==',
|
|
175
|
+
...cardLines,
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Check if a column name is the archive column (case-insensitive). */
|
|
181
|
+
export function isArchiveColumn(name: string): boolean {
|
|
182
|
+
return name.toLowerCase() === ARCHIVE_COLUMN_NAME;
|
|
183
|
+
}
|