@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.
Files changed (49) hide show
  1. package/.claude/skills/dgmo-chart/SKILL.md +107 -0
  2. package/.claude/skills/dgmo-flowchart/SKILL.md +61 -0
  3. package/.claude/skills/dgmo-generate/SKILL.md +58 -0
  4. package/.claude/skills/dgmo-sequence/SKILL.md +83 -0
  5. package/.cursorrules +117 -0
  6. package/.github/copilot-instructions.md +117 -0
  7. package/.windsurfrules +117 -0
  8. package/README.md +10 -3
  9. package/dist/cli.cjs +366 -918
  10. package/dist/index.cjs +581 -396
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +39 -24
  13. package/dist/index.d.ts +39 -24
  14. package/dist/index.js +578 -395
  15. package/dist/index.js.map +1 -1
  16. package/docs/ai-integration.md +125 -0
  17. package/docs/language-reference.md +786 -0
  18. package/package.json +15 -8
  19. package/src/c4/parser.ts +90 -74
  20. package/src/c4/renderer.ts +13 -12
  21. package/src/c4/types.ts +6 -4
  22. package/src/chart.ts +3 -2
  23. package/src/class/layout.ts +17 -12
  24. package/src/class/parser.ts +22 -52
  25. package/src/class/renderer.ts +44 -46
  26. package/src/class/types.ts +1 -1
  27. package/src/cli.ts +130 -19
  28. package/src/d3.ts +1 -1
  29. package/src/dgmo-mermaid.ts +1 -1
  30. package/src/dgmo-router.ts +1 -1
  31. package/src/echarts.ts +33 -13
  32. package/src/er/parser.ts +34 -43
  33. package/src/er/types.ts +1 -1
  34. package/src/graph/flowchart-parser.ts +2 -25
  35. package/src/graph/types.ts +1 -1
  36. package/src/index.ts +5 -0
  37. package/src/initiative-status/parser.ts +36 -7
  38. package/src/initiative-status/types.ts +1 -1
  39. package/src/kanban/parser.ts +32 -53
  40. package/src/kanban/renderer.ts +9 -8
  41. package/src/kanban/types.ts +6 -14
  42. package/src/org/parser.ts +47 -87
  43. package/src/org/resolver.ts +11 -12
  44. package/src/sequence/parser.ts +97 -15
  45. package/src/sequence/renderer.ts +62 -69
  46. package/src/utils/arrows.ts +75 -0
  47. package/src/utils/inline-markdown.ts +75 -0
  48. package/src/utils/parsing.ts +67 -0
  49. 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
+ }