@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 || orgParsed.roots.length === 0) return '';
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');
@@ -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
- let html = `<strong>${p.name}</strong>: ${val}`;
1202
- if (!isFirst) {
1203
- const stepDrop = ((1 - val / prev) * 100).toFixed(1);
1204
- html += `<br/>Step drop-off: ${stepDrop}%`;
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
- html += `<br/>Overall drop-off: ${totalDrop}%`;
1207
+ parts.push(`Overall drop-off: ${totalDrop}%`);
1209
1208
  }
1210
- return html;
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
+ }