@diagrammo/dgmo 0.8.21 → 0.8.23
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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +145 -93
- package/dist/editor.cjs +20 -3
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +20 -3
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +15 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +15 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +20843 -14937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -17
- package/dist/index.d.ts +426 -17
- package/dist/index.js +20795 -14912
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +380 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +179 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +337 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/registry.json +6 -0
- package/docs/language-reference.md +177 -6
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +11 -1
- package/src/boxes-and-lines/layout.ts +309 -33
- package/src/boxes-and-lines/parser.ts +86 -10
- package/src/boxes-and-lines/renderer.ts +250 -91
- package/src/boxes-and-lines/types.ts +1 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/cli.ts +5 -35
- package/src/completion.ts +233 -41
- package/src/cycle/layout.ts +723 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +566 -0
- package/src/cycle/types.ts +98 -0
- package/src/d3.ts +107 -8
- package/src/dgmo-router.ts +82 -3
- package/src/echarts.ts +8 -5
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +17 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/state-parser.ts +5 -10
- package/src/index.ts +63 -2
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +32 -8
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/internal.ts +16 -0
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1521 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +31 -15
- package/src/mindmap/parser.ts +12 -18
- package/src/mindmap/renderer.ts +14 -13
- package/src/mindmap/text-wrap.ts +22 -12
- package/src/mindmap/types.ts +2 -2
- package/src/org/collapse.ts +81 -0
- package/src/org/parser.ts +2 -6
- package/src/org/renderer.ts +212 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +62 -20
- package/src/sequence/renderer.ts +146 -40
- package/src/sharing.ts +1 -0
- package/src/sitemap/layout.ts +21 -6
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1112 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/legend-layout.ts +3 -1
- package/src/utils/parsing.ts +47 -7
- package/src/utils/tag-groups.ts +46 -60
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DgmoError } from '../diagnostics';
|
|
2
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
3
|
+
|
|
4
|
+
export interface JourneyMapAnnotation {
|
|
5
|
+
type: 'pain' | 'opportunity' | 'thought';
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface JourneyMapStep {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
score?: number;
|
|
13
|
+
emotionLabel?: string;
|
|
14
|
+
tags: Record<string, string>;
|
|
15
|
+
annotations: JourneyMapAnnotation[];
|
|
16
|
+
description?: string;
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
endLineNumber: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JourneyMapPhase {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
steps: JourneyMapStep[];
|
|
25
|
+
lineNumber: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JourneyMapPersona {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
color?: string;
|
|
32
|
+
lineNumber: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedJourneyMap {
|
|
36
|
+
type: 'journey-map';
|
|
37
|
+
title?: string;
|
|
38
|
+
titleLineNumber?: number;
|
|
39
|
+
persona?: JourneyMapPersona;
|
|
40
|
+
phases: JourneyMapPhase[];
|
|
41
|
+
/** Flat-mode steps (not inside any phase) */
|
|
42
|
+
steps: JourneyMapStep[];
|
|
43
|
+
tagGroups: TagGroup[];
|
|
44
|
+
options: Record<string, string>;
|
|
45
|
+
diagnostics: DgmoError[];
|
|
46
|
+
error: string | null;
|
|
47
|
+
}
|
package/src/kanban/parser.ts
CHANGED
|
@@ -387,8 +387,8 @@ function parseCardLine(
|
|
|
387
387
|
lineNumber: number,
|
|
388
388
|
counter: number,
|
|
389
389
|
aliasMap: Map<string, string>,
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
_palette?: PaletteColors,
|
|
391
|
+
_diagnostics?: import('../diagnostics').DgmoError[]
|
|
392
392
|
): KanbanCard {
|
|
393
393
|
// Split on first pipe: Title | tag: value, tag: value
|
|
394
394
|
const pipeIdx = trimmed.indexOf('|');
|
|
@@ -402,13 +402,7 @@ function parseCardLine(
|
|
|
402
402
|
rawTitle = trimmed;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
|
|
406
|
-
const { label: title, color } = extractColor(
|
|
407
|
-
rawTitle,
|
|
408
|
-
palette,
|
|
409
|
-
diagnostics,
|
|
410
|
-
lineNumber
|
|
411
|
-
);
|
|
405
|
+
const title = rawTitle;
|
|
412
406
|
|
|
413
407
|
// Parse tags: comma-separated key: value pairs
|
|
414
408
|
const tags: Record<string, string> = {};
|
|
@@ -431,6 +425,5 @@ function parseCardLine(
|
|
|
431
425
|
details: [],
|
|
432
426
|
lineNumber,
|
|
433
427
|
endLineNumber: lineNumber,
|
|
434
|
-
color,
|
|
435
428
|
};
|
|
436
429
|
}
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -499,17 +499,21 @@ export function renderKanban(
|
|
|
499
499
|
.attr('fill', palette.text)
|
|
500
500
|
.text(col.name);
|
|
501
501
|
|
|
502
|
-
// WIP limit badge
|
|
503
|
-
|
|
504
|
-
const wipExceeded =
|
|
505
|
-
|
|
506
|
-
const
|
|
502
|
+
// Card count / WIP limit badge (right-aligned)
|
|
503
|
+
{
|
|
504
|
+
const wipExceeded =
|
|
505
|
+
col.wipLimit != null && col.cards.length > col.wipLimit;
|
|
506
|
+
const badgeText =
|
|
507
|
+
col.wipLimit != null
|
|
508
|
+
? `${col.cards.length}/${col.wipLimit}`
|
|
509
|
+
: String(col.cards.length);
|
|
507
510
|
g.append('text')
|
|
508
|
-
.attr('x', colLayout.x +
|
|
511
|
+
.attr('x', colLayout.x + colLayout.width - COLUMN_PADDING)
|
|
509
512
|
.attr(
|
|
510
513
|
'y',
|
|
511
514
|
colLayout.y + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
512
515
|
)
|
|
516
|
+
.attr('text-anchor', 'end')
|
|
513
517
|
.attr('font-size', WIP_FONT_SIZE)
|
|
514
518
|
.attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
|
|
515
519
|
.attr('font-weight', wipExceeded ? 'bold' : 'normal')
|
|
@@ -978,6 +982,26 @@ function renderSwimlaneBoard(
|
|
|
978
982
|
.attr('font-weight', 'bold')
|
|
979
983
|
.attr('fill', palette.text)
|
|
980
984
|
.text(col.name);
|
|
985
|
+
|
|
986
|
+
// Card count (right-aligned)
|
|
987
|
+
const wipExceeded =
|
|
988
|
+
col.wipLimit != null && col.cards.length > col.wipLimit;
|
|
989
|
+
const badgeText =
|
|
990
|
+
col.wipLimit != null
|
|
991
|
+
? `${col.cards.length}/${col.wipLimit}`
|
|
992
|
+
: String(col.cards.length);
|
|
993
|
+
headerG
|
|
994
|
+
.append('text')
|
|
995
|
+
.attr('x', colInfo.x + colInfo.width - COLUMN_PADDING)
|
|
996
|
+
.attr(
|
|
997
|
+
'y',
|
|
998
|
+
grid.startY + COLUMN_HEADER_HEIGHT / 2 + WIP_FONT_SIZE / 2 - 1
|
|
999
|
+
)
|
|
1000
|
+
.attr('text-anchor', 'end')
|
|
1001
|
+
.attr('font-size', WIP_FONT_SIZE)
|
|
1002
|
+
.attr('fill', wipExceeded ? palette.colors.red : palette.textMuted)
|
|
1003
|
+
.attr('font-weight', wipExceeded ? 'bold' : 'normal')
|
|
1004
|
+
.text(badgeText);
|
|
981
1005
|
}
|
|
982
1006
|
}
|
|
983
1007
|
|
|
@@ -1031,7 +1055,7 @@ function renderSwimlaneBoard(
|
|
|
1031
1055
|
.attr('fill', palette.textMuted)
|
|
1032
1056
|
.text(`${lane.bucket.laneName} (${totalCards})`);
|
|
1033
1057
|
} else {
|
|
1034
|
-
// Expanded: name
|
|
1058
|
+
// Expanded: name only (count omitted to match app view)
|
|
1035
1059
|
headerG
|
|
1036
1060
|
.append('text')
|
|
1037
1061
|
.attr('x', labelX)
|
|
@@ -1040,14 +1064,6 @@ function renderSwimlaneBoard(
|
|
|
1040
1064
|
.attr('font-weight', 'bold')
|
|
1041
1065
|
.attr('fill', lane.bucket.isFallback ? palette.textMuted : palette.text)
|
|
1042
1066
|
.text(lane.bucket.laneName);
|
|
1043
|
-
|
|
1044
|
-
headerG
|
|
1045
|
-
.append('text')
|
|
1046
|
-
.attr('x', labelX)
|
|
1047
|
-
.attr('y', 36)
|
|
1048
|
-
.attr('font-size', 10)
|
|
1049
|
-
.attr('fill', palette.textMuted)
|
|
1050
|
-
.text(`(${totalCards})`);
|
|
1051
1067
|
}
|
|
1052
1068
|
|
|
1053
1069
|
if (isLaneCollapsed) {
|
package/src/mindmap/parser.ts
CHANGED
|
@@ -16,13 +16,12 @@ import {
|
|
|
16
16
|
OPTION_NOCOLON_RE,
|
|
17
17
|
} from '../utils/parsing';
|
|
18
18
|
import type { MindmapNode, ParsedMindmap } from './types';
|
|
19
|
+
import { tryStripDescriptionKeyword } from '../utils/description-helpers';
|
|
19
20
|
|
|
20
21
|
// ============================================================
|
|
21
22
|
// Constants
|
|
22
23
|
// ============================================================
|
|
23
24
|
|
|
24
|
-
const DESCRIPTION_RE = /^description:\s*(.*)$/i;
|
|
25
|
-
|
|
26
25
|
/** Known mindmap options (key-value). */
|
|
27
26
|
const KNOWN_OPTIONS = new Set(['active-tag']);
|
|
28
27
|
|
|
@@ -111,8 +110,8 @@ export function parseMindmap(
|
|
|
111
110
|
return fail(lineNumber, msg);
|
|
112
111
|
}
|
|
113
112
|
if (firstLine.title) {
|
|
114
|
-
// Title IS the root
|
|
115
|
-
const
|
|
113
|
+
// Title IS the root
|
|
114
|
+
const label = firstLine.title;
|
|
116
115
|
result.title = label;
|
|
117
116
|
result.titleLineNumber = lineNumber;
|
|
118
117
|
|
|
@@ -124,7 +123,6 @@ export function parseMindmap(
|
|
|
124
123
|
children: [],
|
|
125
124
|
parentId: null,
|
|
126
125
|
lineNumber,
|
|
127
|
-
color,
|
|
128
126
|
};
|
|
129
127
|
result.roots.push(titleRoot);
|
|
130
128
|
// Push title root onto indent stack at indent -1 so all indent-0 lines become children
|
|
@@ -208,14 +206,14 @@ export function parseMindmap(
|
|
|
208
206
|
|
|
209
207
|
const indent = measureIndent(line);
|
|
210
208
|
|
|
211
|
-
// Check for indented `description: text` metadata
|
|
209
|
+
// Check for indented `description: text` or `description text` metadata
|
|
212
210
|
if (indent > 0) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
211
|
+
const descResult = tryStripDescriptionKeyword(trimmed);
|
|
212
|
+
if (descResult.isKeyword) {
|
|
215
213
|
// Find parent node from indent stack
|
|
216
214
|
const parent = findMetadataParent(indent, indentStack);
|
|
217
215
|
if (parent) {
|
|
218
|
-
const descValue =
|
|
216
|
+
const descValue = descResult.text.trim();
|
|
219
217
|
if (!descValue) {
|
|
220
218
|
// Empty description: silently skip
|
|
221
219
|
continue;
|
|
@@ -228,10 +226,8 @@ export function parseMindmap(
|
|
|
228
226
|
);
|
|
229
227
|
continue;
|
|
230
228
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
parent.description = descValue;
|
|
234
|
-
}
|
|
229
|
+
if (!parent.description) parent.description = [];
|
|
230
|
+
parent.description.push(descValue);
|
|
235
231
|
continue;
|
|
236
232
|
}
|
|
237
233
|
}
|
|
@@ -300,19 +296,18 @@ function parseNodeLine(
|
|
|
300
296
|
warnFn: (line: number, msg: string) => void
|
|
301
297
|
): MindmapNode {
|
|
302
298
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
303
|
-
const
|
|
304
|
-
const { label, color } = extractColor(rawLabel, palette);
|
|
299
|
+
const label = segments[0];
|
|
305
300
|
|
|
306
301
|
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
307
302
|
warnFn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
308
303
|
);
|
|
309
304
|
|
|
310
305
|
// Extract description from pipe metadata as a dedicated field
|
|
311
|
-
let description: string | undefined;
|
|
306
|
+
let description: string[] | undefined;
|
|
312
307
|
if ('description' in metadata) {
|
|
313
308
|
const descVal = metadata['description'].trim();
|
|
314
309
|
if (descVal) {
|
|
315
|
-
description = descVal;
|
|
310
|
+
description = [descVal];
|
|
316
311
|
}
|
|
317
312
|
delete metadata['description'];
|
|
318
313
|
}
|
|
@@ -332,7 +327,6 @@ function parseNodeLine(
|
|
|
332
327
|
children: [],
|
|
333
328
|
parentId: null,
|
|
334
329
|
lineNumber,
|
|
335
|
-
color,
|
|
336
330
|
collapsed,
|
|
337
331
|
};
|
|
338
332
|
}
|
package/src/mindmap/renderer.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type { MindmapLayoutResult } from './types';
|
|
|
15
15
|
import { parseMindmap } from './parser';
|
|
16
16
|
import { layoutMindmap } from './layout';
|
|
17
17
|
import { computeNodeText } from './text-wrap';
|
|
18
|
+
import { renderInlineText } from '../utils/inline-markdown';
|
|
19
|
+
import { preprocessDescriptionLine } from '../utils/description-helpers';
|
|
18
20
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
19
21
|
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
20
22
|
import { LEGEND_HEIGHT, LEGEND_GROUP_GAP } from '../utils/legend-constants';
|
|
@@ -419,31 +421,30 @@ export function renderMindmap(
|
|
|
419
421
|
.attr('stroke-opacity', 0.3)
|
|
420
422
|
.attr('stroke-width', 1);
|
|
421
423
|
|
|
422
|
-
// Description text
|
|
424
|
+
// Description text (with inline markdown + preprocessing)
|
|
423
425
|
if (descLines.length <= 1) {
|
|
424
426
|
const descY = separatorY + 4 + descFontSize;
|
|
425
|
-
|
|
427
|
+
const processed = preprocessDescriptionLine(descLines[0]);
|
|
428
|
+
const textEl = nodeG
|
|
426
429
|
.append('text')
|
|
427
430
|
.attr('x', centerX)
|
|
428
431
|
.attr('y', descY)
|
|
429
432
|
.attr('text-anchor', 'middle')
|
|
430
433
|
.attr('font-size', descFontSize)
|
|
431
|
-
.attr('fill', palette.textMuted)
|
|
432
|
-
|
|
434
|
+
.attr('fill', palette.textMuted);
|
|
435
|
+
renderInlineText(textEl, processed, palette, descFontSize);
|
|
433
436
|
} else {
|
|
434
437
|
const descStartY = separatorY + 4 + descFontSize;
|
|
435
|
-
const descTextEl = nodeG
|
|
436
|
-
.append('text')
|
|
437
|
-
.attr('x', centerX)
|
|
438
|
-
.attr('text-anchor', 'middle')
|
|
439
|
-
.attr('font-size', descFontSize)
|
|
440
|
-
.attr('fill', palette.textMuted);
|
|
441
438
|
for (let i = 0; i < descLines.length; i++) {
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
const processed = preprocessDescriptionLine(descLines[i]);
|
|
440
|
+
const textEl = nodeG
|
|
441
|
+
.append('text')
|
|
444
442
|
.attr('x', centerX)
|
|
445
443
|
.attr('y', descStartY + i * DESC_LINE_HEIGHT)
|
|
446
|
-
.text
|
|
444
|
+
.attr('text-anchor', 'middle')
|
|
445
|
+
.attr('font-size', descFontSize)
|
|
446
|
+
.attr('fill', palette.textMuted);
|
|
447
|
+
renderInlineText(textEl, processed, palette, descFontSize);
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
450
|
}
|
package/src/mindmap/text-wrap.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// multiple lines. Used by both layout (for sizing) and renderer
|
|
7
7
|
// (for drawing). Ensures both agree on line breaks and font size.
|
|
8
8
|
|
|
9
|
+
import { preprocessDescriptionLine } from '../utils/description-helpers';
|
|
10
|
+
|
|
9
11
|
const CHAR_WIDTH_RATIO = 0.58; // avg char width / fontSize for Helvetica
|
|
10
12
|
const H_PAD = 16; // 8px padding each side
|
|
11
13
|
const MAX_LABEL_LINES = 3;
|
|
@@ -170,7 +172,7 @@ interface NodeTextLayout {
|
|
|
170
172
|
*/
|
|
171
173
|
export function computeNodeText(
|
|
172
174
|
label: string,
|
|
173
|
-
description: string | undefined,
|
|
175
|
+
description: string[] | undefined,
|
|
174
176
|
depth: number,
|
|
175
177
|
nodeWidth: number,
|
|
176
178
|
hideDescriptions: boolean
|
|
@@ -184,18 +186,26 @@ export function computeNodeText(
|
|
|
184
186
|
MAX_LABEL_LINES
|
|
185
187
|
);
|
|
186
188
|
|
|
187
|
-
|
|
189
|
+
const descLines: string[] = [];
|
|
188
190
|
let descFontSize = DESC_FONT_SIZE;
|
|
189
|
-
if (!hideDescriptions && description) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
if (!hideDescriptions && description && description.length > 0) {
|
|
192
|
+
// Wrap each description line independently so bullets and line breaks are preserved.
|
|
193
|
+
// Cap total output lines at MAX_DESC_LINES across all input lines.
|
|
194
|
+
let remaining = MAX_DESC_LINES;
|
|
195
|
+
for (const rawLine of description) {
|
|
196
|
+
if (remaining <= 0) break;
|
|
197
|
+
const line = preprocessDescriptionLine(rawLine);
|
|
198
|
+
const lineResult = wrapText(
|
|
199
|
+
line,
|
|
200
|
+
nodeWidth,
|
|
201
|
+
DESC_FONT_SIZE,
|
|
202
|
+
DESC_FONT_SIZE,
|
|
203
|
+
remaining
|
|
204
|
+
);
|
|
205
|
+
descLines.push(...lineResult.lines);
|
|
206
|
+
remaining -= lineResult.lines.length;
|
|
207
|
+
descFontSize = lineResult.fontSize;
|
|
208
|
+
}
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
return {
|
package/src/mindmap/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TagGroup } from '../utils/tag-groups.js';
|
|
|
4
4
|
export interface MindmapNode {
|
|
5
5
|
id: string;
|
|
6
6
|
label: string;
|
|
7
|
-
description?: string;
|
|
7
|
+
description?: string[];
|
|
8
8
|
metadata: Record<string, string>;
|
|
9
9
|
children: MindmapNode[];
|
|
10
10
|
parentId: string | null;
|
|
@@ -26,7 +26,7 @@ export interface ParsedMindmap {
|
|
|
26
26
|
export interface MindmapLayoutNode {
|
|
27
27
|
id: string;
|
|
28
28
|
label: string;
|
|
29
|
-
description?: string;
|
|
29
|
+
description?: string[];
|
|
30
30
|
metadata: Record<string, string>;
|
|
31
31
|
lineNumber: number;
|
|
32
32
|
color?: string;
|
package/src/org/collapse.ts
CHANGED
|
@@ -15,6 +15,22 @@ export interface CollapsedOrgResult {
|
|
|
15
15
|
hiddenCounts: Map<string, number>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface AncestorInfo {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
lineNumber: number;
|
|
22
|
+
color?: string;
|
|
23
|
+
metadata: Record<string, string>;
|
|
24
|
+
isContainer: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FocusOrgResult {
|
|
28
|
+
/** ParsedOrg with only the focused subtree as the single root */
|
|
29
|
+
parsed: ParsedOrg;
|
|
30
|
+
/** Ancestor path from original root → parent of focused node (top-down order) */
|
|
31
|
+
ancestorPath: AncestorInfo[];
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
// ============================================================
|
|
19
35
|
// Helpers
|
|
20
36
|
// ============================================================
|
|
@@ -99,3 +115,68 @@ export function collapseOrgTree(
|
|
|
99
115
|
hiddenCounts,
|
|
100
116
|
};
|
|
101
117
|
}
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Focus (subtree drill-down)
|
|
121
|
+
// ============================================================
|
|
122
|
+
|
|
123
|
+
/** Find a node by ID and collect the ancestor path leading to it. */
|
|
124
|
+
function findNodeWithPath(
|
|
125
|
+
nodes: OrgNode[],
|
|
126
|
+
targetId: string,
|
|
127
|
+
path: AncestorInfo[]
|
|
128
|
+
): { node: OrgNode; path: AncestorInfo[] } | null {
|
|
129
|
+
for (const node of nodes) {
|
|
130
|
+
if (node.id === targetId) {
|
|
131
|
+
return { node, path };
|
|
132
|
+
}
|
|
133
|
+
const result = findNodeWithPath(node.children, targetId, [
|
|
134
|
+
...path,
|
|
135
|
+
{
|
|
136
|
+
id: node.id,
|
|
137
|
+
label: node.label,
|
|
138
|
+
lineNumber: node.lineNumber,
|
|
139
|
+
color: node.color,
|
|
140
|
+
metadata: { ...node.metadata },
|
|
141
|
+
isContainer: node.isContainer,
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
if (result) return result;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract a subtree rooted at `focusNodeId`, returning the focused tree
|
|
151
|
+
* and the ancestor breadcrumb path. Returns null if the node is not found.
|
|
152
|
+
*/
|
|
153
|
+
export function focusOrgTree(
|
|
154
|
+
original: ParsedOrg,
|
|
155
|
+
focusNodeId: string
|
|
156
|
+
): FocusOrgResult | null {
|
|
157
|
+
const found = findNodeWithPath(original.roots, focusNodeId, []);
|
|
158
|
+
if (!found) return null;
|
|
159
|
+
|
|
160
|
+
// If it's already a root, return as-is with empty ancestor path
|
|
161
|
+
const isRoot = original.roots.some((r) => r.id === focusNodeId);
|
|
162
|
+
if (isRoot) {
|
|
163
|
+
return {
|
|
164
|
+
parsed: {
|
|
165
|
+
...original,
|
|
166
|
+
roots: [cloneNode(found.node)],
|
|
167
|
+
},
|
|
168
|
+
ancestorPath: [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const cloned = cloneNode(found.node);
|
|
173
|
+
cloned.parentId = null;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
parsed: {
|
|
177
|
+
...original,
|
|
178
|
+
roots: [cloned],
|
|
179
|
+
},
|
|
180
|
+
ancestorPath: found.path,
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/org/parser.ts
CHANGED
|
@@ -270,8 +270,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
270
270
|
|
|
271
271
|
if (containerMatch) {
|
|
272
272
|
// It's a container node
|
|
273
|
-
const
|
|
274
|
-
const { label, color } = extractColor(rawLabel, palette);
|
|
273
|
+
const label = containerMatch[1].trim();
|
|
275
274
|
|
|
276
275
|
containerCounter++;
|
|
277
276
|
const node: OrgNode = {
|
|
@@ -282,7 +281,6 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
282
281
|
parentId: null,
|
|
283
282
|
isContainer: true,
|
|
284
283
|
lineNumber,
|
|
285
|
-
color,
|
|
286
284
|
};
|
|
287
285
|
|
|
288
286
|
attachNode(node, indent, indentStack, result);
|
|
@@ -378,8 +376,7 @@ function parseNodeLabel(
|
|
|
378
376
|
// Check for single-line compact metadata: "Alice Park | role: Senior, location: NY"
|
|
379
377
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
380
378
|
|
|
381
|
-
const
|
|
382
|
-
const { label, color } = extractColor(rawLabel, palette);
|
|
379
|
+
const label = segments[0];
|
|
383
380
|
|
|
384
381
|
const metadata = parsePipeMetadata(
|
|
385
382
|
segments,
|
|
@@ -395,7 +392,6 @@ function parseNodeLabel(
|
|
|
395
392
|
parentId: null,
|
|
396
393
|
isContainer: false,
|
|
397
394
|
lineNumber,
|
|
398
|
-
color,
|
|
399
395
|
};
|
|
400
396
|
}
|
|
401
397
|
|