@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Inline Markdown — shared parsing + SVG rendering for text fields
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as d3Selection from 'd3-selection';
|
|
6
|
+
import type { PaletteColors } from '../palettes';
|
|
7
|
+
|
|
8
|
+
export interface InlineSpan {
|
|
9
|
+
text: string;
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
italic?: boolean;
|
|
12
|
+
code?: boolean;
|
|
13
|
+
href?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseInlineMarkdown(text: string): InlineSpan[] {
|
|
17
|
+
const spans: InlineSpan[] = [];
|
|
18
|
+
const regex =
|
|
19
|
+
/\*\*(.+?)\*\*|__(.+?)__|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)|(https?:\/\/[^\s)>\]]+|www\.[^\s)>\]]+)|([^*_`[]+?(?=https?:\/\/|www\.|$)|[^*_`[]+)/g;
|
|
20
|
+
let match;
|
|
21
|
+
while ((match = regex.exec(text)) !== null) {
|
|
22
|
+
if (match[1]) spans.push({ text: match[1], bold: true }); // **bold**
|
|
23
|
+
else if (match[2]) spans.push({ text: match[2], bold: true }); // __bold__
|
|
24
|
+
else if (match[3]) spans.push({ text: match[3], italic: true }); // *italic*
|
|
25
|
+
else if (match[4]) spans.push({ text: match[4], italic: true }); // _italic_
|
|
26
|
+
else if (match[5]) spans.push({ text: match[5], code: true }); // `code`
|
|
27
|
+
else if (match[6]) spans.push({ text: match[6], href: match[7] }); // [text](url)
|
|
28
|
+
else if (match[8]) { // bare URL
|
|
29
|
+
const url = match[8];
|
|
30
|
+
const href = url.startsWith('www.') ? `https://${url}` : url;
|
|
31
|
+
spans.push({ text: url, href });
|
|
32
|
+
} else if (match[9]) spans.push({ text: match[9] }); // plain text
|
|
33
|
+
}
|
|
34
|
+
return spans;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BARE_URL_MAX_DISPLAY = 35;
|
|
38
|
+
|
|
39
|
+
export function truncateBareUrl(url: string): string {
|
|
40
|
+
const stripped = url.replace(/^https?:\/\//, '').replace(/^www\./, '');
|
|
41
|
+
if (stripped.length <= BARE_URL_MAX_DISPLAY) return stripped;
|
|
42
|
+
return stripped.slice(0, BARE_URL_MAX_DISPLAY - 1) + '\u2026';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderInlineText(
|
|
46
|
+
textEl: d3Selection.Selection<SVGTextElement, unknown, null, undefined>,
|
|
47
|
+
text: string,
|
|
48
|
+
palette: PaletteColors,
|
|
49
|
+
fontSize?: number
|
|
50
|
+
): void {
|
|
51
|
+
const spans = parseInlineMarkdown(text);
|
|
52
|
+
for (const span of spans) {
|
|
53
|
+
if (span.href) {
|
|
54
|
+
// Bare URLs (text === href or href with https:// prepended) get truncated display;
|
|
55
|
+
// markdown links [text](url) keep their user-chosen text as-is.
|
|
56
|
+
const isBareUrl =
|
|
57
|
+
span.text === span.href ||
|
|
58
|
+
`https://${span.text}` === span.href;
|
|
59
|
+
const display = isBareUrl ? truncateBareUrl(span.text) : span.text;
|
|
60
|
+
const a = textEl.append('a').attr('href', span.href);
|
|
61
|
+
a.append('tspan')
|
|
62
|
+
.text(display)
|
|
63
|
+
.attr('fill', palette.primary)
|
|
64
|
+
.style('text-decoration', 'underline');
|
|
65
|
+
} else {
|
|
66
|
+
const tspan = textEl.append('tspan').text(span.text);
|
|
67
|
+
if (span.bold) tspan.attr('font-weight', 'bold');
|
|
68
|
+
if (span.italic) tspan.attr('font-style', 'italic');
|
|
69
|
+
if (span.code) {
|
|
70
|
+
tspan.attr('font-family', 'monospace');
|
|
71
|
+
if (fontSize) tspan.attr('font-size', fontSize - 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parser utilities — extracted from individual parsers to eliminate
|
|
3
|
+
* duplication of measureIndent, extractColor, header regexes, and
|
|
4
|
+
* pipe-metadata parsing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolveColor } from '../colors';
|
|
8
|
+
import type { PaletteColors } from '../palettes';
|
|
9
|
+
|
|
10
|
+
/** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */
|
|
11
|
+
export function measureIndent(line: string): number {
|
|
12
|
+
let indent = 0;
|
|
13
|
+
for (const ch of line) {
|
|
14
|
+
if (ch === ' ') indent++;
|
|
15
|
+
else if (ch === '\t') indent += 4;
|
|
16
|
+
else break;
|
|
17
|
+
}
|
|
18
|
+
return indent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Matches a trailing `(colorName)` suffix on a label. */
|
|
22
|
+
export const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
|
|
23
|
+
|
|
24
|
+
/** Extract an optional trailing color suffix from a label, resolving via palette. */
|
|
25
|
+
export function extractColor(
|
|
26
|
+
label: string,
|
|
27
|
+
palette?: PaletteColors,
|
|
28
|
+
): { label: string; color?: string } {
|
|
29
|
+
const m = label.match(COLOR_SUFFIX_RE);
|
|
30
|
+
if (!m) return { label };
|
|
31
|
+
const colorName = m[1].trim();
|
|
32
|
+
return {
|
|
33
|
+
label: label.substring(0, m.index!).trim(),
|
|
34
|
+
color: resolveColor(colorName, palette),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Matches `chart: <type>` header lines. */
|
|
39
|
+
export const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
|
|
40
|
+
|
|
41
|
+
/** Matches `title: <text>` header lines. */
|
|
42
|
+
export const TITLE_RE = /^title\s*:\s*(.+)/i;
|
|
43
|
+
|
|
44
|
+
/** Matches `option: value` header lines. */
|
|
45
|
+
export const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
46
|
+
|
|
47
|
+
/** Parse pipe-delimited metadata from segments after the first (name) segment. */
|
|
48
|
+
export function parsePipeMetadata(
|
|
49
|
+
segments: string[],
|
|
50
|
+
aliasMap: Map<string, string> = new Map(),
|
|
51
|
+
): Record<string, string> {
|
|
52
|
+
const metadata: Record<string, string> = {};
|
|
53
|
+
for (let j = 1; j < segments.length; j++) {
|
|
54
|
+
for (const part of segments[j].split(',')) {
|
|
55
|
+
const trimmedPart = part.trim();
|
|
56
|
+
if (!trimmedPart) continue;
|
|
57
|
+
const colonIdx = trimmedPart.indexOf(':');
|
|
58
|
+
if (colonIdx > 0) {
|
|
59
|
+
const rawKey = trimmedPart.substring(0, colonIdx).trim().toLowerCase();
|
|
60
|
+
const key = aliasMap.get(rawKey) ?? rawKey;
|
|
61
|
+
const value = trimmedPart.substring(colonIdx + 1).trim();
|
|
62
|
+
metadata[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return metadata;
|
|
67
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Shared tag-group types, regexes, and matchers
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/** A single entry inside a tag group: `Value(color)` */
|
|
6
|
+
export interface TagEntry {
|
|
7
|
+
value: string;
|
|
8
|
+
color: string;
|
|
9
|
+
lineNumber: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A tag group block: heading + entries */
|
|
13
|
+
export interface TagGroup {
|
|
14
|
+
name: string;
|
|
15
|
+
alias?: string;
|
|
16
|
+
entries: TagEntry[];
|
|
17
|
+
/** Value of the entry marked `default` (nodes without metadata get this) */
|
|
18
|
+
defaultValue?: string;
|
|
19
|
+
lineNumber: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Result of matching a tag block heading */
|
|
23
|
+
export interface TagBlockMatch {
|
|
24
|
+
name: string;
|
|
25
|
+
alias: string | undefined;
|
|
26
|
+
colorHint: string | undefined;
|
|
27
|
+
/** true when the heading used `## …` (deprecated) */
|
|
28
|
+
deprecated: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Regexes ─────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** New canonical syntax: `tag: GroupName [alias X] [(color)]` (case-insensitive) */
|
|
34
|
+
export const TAG_BLOCK_RE =
|
|
35
|
+
/^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
|
|
36
|
+
|
|
37
|
+
/** Legacy syntax: `## GroupName [alias X] [(color)]` */
|
|
38
|
+
export const GROUP_HEADING_RE =
|
|
39
|
+
/^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
|
|
40
|
+
|
|
41
|
+
// ── Matchers ────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** Returns true if `trimmed` is a tag block heading in either syntax. */
|
|
44
|
+
export function isTagBlockHeading(trimmed: string): boolean {
|
|
45
|
+
return TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a tag block heading line into structured data.
|
|
50
|
+
* Returns `null` if the line is not a tag block heading.
|
|
51
|
+
*/
|
|
52
|
+
export function matchTagBlockHeading(trimmed: string): TagBlockMatch | null {
|
|
53
|
+
// Try new syntax first
|
|
54
|
+
const tagMatch = trimmed.match(TAG_BLOCK_RE);
|
|
55
|
+
if (tagMatch) {
|
|
56
|
+
return {
|
|
57
|
+
name: tagMatch[1].trim(),
|
|
58
|
+
alias: tagMatch[2] || undefined,
|
|
59
|
+
colorHint: tagMatch[3] || undefined,
|
|
60
|
+
deprecated: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fall back to legacy syntax
|
|
65
|
+
const groupMatch = trimmed.match(GROUP_HEADING_RE);
|
|
66
|
+
if (groupMatch) {
|
|
67
|
+
return {
|
|
68
|
+
name: groupMatch[1].trim(),
|
|
69
|
+
alias: groupMatch[2] || undefined,
|
|
70
|
+
colorHint: groupMatch[3] || undefined,
|
|
71
|
+
deprecated: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|