@diagrammo/dgmo 0.2.27 → 0.3.0
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/.claude/skills/dgmo-chart/SKILL.md +107 -0
- package/.claude/skills/dgmo-flowchart/SKILL.md +61 -0
- package/.claude/skills/dgmo-generate/SKILL.md +58 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +83 -0
- package/.cursorrules +117 -0
- package/.github/copilot-instructions.md +117 -0
- package/.windsurfrules +117 -0
- package/README.md +10 -3
- package/dist/cli.cjs +366 -918
- package/dist/index.cjs +581 -396
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -24
- package/dist/index.d.ts +39 -24
- package/dist/index.js +578 -395
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +125 -0
- package/docs/language-reference.md +786 -0
- package/package.json +15 -8
- package/src/c4/parser.ts +90 -74
- package/src/c4/renderer.ts +13 -12
- package/src/c4/types.ts +6 -4
- package/src/chart.ts +3 -2
- package/src/class/layout.ts +17 -12
- package/src/class/parser.ts +22 -52
- package/src/class/renderer.ts +44 -46
- package/src/class/types.ts +1 -1
- package/src/cli.ts +130 -19
- package/src/d3.ts +1 -1
- package/src/dgmo-mermaid.ts +1 -1
- package/src/dgmo-router.ts +1 -1
- package/src/echarts.ts +33 -13
- package/src/er/parser.ts +34 -43
- package/src/er/types.ts +1 -1
- package/src/graph/flowchart-parser.ts +2 -25
- package/src/graph/types.ts +1 -1
- package/src/index.ts +5 -0
- package/src/initiative-status/parser.ts +36 -7
- package/src/initiative-status/types.ts +1 -1
- package/src/kanban/parser.ts +32 -53
- package/src/kanban/renderer.ts +9 -8
- package/src/kanban/types.ts +6 -14
- package/src/org/parser.ts +47 -87
- package/src/org/resolver.ts +11 -12
- package/src/sequence/parser.ts +97 -15
- package/src/sequence/renderer.ts +62 -69
- package/src/utils/arrows.ts +75 -0
- package/src/utils/inline-markdown.ts +75 -0
- package/src/utils/parsing.ts +67 -0
- package/src/utils/tag-groups.ts +76 -0
package/src/er/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
+
import { measureIndent } from '../utils/parsing';
|
|
4
5
|
import type {
|
|
5
6
|
ParsedERDiagram,
|
|
6
7
|
ERTable,
|
|
@@ -13,16 +14,6 @@ import type {
|
|
|
13
14
|
// Helpers
|
|
14
15
|
// ============================================================
|
|
15
16
|
|
|
16
|
-
function measureIndent(line: string): number {
|
|
17
|
-
let indent = 0;
|
|
18
|
-
for (const ch of line) {
|
|
19
|
-
if (ch === ' ') indent++;
|
|
20
|
-
else if (ch === '\t') indent += 4;
|
|
21
|
-
else break;
|
|
22
|
-
}
|
|
23
|
-
return indent;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
17
|
function tableId(name: string): string {
|
|
27
18
|
return name.toLowerCase().trim();
|
|
28
19
|
}
|
|
@@ -50,42 +41,39 @@ const CONSTRAINT_MAP: Record<string, ERConstraint> = {
|
|
|
50
41
|
// Cardinality parsing
|
|
51
42
|
// ============================================================
|
|
52
43
|
|
|
53
|
-
// Cardinality keyword map
|
|
54
|
-
const CARD_WORD: Record<string, ERCardinality> = {
|
|
55
|
-
one: '1',
|
|
56
|
-
many: '*',
|
|
57
|
-
'1': '1',
|
|
58
|
-
'*': '*',
|
|
59
|
-
'?': '?',
|
|
60
|
-
zero: '?',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
44
|
/**
|
|
64
|
-
* Parse a cardinality side token (
|
|
45
|
+
* Parse a cardinality side token (symbolic only: "1", "*", "?").
|
|
65
46
|
*/
|
|
66
47
|
function parseCardSide(token: string): ERCardinality | null {
|
|
67
|
-
|
|
48
|
+
if (token === '1' || token === '*' || token === '?') return token;
|
|
49
|
+
return null;
|
|
68
50
|
}
|
|
69
51
|
|
|
70
52
|
/**
|
|
71
|
-
* Try to parse a relationship line with cardinality.
|
|
53
|
+
* Try to parse a relationship line with symbolic cardinality.
|
|
72
54
|
*
|
|
73
|
-
* Supported
|
|
55
|
+
* Supported form:
|
|
74
56
|
* tableName 1--* tableName : label
|
|
75
57
|
* tableName 1-* tableName : label
|
|
76
|
-
* tableName one-to-many tableName : label
|
|
77
|
-
* tableName one to many tableName : label
|
|
78
|
-
* tableName 1 to many tableName : label
|
|
79
58
|
* tableName ?--1 tableName : label
|
|
80
59
|
*/
|
|
81
60
|
const REL_SYMBOLIC_RE =
|
|
82
61
|
/^([a-zA-Z_]\w*)\s+([1*?])\s*-{1,2}\s*([1*?])\s+([a-zA-Z_]\w*)(?:\s*:\s*(.+))?$/;
|
|
83
62
|
|
|
63
|
+
/** Detects keyword cardinality forms to emit helpful error */
|
|
84
64
|
const REL_KEYWORD_RE =
|
|
85
|
-
/^([a-zA-Z_]\w*)\s+(one|many|zero
|
|
65
|
+
/^([a-zA-Z_]\w*)\s+(one|many|zero)[- ]to[- ](one|many|zero)\s+([a-zA-Z_]\w*)(?:\s*:\s*(.+))?$/i;
|
|
66
|
+
|
|
67
|
+
const KEYWORD_TO_SYMBOL: Record<string, string> = {
|
|
68
|
+
one: '1',
|
|
69
|
+
many: '*',
|
|
70
|
+
zero: '?',
|
|
71
|
+
};
|
|
86
72
|
|
|
87
73
|
function parseRelationship(
|
|
88
|
-
trimmed: string
|
|
74
|
+
trimmed: string,
|
|
75
|
+
lineNumber: number,
|
|
76
|
+
pushError: (line: number, message: string) => void,
|
|
89
77
|
): {
|
|
90
78
|
source: string;
|
|
91
79
|
target: string;
|
|
@@ -109,20 +97,16 @@ function parseRelationship(
|
|
|
109
97
|
}
|
|
110
98
|
}
|
|
111
99
|
|
|
112
|
-
// Keyword / natural:
|
|
100
|
+
// Keyword / natural: produce helpful error with symbolic suggestion
|
|
113
101
|
const kw = trimmed.match(REL_KEYWORD_RE);
|
|
114
102
|
if (kw) {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
to: toCard,
|
|
123
|
-
label: kw[5]?.trim(),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
103
|
+
const fromSym = KEYWORD_TO_SYMBOL[kw[2].toLowerCase()] ?? kw[2];
|
|
104
|
+
const toSym = KEYWORD_TO_SYMBOL[kw[3].toLowerCase()] ?? kw[3];
|
|
105
|
+
pushError(
|
|
106
|
+
lineNumber,
|
|
107
|
+
`Use symbolic cardinality (1--*, ?--1, *--*) instead of "${kw[2]}-to-${kw[3]}". Example: ${kw[1]} ${fromSym}--${toSym} ${kw[4]}`,
|
|
108
|
+
);
|
|
109
|
+
return null;
|
|
126
110
|
}
|
|
127
111
|
|
|
128
112
|
return null;
|
|
@@ -157,6 +141,7 @@ export function parseERDiagram(
|
|
|
157
141
|
tables: [],
|
|
158
142
|
relationships: [],
|
|
159
143
|
diagnostics: [],
|
|
144
|
+
error: null,
|
|
160
145
|
};
|
|
161
146
|
|
|
162
147
|
const fail = (line: number, message: string): ParsedERDiagram => {
|
|
@@ -166,6 +151,12 @@ export function parseERDiagram(
|
|
|
166
151
|
return result;
|
|
167
152
|
};
|
|
168
153
|
|
|
154
|
+
const pushError = (line: number, message: string): void => {
|
|
155
|
+
const diag = makeDgmoError(line, message);
|
|
156
|
+
result.diagnostics.push(diag);
|
|
157
|
+
if (!result.error) result.error = formatDgmoError(diag);
|
|
158
|
+
};
|
|
159
|
+
|
|
169
160
|
const tableMap = new Map<string, ERTable>();
|
|
170
161
|
let currentTable: ERTable | null = null;
|
|
171
162
|
let contentStarted = false;
|
|
@@ -257,7 +248,7 @@ export function parseERDiagram(
|
|
|
257
248
|
contentStarted = true;
|
|
258
249
|
|
|
259
250
|
// Try relationship
|
|
260
|
-
const rel = parseRelationship(trimmed);
|
|
251
|
+
const rel = parseRelationship(trimmed, lineNumber, pushError);
|
|
261
252
|
if (rel) {
|
|
262
253
|
getOrCreateTable(rel.source, lineNumber);
|
|
263
254
|
getOrCreateTable(rel.target, lineNumber);
|
|
@@ -347,7 +338,7 @@ export function looksLikeERDiagram(content: string): boolean {
|
|
|
347
338
|
hasTableDecl = true;
|
|
348
339
|
}
|
|
349
340
|
// Check for relationship patterns
|
|
350
|
-
if (REL_SYMBOLIC_RE.test(trimmed)
|
|
341
|
+
if (REL_SYMBOLIC_RE.test(trimmed)) {
|
|
351
342
|
hasRelationship = true;
|
|
352
343
|
}
|
|
353
344
|
}
|
package/src/er/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
+
import { measureIndent, extractColor } from '../utils/parsing';
|
|
4
5
|
import type {
|
|
5
6
|
ParsedGraph,
|
|
6
7
|
GraphNode,
|
|
@@ -14,16 +15,6 @@ import type {
|
|
|
14
15
|
// Helpers
|
|
15
16
|
// ============================================================
|
|
16
17
|
|
|
17
|
-
function measureIndent(line: string): number {
|
|
18
|
-
let indent = 0;
|
|
19
|
-
for (const ch of line) {
|
|
20
|
-
if (ch === ' ') indent++;
|
|
21
|
-
else if (ch === '\t') indent += 4;
|
|
22
|
-
else break;
|
|
23
|
-
}
|
|
24
|
-
return indent;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
18
|
function nodeId(shape: GraphShape, label: string): string {
|
|
28
19
|
return `${shape}:${label.toLowerCase().trim()}`;
|
|
29
20
|
}
|
|
@@ -35,21 +26,6 @@ interface NodeRef {
|
|
|
35
26
|
color?: string;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
|
|
39
|
-
|
|
40
|
-
function extractColor(
|
|
41
|
-
label: string,
|
|
42
|
-
palette?: PaletteColors
|
|
43
|
-
): { label: string; color?: string } {
|
|
44
|
-
const m = label.match(COLOR_SUFFIX_RE);
|
|
45
|
-
if (!m) return { label };
|
|
46
|
-
const colorName = m[1].trim();
|
|
47
|
-
return {
|
|
48
|
-
label: label.substring(0, m.index!).trim(),
|
|
49
|
-
color: resolveColor(colorName, palette),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
/**
|
|
54
30
|
* Try to parse a node reference from a text fragment.
|
|
55
31
|
* Order matters: subroutine & document before process.
|
|
@@ -239,6 +215,7 @@ export function parseFlowchart(
|
|
|
239
215
|
edges: [],
|
|
240
216
|
options: {},
|
|
241
217
|
diagnostics: [],
|
|
218
|
+
error: null,
|
|
242
219
|
};
|
|
243
220
|
|
|
244
221
|
const fail = (line: number, message: string): ParsedGraph => {
|
package/src/graph/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -126,6 +126,11 @@ export type {
|
|
|
126
126
|
GraphDirection,
|
|
127
127
|
} from './graph/types';
|
|
128
128
|
|
|
129
|
+
export type { TagGroup, TagEntry } from './utils/tag-groups';
|
|
130
|
+
|
|
131
|
+
export { parseInlineMarkdown, truncateBareUrl } from './utils/inline-markdown';
|
|
132
|
+
export type { InlineSpan } from './utils/inline-markdown';
|
|
133
|
+
|
|
129
134
|
export { parseOrg } from './org/parser';
|
|
130
135
|
export type {
|
|
131
136
|
ParsedOrg,
|
|
@@ -29,14 +29,14 @@ export function looksLikeInitiativeStatus(content: string): boolean {
|
|
|
29
29
|
let hasIndentedArrow = false;
|
|
30
30
|
for (const line of lines) {
|
|
31
31
|
const trimmed = line.trim();
|
|
32
|
-
if (!trimmed || trimmed.startsWith('
|
|
32
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
33
33
|
if (trimmed.match(/^chart\s*:/i)) continue;
|
|
34
34
|
if (trimmed.match(/^title\s*:/i)) continue;
|
|
35
35
|
if (trimmed.includes('->')) hasArrow = true;
|
|
36
36
|
if (/\|\s*(done|wip|todo|na)\s*$/i.test(trimmed)) hasStatus = true;
|
|
37
37
|
// Indented arrow is a strong signal — only initiative-status uses this
|
|
38
38
|
const isIndented = line.length > 0 && line !== trimmed && /^\s/.test(line);
|
|
39
|
-
if (isIndented && trimmed.startsWith('->')) hasIndentedArrow = true;
|
|
39
|
+
if (isIndented && (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed))) hasIndentedArrow = true;
|
|
40
40
|
if (hasArrow && hasStatus) return true;
|
|
41
41
|
}
|
|
42
42
|
return hasIndentedArrow;
|
|
@@ -68,7 +68,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
68
68
|
groups: [],
|
|
69
69
|
options: {},
|
|
70
70
|
diagnostics: [],
|
|
71
|
-
error:
|
|
71
|
+
error: null,
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const lines = content.split('\n');
|
|
@@ -82,7 +82,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
82
82
|
const trimmed = raw.trim();
|
|
83
83
|
|
|
84
84
|
// Skip blanks and comments
|
|
85
|
-
if (!trimmed || trimmed.startsWith('
|
|
85
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
86
86
|
|
|
87
87
|
// chart: header
|
|
88
88
|
const chartMatch = trimmed.match(/^chart\s*:\s*(.+)/i);
|
|
@@ -123,11 +123,11 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
|
|
|
123
123
|
currentGroup = null;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// Edge: contains `->`
|
|
126
|
+
// Edge: contains `->` or labeled form `-label->`
|
|
127
127
|
if (trimmed.includes('->')) {
|
|
128
128
|
let edgeText = trimmed;
|
|
129
|
-
// Indented `-> Target`
|
|
130
|
-
if (trimmed.startsWith('->')) {
|
|
129
|
+
// Indented `-> Target` or `-label-> Target` shorthand
|
|
130
|
+
if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
|
|
131
131
|
if (!lastNodeLabel) {
|
|
132
132
|
result.diagnostics.push(
|
|
133
133
|
makeDgmoError(lineNum, 'Indented edge has no preceding node to use as source', 'warning')
|
|
@@ -222,6 +222,35 @@ function parseEdgeLine(
|
|
|
222
222
|
// or: <source> -> <target> | <status>
|
|
223
223
|
// or: <source> -> <target>: <label>
|
|
224
224
|
// or: <source> -> <target>
|
|
225
|
+
// or: <source> -<label>-> <target> [| <status>]
|
|
226
|
+
|
|
227
|
+
// Check for labeled arrow form: SOURCE -LABEL-> TARGET [| status]
|
|
228
|
+
const labeledMatch = trimmed.match(/^(\S+)\s+-(.+)->\s+(.+)$/);
|
|
229
|
+
if (labeledMatch) {
|
|
230
|
+
const source = labeledMatch[1];
|
|
231
|
+
const label = labeledMatch[2].trim();
|
|
232
|
+
let targetRest = labeledMatch[3].trim();
|
|
233
|
+
|
|
234
|
+
if (label) {
|
|
235
|
+
// Extract status from end (after last |)
|
|
236
|
+
let status: InitiativeStatus = 'na';
|
|
237
|
+
const lastPipe = targetRest.lastIndexOf('|');
|
|
238
|
+
if (lastPipe >= 0) {
|
|
239
|
+
const statusRaw = targetRest.slice(lastPipe + 1).trim();
|
|
240
|
+
status = parseStatus(statusRaw, lineNum, diagnostics);
|
|
241
|
+
targetRest = targetRest.slice(0, lastPipe).trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const target = targetRest.trim();
|
|
245
|
+
if (!target) {
|
|
246
|
+
diagnostics.push(makeDgmoError(lineNum, 'Edge is missing target'));
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { source, target, label, status, lineNumber: lineNum };
|
|
251
|
+
}
|
|
252
|
+
// Empty label — fall through to plain arrow parsing
|
|
253
|
+
}
|
|
225
254
|
|
|
226
255
|
const arrowIdx = trimmed.indexOf('->');
|
|
227
256
|
if (arrowIdx < 0) return null;
|
package/src/kanban/parser.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { resolveColor } from '../colors';
|
|
2
1
|
import type { PaletteColors } from '../palettes';
|
|
3
2
|
import type { DgmoError } from '../diagnostics';
|
|
4
3
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
4
|
+
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
5
|
+
import {
|
|
6
|
+
measureIndent,
|
|
7
|
+
extractColor,
|
|
8
|
+
CHART_TYPE_RE,
|
|
9
|
+
TITLE_RE,
|
|
10
|
+
OPTION_RE,
|
|
11
|
+
} from '../utils/parsing';
|
|
5
12
|
import type {
|
|
6
13
|
ParsedKanban,
|
|
7
14
|
KanbanColumn,
|
|
@@ -14,40 +21,7 @@ import type {
|
|
|
14
21
|
// Regex patterns
|
|
15
22
|
// ============================================================
|
|
16
23
|
|
|
17
|
-
const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
|
|
18
|
-
const TITLE_RE = /^title\s*:\s*(.+)/i;
|
|
19
|
-
const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
20
|
-
const GROUP_HEADING_RE =
|
|
21
|
-
/^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
|
|
22
24
|
const COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
|
|
23
|
-
const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
|
|
24
|
-
|
|
25
|
-
// ============================================================
|
|
26
|
-
// Helpers
|
|
27
|
-
// ============================================================
|
|
28
|
-
|
|
29
|
-
function measureIndent(line: string): number {
|
|
30
|
-
let indent = 0;
|
|
31
|
-
for (const ch of line) {
|
|
32
|
-
if (ch === ' ') indent++;
|
|
33
|
-
else if (ch === '\t') indent += 4;
|
|
34
|
-
else break;
|
|
35
|
-
}
|
|
36
|
-
return indent;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function extractColor(
|
|
40
|
-
label: string,
|
|
41
|
-
palette?: PaletteColors
|
|
42
|
-
): { label: string; color?: string } {
|
|
43
|
-
const m = label.match(COLOR_SUFFIX_RE);
|
|
44
|
-
if (!m) return { label };
|
|
45
|
-
const colorName = m[1].trim();
|
|
46
|
-
return {
|
|
47
|
-
label: label.substring(0, m.index!).trim(),
|
|
48
|
-
color: resolveColor(colorName, palette),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
25
|
|
|
52
26
|
// ============================================================
|
|
53
27
|
// Parser
|
|
@@ -63,6 +37,7 @@ export function parseKanban(
|
|
|
63
37
|
tagGroups: [],
|
|
64
38
|
options: {},
|
|
65
39
|
diagnostics: [],
|
|
40
|
+
error: null,
|
|
66
41
|
};
|
|
67
42
|
|
|
68
43
|
const fail = (line: number, message: string): ParsedKanban => {
|
|
@@ -146,10 +121,32 @@ export function parseKanban(
|
|
|
146
121
|
}
|
|
147
122
|
}
|
|
148
123
|
|
|
124
|
+
// Tag group heading — `tag: Name` (new) or `## Name` (deprecated)
|
|
125
|
+
// Must be checked BEFORE OPTION_RE to prevent `tag: Rank` being swallowed as option
|
|
126
|
+
if (!contentStarted) {
|
|
127
|
+
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
128
|
+
if (tagBlockMatch) {
|
|
129
|
+
if (tagBlockMatch.deprecated) {
|
|
130
|
+
warn(lineNumber, `'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
131
|
+
}
|
|
132
|
+
currentTagGroup = {
|
|
133
|
+
name: tagBlockMatch.name,
|
|
134
|
+
alias: tagBlockMatch.alias,
|
|
135
|
+
entries: [],
|
|
136
|
+
lineNumber,
|
|
137
|
+
};
|
|
138
|
+
if (tagBlockMatch.alias) {
|
|
139
|
+
aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
|
|
140
|
+
}
|
|
141
|
+
result.tagGroups.push(currentTagGroup);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
149
146
|
// Generic header options (key: value before content/tag groups)
|
|
150
147
|
if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
|
|
151
148
|
const optMatch = trimmed.match(OPTION_RE);
|
|
152
|
-
if (optMatch && !
|
|
149
|
+
if (optMatch && !COLUMN_RE.test(trimmed)) {
|
|
153
150
|
const key = optMatch[1].trim().toLowerCase();
|
|
154
151
|
if (key !== 'chart' && key !== 'title') {
|
|
155
152
|
result.options[key] = optMatch[2].trim();
|
|
@@ -158,24 +155,6 @@ export function parseKanban(
|
|
|
158
155
|
}
|
|
159
156
|
}
|
|
160
157
|
|
|
161
|
-
// ## Tag group heading
|
|
162
|
-
const groupMatch = trimmed.match(GROUP_HEADING_RE);
|
|
163
|
-
if (groupMatch && !contentStarted) {
|
|
164
|
-
const groupName = groupMatch[1].trim();
|
|
165
|
-
const alias = groupMatch[2] || undefined;
|
|
166
|
-
currentTagGroup = {
|
|
167
|
-
name: groupName,
|
|
168
|
-
alias,
|
|
169
|
-
entries: [],
|
|
170
|
-
lineNumber,
|
|
171
|
-
};
|
|
172
|
-
if (alias) {
|
|
173
|
-
aliasMap.set(alias.toLowerCase(), groupName.toLowerCase());
|
|
174
|
-
}
|
|
175
|
-
result.tagGroups.push(currentTagGroup);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
158
|
// Tag group entries (indented Value(color) [default] under ## heading)
|
|
180
159
|
if (currentTagGroup && !contentStarted) {
|
|
181
160
|
const indent = measureIndent(line);
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import { FONT_FAMILY } from '../fonts';
|
|
7
7
|
import type { PaletteColors } from '../palettes';
|
|
8
|
+
import { renderInlineText } from '../utils/inline-markdown';
|
|
8
9
|
import type { ParsedKanban, KanbanColumn, KanbanCard, KanbanTagGroup } from './types';
|
|
9
10
|
import { parseKanban } from './parser';
|
|
10
11
|
import { isArchiveColumn } from './mutations';
|
|
@@ -498,14 +499,14 @@ export function renderKanban(
|
|
|
498
499
|
.attr('stroke', cardStroke)
|
|
499
500
|
.attr('stroke-width', CARD_STROKE_WIDTH);
|
|
500
501
|
|
|
501
|
-
// Card title
|
|
502
|
-
cg.append('text')
|
|
502
|
+
// Card title (inline markdown)
|
|
503
|
+
const titleEl = cg.append('text')
|
|
503
504
|
.attr('x', cx + CARD_PADDING_X)
|
|
504
505
|
.attr('y', cy + CARD_PADDING_Y + CARD_TITLE_FONT_SIZE)
|
|
505
506
|
.attr('font-size', CARD_TITLE_FONT_SIZE)
|
|
506
507
|
.attr('font-weight', '500')
|
|
507
|
-
.attr('fill', palette.text)
|
|
508
|
-
|
|
508
|
+
.attr('fill', palette.text);
|
|
509
|
+
renderInlineText(titleEl, card.title, palette, CARD_TITLE_FONT_SIZE);
|
|
509
510
|
|
|
510
511
|
// Separator + metadata
|
|
511
512
|
if (hasMeta) {
|
|
@@ -542,14 +543,14 @@ export function renderKanban(
|
|
|
542
543
|
metaY += CARD_META_LINE_HEIGHT;
|
|
543
544
|
}
|
|
544
545
|
|
|
545
|
-
// Detail lines
|
|
546
|
+
// Detail lines (inline markdown)
|
|
546
547
|
for (const detail of card.details) {
|
|
547
|
-
cg.append('text')
|
|
548
|
+
const detailEl = cg.append('text')
|
|
548
549
|
.attr('x', cx + CARD_PADDING_X)
|
|
549
550
|
.attr('y', metaY)
|
|
550
551
|
.attr('font-size', CARD_META_FONT_SIZE)
|
|
551
|
-
.attr('fill', palette.textMuted)
|
|
552
|
-
|
|
552
|
+
.attr('fill', palette.textMuted);
|
|
553
|
+
renderInlineText(detailEl, detail, palette, CARD_META_FONT_SIZE);
|
|
553
554
|
|
|
554
555
|
metaY += CARD_META_LINE_HEIGHT;
|
|
555
556
|
}
|
package/src/kanban/types.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import type { DgmoError } from '../diagnostics';
|
|
2
|
+
import type { TagGroup, TagEntry } from '../utils/tag-groups';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface KanbanTagGroup {
|
|
10
|
-
name: string;
|
|
11
|
-
alias?: string;
|
|
12
|
-
entries: KanbanTagEntry[];
|
|
13
|
-
defaultValue?: string;
|
|
14
|
-
lineNumber: number;
|
|
15
|
-
}
|
|
4
|
+
/** @deprecated Use `TagEntry` from `utils/tag-groups` */
|
|
5
|
+
export type KanbanTagEntry = TagEntry;
|
|
6
|
+
/** @deprecated Use `TagGroup` from `utils/tag-groups` */
|
|
7
|
+
export type KanbanTagGroup = TagGroup;
|
|
16
8
|
|
|
17
9
|
export interface KanbanCard {
|
|
18
10
|
id: string;
|
|
@@ -41,5 +33,5 @@ export interface ParsedKanban {
|
|
|
41
33
|
tagGroups: KanbanTagGroup[];
|
|
42
34
|
options: Record<string, string>;
|
|
43
35
|
diagnostics: DgmoError[];
|
|
44
|
-
error
|
|
36
|
+
error: string | null;
|
|
45
37
|
}
|