@diagrammo/dgmo 0.8.3 → 0.8.5

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 (122) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +452 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +188 -185
  9. package/dist/editor.cjs +338 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
@@ -0,0 +1,69 @@
1
+ @top Document { (newline | Comment | ContentLine)* }
2
+
3
+ ContentLine { contentPart+ newline }
4
+
5
+ contentPart {
6
+ SyncArrow | AsyncArrow |
7
+ Duration | DateLiteral | Percentage | Number |
8
+ SectionMarker |
9
+ Url |
10
+ OpenBracket | CloseBracket | OpenParen | CloseParen | OpenAngle | CloseAngle |
11
+ ColorAnnotation |
12
+ Pipe | Colon | Comma | Plus | Dash | Tilde | Star | Question |
13
+ ChartType |
14
+ TagKeyword | DirectiveKeyword | ControlKeyword | ModifierKeyword |
15
+ Identifier |
16
+ Punct
17
+ }
18
+
19
+ @skip { spaces }
20
+
21
+ @tokens {
22
+ spaces { $[ \t]+ }
23
+ newline { "\n" }
24
+
25
+ Comment { "//" ![\n]* }
26
+
27
+ SyncArrow { "->" }
28
+ AsyncArrow { "~>" }
29
+
30
+ Duration { $[0-9]+ ("." $[0-9]+)? ("min" | "bd" | "h" | "d" | "w" | "m" | "q" | "y") "?"? }
31
+ DateLiteral { $[0-9] $[0-9] $[0-9] $[0-9] "-" $[0-9] $[0-9] ("-" $[0-9] $[0-9])? }
32
+ Percentage { $[0-9]+ ("." $[0-9]+)? "%" }
33
+ Number { $[0-9]+ ("." $[0-9]+)? }
34
+
35
+ SectionMarker { "==" }
36
+ Url { "http" "s"? "://" ![ \t\n|,)\]>]+ }
37
+ ColorAnnotation { "(" $[a-z\-]+ ")" }
38
+
39
+ Pipe { "|" }
40
+ Colon { ":" }
41
+ Comma { "," }
42
+ Plus { "+" }
43
+ Dash { "-" }
44
+ Tilde { "~" }
45
+ Star { "*" }
46
+ Question { "?" }
47
+ OpenBracket { "[" }
48
+ CloseBracket { "]" }
49
+ OpenParen { "(" }
50
+ CloseParen { ")" }
51
+ OpenAngle { "<" }
52
+ CloseAngle { ">" }
53
+
54
+ Identifier { $[a-zA-Z_] ($[a-zA-Z0-9_./&?=@#!'+] | "-" $[a-zA-Z_./&?=@#!'+])* }
55
+
56
+ Punct { ![$[ \t\n] }
57
+
58
+ @precedence {
59
+ Comment, SyncArrow, AsyncArrow,
60
+ Duration, DateLiteral, Percentage, Number, SectionMarker, Url, ColorAnnotation,
61
+ Pipe, Colon, Comma, Plus, Dash, Tilde, Star, Question,
62
+ OpenBracket, CloseBracket, OpenParen, CloseParen, OpenAngle, CloseAngle,
63
+ Identifier, Punct
64
+ }
65
+ }
66
+
67
+ @external specialize {Identifier} specializeKeyword from "./tokens" {
68
+ ChartType, TagKeyword, DirectiveKeyword, ControlKeyword, ModifierKeyword
69
+ }
@@ -0,0 +1,2 @@
1
+ import { LRParser } from '@lezer/lr';
2
+ export declare const parser: LRParser;
@@ -0,0 +1,18 @@
1
+ // This file was generated by lezer-generator. You probably shouldn't edit it.
2
+ import {LRParser} from "@lezer/lr"
3
+ import {specializeKeyword} from "./tokens"
4
+ export const parser = LRParser.deserialize({
5
+ version: 14,
6
+ states: "!WQVQPOOOOQO'#DU'#DUOOQO'#DP'#DPO%]QPO'#CdOOQO'#DO'#DOQVQPOOOOQO-E6}-E6}OOQO,59O,59OOOQO-E6|-E6|",
7
+ stateData: "&Q~OvOS~OPPOQPORPOSPOTPOVSOXPOYPOZPO[PO]PO^PO_PO`POaPObPOcPOdPOePOfPOgPOhPOiPOjPOkPOlPOmPOnPOoPOpPOqPOwSO~OPPOQPORPOSPOTPOXPOYPOZPO[PO]PO^PO_PO`POaPObPOcPOdPOePOfPOgPOhPOiPOjPOkPOlPOmPOnPOoPOpPOqPO~OwVO~P#]OVXYZ[]^_`ghijklmnoabcdefpqk~",
8
+ goto: "!byPPPPPPPPzPPPPPPPPPPPPPPPPPPPPPPPPP!O!UPPPP!]TSOTQTORWTSROTRURVQORT",
9
+ nodeNames: "⚠ ChartType TagKeyword DirectiveKeyword ControlKeyword ModifierKeyword Document Comment ContentLine SyncArrow AsyncArrow Duration DateLiteral Percentage Number SectionMarker Url OpenBracket CloseBracket OpenParen CloseParen OpenAngle CloseAngle ColorAnnotation Pipe Colon Comma Plus Dash Tilde Star Question Identifier Punct",
10
+ maxTerm: 40,
11
+ skippedNodes: [0],
12
+ repeatNodeCount: 2,
13
+ tokenData: "9q~RxOX#oXY#tYZ$PZp#opq#tqt#oux#oxy$Uyz$tz{${{|%S|}%Z}!O%b!O!P#o!P!Q%q!Q![&b![!],v!]!^#o!^!_,}!_!`-U!`!a-c!a!b-j!b!c#o!c!}-q!}#O0w#O#P#o#P#Q0|#Q#R#o#R#S-q#S#T#o#T#[-q#[#]1T#]#o-q#o#p#o#p#q9T#q#r#o#r#s9[#s;'S#o;'S;=`9k<%lO#o~#tOq~~#yQv~XY#tpq#t~$UOw~~$]Qc~q~}!O$c#T#o$c~$fRyz$o}!O$c#T#o$c~$tOg~~${Od~q~~%SOn~q~~%ZOk~q~~%bOj~q~~%iPl~q~!`!a%l~%qOX~~%vPq~!P!Q%y~&OSV~OY%yZ;'S%y;'S;=`&[<%lO%y~&_P;=`<%l%y~&iY^~q~uv'X!O!P'^!Q![(z#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~'^O]~~'aP!Q!['d~'iX^~uv'X!Q!['d#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~(XP#W#X([~(aPZ~!a!b(d~(iOZ~~(nQZ~!a!b(d#]#^(t~(wP#b#c([~)PY^~uv'X!O!P'^!Q![)o#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~)tY^~uv'X!O!P'^!Q![*d#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~*iZ^~uv'X}!O+[!O!P'^!Q![,R#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~+_P!Q![+b~+eP!Q![+h~+mP[~}!O+p~+sP!Q![+v~+yP!Q![+|~,RO[~~,WY^~uv'X!O!P'^!Q![,R#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~,}Oi~q~~-UOe~q~~-ZPq~!_!`-^~-cO_~~-jOf~q~~-qOo~q~~-x_p~q~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~.|_p~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~0O]qr.wst.wvw.wwx.w{|.w!O!P.w!P!Q.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~0|Oa~~1TOb~q~~1[ap~q~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#h.w#h#i2a#i#o.w~2fap~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#h.w#h#i3k#i#o.w~3pap~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#d.w#d#e4u#e#o.w~4zbp~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w![!]6S!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#g.w#g#h7|#h#o.w~6VP!P!Q6Y~6]P!P!Q6`~6cYOX7RZp7Rqy7Rz|7R}!`7R!a#P7R#Q#p7R#q;'S7R;'S;=`7v<%lO7R~7WY`~OX7RZp7Rqy7Rz|7R}!`7R!a#P7R#Q#p7R#q;'S7R;'S;=`7v<%lO7R~7yP;=`<%l7R~8R`p~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w![!]6S!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~9[Oh~q~~9cPm~q~!`!a9f~9kOY~~9nP;=`<%l#o",
14
+ tokenizers: [0],
15
+ topRules: {"Document":[0,6]},
16
+ specialized: [{term: 32, get: (value, stack) => (specializeKeyword(value, stack) << 1), external: specializeKeyword}],
17
+ tokenPrec: 204
18
+ })
@@ -0,0 +1,5 @@
1
+ export declare const ChartType: number;
2
+ export declare const TagKeyword: number;
3
+ export declare const DirectiveKeyword: number;
4
+ export declare const ControlKeyword: number;
5
+ export declare const ModifierKeyword: number;
@@ -0,0 +1,35 @@
1
+ // This file was generated by lezer-generator. You probably shouldn't edit it.
2
+ export const
3
+ ChartType = 1,
4
+ TagKeyword = 2,
5
+ DirectiveKeyword = 3,
6
+ ControlKeyword = 4,
7
+ ModifierKeyword = 5,
8
+ Document = 6,
9
+ Comment = 7,
10
+ ContentLine = 8,
11
+ SyncArrow = 9,
12
+ AsyncArrow = 10,
13
+ Duration = 11,
14
+ DateLiteral = 12,
15
+ Percentage = 13,
16
+ Number = 14,
17
+ SectionMarker = 15,
18
+ Url = 16,
19
+ OpenBracket = 17,
20
+ CloseBracket = 18,
21
+ OpenParen = 19,
22
+ CloseParen = 20,
23
+ OpenAngle = 21,
24
+ CloseAngle = 22,
25
+ ColorAnnotation = 23,
26
+ Pipe = 24,
27
+ Colon = 25,
28
+ Comma = 26,
29
+ Plus = 27,
30
+ Dash = 28,
31
+ Tilde = 29,
32
+ Star = 30,
33
+ Question = 31,
34
+ Identifier = 32,
35
+ Punct = 33
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Standalone DGMO syntax highlighter — no CodeMirror dependency.
3
+ *
4
+ * Exports:
5
+ * - `highlightDgmo(source)` → `HighlightToken[]` (consumer-agnostic)
6
+ * - `NORD_ROLE_STYLES` — inline style objects keyed by role (for React/Astro)
7
+ * - `ROLE_TO_ANSI` — ANSI escape codes keyed by role (for CLI)
8
+ *
9
+ * Uses the raw Lezer parser directly — keyword specialization is wired into
10
+ * the grammar, so `parser.parse()` runs it automatically.
11
+ *
12
+ * @module @diagrammo/dgmo/highlight
13
+ */
14
+
15
+ import { parser } from './dgmo.grammar.js';
16
+
17
+ // ============================================================
18
+ // Types
19
+ // ============================================================
20
+
21
+ export interface HighlightToken {
22
+ text: string;
23
+ role: string;
24
+ }
25
+
26
+ // ============================================================
27
+ // NODE_TO_ROLE — keep in sync with highlight.ts
28
+ // ============================================================
29
+
30
+ const NODE_TO_ROLE: Record<string, string> = {
31
+ Comment: 'comment',
32
+ ChartType: 'chartType',
33
+ TagKeyword: 'definitionKeyword',
34
+ DirectiveKeyword: 'keyword',
35
+ ControlKeyword: 'controlKeyword',
36
+ ModifierKeyword: 'modifier',
37
+ SyncArrow: 'operator',
38
+ AsyncArrow: 'operator',
39
+ Dash: 'operator',
40
+ Tilde: 'operator',
41
+ Star: 'operator',
42
+ Question: 'operator',
43
+ Duration: 'number',
44
+ DateLiteral: 'number',
45
+ Number: 'number',
46
+ Percentage: 'number',
47
+ SectionMarker: 'heading',
48
+ Url: 'url',
49
+ ColorAnnotation: 'colorAnnotation',
50
+ OpenBracket: 'bracket',
51
+ CloseBracket: 'bracket',
52
+ OpenParen: 'bracket',
53
+ CloseParen: 'bracket',
54
+ OpenAngle: 'bracket',
55
+ CloseAngle: 'bracket',
56
+ Pipe: 'separator',
57
+ Colon: 'separator',
58
+ Plus: 'separator',
59
+ Comma: 'punctuation',
60
+ Punct: 'punctuation',
61
+ Identifier: 'default',
62
+ };
63
+
64
+ // ============================================================
65
+ // Entity detection — keep in sync with entity-highlight.ts
66
+ // ============================================================
67
+
68
+ /** Tokens that have grammar-level styling which should be overridden in labels. */
69
+ const OVERRIDE_IN_LABEL = new Set([
70
+ 'ChartType',
71
+ 'TagKeyword',
72
+ 'DirectiveKeyword',
73
+ 'ControlKeyword',
74
+ 'ModifierKeyword',
75
+ 'Number',
76
+ 'Percentage',
77
+ 'Duration',
78
+ 'DateLiteral',
79
+ ]);
80
+
81
+ /** Lines starting with these are keyword-led — not entity declarations. */
82
+ const KEYWORD_STARTS = new Set([
83
+ 'TagKeyword',
84
+ 'DirectiveKeyword',
85
+ 'ControlKeyword',
86
+ 'ModifierKeyword',
87
+ 'SectionMarker',
88
+ 'Comment',
89
+ 'Duration',
90
+ 'DateLiteral',
91
+ ]);
92
+
93
+ // ============================================================
94
+ // Core: highlightDgmo()
95
+ // ============================================================
96
+
97
+ /**
98
+ * Tokenize DGMO source into annotated highlight spans.
99
+ *
100
+ * Guarantees lossless round-trip:
101
+ * `highlightDgmo(src).map(t => t.text).join('') === src`
102
+ */
103
+ export function highlightDgmo(source: string): HighlightToken[] {
104
+ const tree = parser.parse(source);
105
+ const tokens: HighlightToken[] = [];
106
+
107
+ // Phase 1: Walk tree cursor, collect leaf tokens with gap filling
108
+ let pos = 0;
109
+ const cursor = tree.cursor();
110
+
111
+ // Descend to leaves, process them, then advance via next() or parent+next()
112
+ function descend(): void {
113
+ for (;;) {
114
+ // Try to go deeper
115
+ if (cursor.firstChild()) continue;
116
+
117
+ // At a leaf — emit it
118
+ emitLeaf();
119
+
120
+ // Try to advance to next sibling or ascend
121
+ while (!cursor.nextSibling()) {
122
+ if (!cursor.parent()) return; // back at root — done
123
+ }
124
+
125
+ // Now at next sibling — loop will try to descend into it
126
+ }
127
+ }
128
+
129
+ function emitLeaf(): void {
130
+ const from = cursor.from;
131
+ const to = cursor.to;
132
+
133
+ // Fill gap before this node
134
+ if (from > pos) {
135
+ tokens.push({ text: source.slice(pos, from), role: 'default' });
136
+ }
137
+
138
+ // Emit this leaf node
139
+ if (to > from) {
140
+ const role = NODE_TO_ROLE[cursor.name] ?? 'default';
141
+ tokens.push({ text: source.slice(from, to), role });
142
+ }
143
+
144
+ pos = to;
145
+ }
146
+
147
+ descend();
148
+
149
+ // Fill trailing gap
150
+ if (pos < source.length) {
151
+ tokens.push({ text: source.slice(pos, source.length), role: 'default' });
152
+ }
153
+
154
+ // Phase 2: Post-process — entity detection (label override only)
155
+ applyLabelOverrides(tokens);
156
+
157
+ // Phase 3: Post-process — note content detection
158
+ applyNoteContent(tokens);
159
+
160
+ return tokens;
161
+ }
162
+
163
+ // ============================================================
164
+ // Post-processing: label overrides
165
+ // ============================================================
166
+
167
+ interface LineTokenRef {
168
+ /** Index into the flat token array. */
169
+ idx: number;
170
+ /** Grammar node name (looked up from role + NODE_TO_ROLE reverse). */
171
+ nodeName: string;
172
+ }
173
+
174
+ /**
175
+ * Override keyword/number tokens in message-label positions to `default` role.
176
+ *
177
+ * A "label" is the span between the first Dash/Tilde and the last arrow on a
178
+ * content line. Tokens in OVERRIDE_IN_LABEL within that zone get their role
179
+ * set to `default` so they render as plain text.
180
+ *
181
+ * Also handles ChartType tokens on non-first content lines — they become
182
+ * `default` in labels.
183
+ */
184
+ function applyLabelOverrides(tokens: HighlightToken[]): void {
185
+ // Build reverse map: role → possible node names (for override detection)
186
+ const ROLE_TO_NODES: Record<string, string[]> = {};
187
+ for (const [node, role] of Object.entries(NODE_TO_ROLE)) {
188
+ (ROLE_TO_NODES[role] ??= []).push(node);
189
+ }
190
+
191
+ // Split tokens into lines
192
+ const lines: LineTokenRef[][] = [[]];
193
+ for (let i = 0; i < tokens.length; i++) {
194
+ const t = tokens[i];
195
+ // If the token contains newlines, it belongs to the current line
196
+ // but signals the start of a new line after it
197
+ const currentLine = lines[lines.length - 1];
198
+ const role = t.role;
199
+
200
+ // Determine which node name this token likely had
201
+ let nodeName = '';
202
+ for (const [node, r] of Object.entries(NODE_TO_ROLE)) {
203
+ if (r === role) {
204
+ nodeName = node;
205
+ break;
206
+ }
207
+ }
208
+ // For roles mapping to multiple nodes, refine
209
+ if (role === 'operator') {
210
+ const text = t.text;
211
+ if (text === '->' || text.endsWith('->')) nodeName = 'SyncArrow';
212
+ else if (text === '~>' || text.endsWith('~>')) nodeName = 'AsyncArrow';
213
+ else if (text === '-') nodeName = 'Dash';
214
+ else if (text === '~') nodeName = 'Tilde';
215
+ else if (text === '*') nodeName = 'Star';
216
+ else if (text === '?') nodeName = 'Question';
217
+ } else if (role === 'number') {
218
+ const text = t.text;
219
+ if (/^\d+[smhd]$/i.test(text)) nodeName = 'Duration';
220
+ else if (/^\d{4}-\d{2}-\d{2}/.test(text)) nodeName = 'DateLiteral';
221
+ else if (text.endsWith('%')) nodeName = 'Percentage';
222
+ else nodeName = 'Number';
223
+ } else if (role === 'bracket') {
224
+ const text = t.text;
225
+ if (text === '[') nodeName = 'OpenBracket';
226
+ else if (text === ']') nodeName = 'CloseBracket';
227
+ else if (text === '(') nodeName = 'OpenParen';
228
+ else if (text === ')') nodeName = 'CloseParen';
229
+ else if (text === '<') nodeName = 'OpenAngle';
230
+ else if (text === '>') nodeName = 'CloseAngle';
231
+ } else if (role === 'separator') {
232
+ if (t.text === '|') nodeName = 'Pipe';
233
+ else if (t.text === ':') nodeName = 'Colon';
234
+ else if (t.text === '+') nodeName = 'Plus';
235
+ } else if (role === 'punctuation') {
236
+ if (t.text === ',') nodeName = 'Comma';
237
+ else nodeName = 'Punct';
238
+ }
239
+
240
+ currentLine.push({ idx: i, nodeName });
241
+
242
+ // Check if token text ends with newline — start a new line
243
+ if (t.text.includes('\n')) {
244
+ lines.push([]);
245
+ }
246
+ }
247
+
248
+ // Track first content line (for ChartType handling)
249
+ let seenFirstContent = false;
250
+
251
+ for (const line of lines) {
252
+ // Skip empty lines and whitespace-only
253
+ const nonWs = line.filter((ref) => tokens[ref.idx].text.trim().length > 0);
254
+ if (nonWs.length === 0) continue;
255
+
256
+ const firstTok = nonWs[0];
257
+
258
+ // Skip keyword-led lines
259
+ if (KEYWORD_STARTS.has(firstTok.nodeName)) continue;
260
+
261
+ // First-line chart type — skip
262
+ if (firstTok.nodeName === 'ChartType' && !seenFirstContent) {
263
+ seenFirstContent = true;
264
+ continue;
265
+ }
266
+ seenFirstContent = true;
267
+
268
+ // Find structural boundaries within this line
269
+ let firstDashTildeIdx = -1;
270
+ let lastArrowIdx = -1;
271
+
272
+ for (let li = 0; li < nonWs.length; li++) {
273
+ const ref = nonWs[li];
274
+ if (
275
+ (ref.nodeName === 'Dash' || ref.nodeName === 'Tilde') &&
276
+ firstDashTildeIdx < 0
277
+ ) {
278
+ firstDashTildeIdx = li;
279
+ }
280
+ if (ref.nodeName === 'SyncArrow' || ref.nodeName === 'AsyncArrow') {
281
+ lastArrowIdx = li;
282
+ }
283
+ }
284
+
285
+ const hasArrow = firstDashTildeIdx >= 0 && lastArrowIdx > firstDashTildeIdx;
286
+
287
+ if (!hasArrow) continue;
288
+
289
+ // Override tokens in label zone (between first dash/tilde and last arrow)
290
+ for (let li = firstDashTildeIdx + 1; li < lastArrowIdx; li++) {
291
+ const ref = nonWs[li];
292
+ if (OVERRIDE_IN_LABEL.has(ref.nodeName)) {
293
+ tokens[ref.idx].role = 'default';
294
+ }
295
+ // ChartType in label also overridden
296
+ if (ref.nodeName === 'ChartType') {
297
+ tokens[ref.idx].role = 'default';
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ // ============================================================
304
+ // Post-processing: note content detection
305
+ // ============================================================
306
+
307
+ const NOTE_HEAD_RE = /^note(\s|$)/i;
308
+
309
+ /**
310
+ * Detect `note` keyword lines and mark indented followers as `noteContent`.
311
+ */
312
+ function applyNoteContent(tokens: HighlightToken[]): void {
313
+ // Reconstruct lines from token text
314
+ const fullText = tokens.map((t) => t.text).join('');
315
+ const lines = fullText.split('\n');
316
+
317
+ let inNote = false;
318
+ let noteIndent = 0;
319
+ let charOffset = 0;
320
+
321
+ for (const lineText of lines) {
322
+ const lineStart = charOffset;
323
+ const lineEnd = charOffset + lineText.length;
324
+ const trimmed = lineText.trimStart();
325
+ const indent = lineText.length - trimmed.length;
326
+
327
+ if (NOTE_HEAD_RE.test(trimmed)) {
328
+ inNote = true;
329
+ noteIndent = indent;
330
+ } else if (inNote) {
331
+ if (trimmed.length === 0) {
332
+ // Blank line — stays in note block
333
+ } else if (indent > noteIndent) {
334
+ // Mark all tokens within this line range as noteContent
335
+ markTokensInRange(tokens, lineStart, lineEnd, 'noteContent');
336
+ } else {
337
+ inNote = false;
338
+ }
339
+ }
340
+
341
+ charOffset = lineEnd + 1; // +1 for the \n
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Set the role of all tokens overlapping [from, to) to the given role.
347
+ */
348
+ function markTokensInRange(
349
+ tokens: HighlightToken[],
350
+ from: number,
351
+ to: number,
352
+ role: string
353
+ ): void {
354
+ let pos = 0;
355
+ for (const token of tokens) {
356
+ const tokenEnd = pos + token.text.length;
357
+ // Token overlaps range and is not just whitespace
358
+ if (tokenEnd > from && pos < to && token.text.trim().length > 0) {
359
+ token.role = role;
360
+ }
361
+ pos = tokenEnd;
362
+ }
363
+ }
364
+
365
+ // ============================================================
366
+ // NORD_ROLE_STYLES — hardcoded Nord dark palette for static contexts
367
+ // ============================================================
368
+
369
+ export const NORD_ROLE_STYLES: Record<string, Record<string, string>> = {
370
+ keyword: { color: '#81A1C1', fontWeight: 'bold' }, // nord9
371
+ controlKeyword: { color: '#B48EAD', fontWeight: 'bold' }, // nord15
372
+ definitionKeyword: { color: '#5E81AC', fontWeight: 'bold' }, // nord10
373
+ modifier: { color: '#B48EAD' }, // nord15
374
+ chartType: { color: '#D08770', fontWeight: 'bold' }, // nord12
375
+ operator: { color: '#BF616A', fontWeight: 'bold' }, // nord11
376
+ number: { color: '#B48EAD' }, // nord15
377
+ comment: { color: '#616E88', fontStyle: 'italic' },
378
+ heading: { color: '#D08770', fontWeight: 'bold' }, // nord12
379
+ bracket: { color: '#5E81AC' }, // nord10
380
+ separator: { color: '#88C0D0' }, // nord8
381
+ url: { color: '#88C0D0', textDecoration: 'underline' }, // nord8
382
+ colorAnnotation: { color: '#D08770', fontStyle: 'italic' }, // nord12
383
+ punctuation: { color: '#616E88' },
384
+ noteContent: { color: '#616E88', fontStyle: 'italic' },
385
+ default: {},
386
+ };
387
+
388
+ // ============================================================
389
+ // ROLE_TO_ANSI — ANSI escape codes for CLI output
390
+ // ============================================================
391
+
392
+ export const ROLE_TO_ANSI: Record<string, string> = {
393
+ comment: '\x1b[3;90m', // italic dim
394
+ keyword: '\x1b[1;34m', // bold blue
395
+ controlKeyword: '\x1b[1;35m', // bold magenta
396
+ definitionKeyword: '\x1b[1;34m', // bold blue
397
+ modifier: '\x1b[35m', // magenta
398
+ chartType: '\x1b[1;33m', // bold yellow
399
+ operator: '\x1b[1;31m', // bold red
400
+ number: '\x1b[35m', // magenta
401
+ heading: '\x1b[1;33m', // bold yellow
402
+ bracket: '\x1b[34m', // blue
403
+ separator: '\x1b[36m', // cyan
404
+ url: '\x1b[4;36m', // underline cyan
405
+ colorAnnotation: '\x1b[3;33m', // italic yellow
406
+ punctuation: '\x1b[90m', // dim
407
+ noteContent: '\x1b[3;90m', // italic dim
408
+ };
409
+
410
+ const ANSI_RESET = '\x1b[0m';
411
+
412
+ /**
413
+ * Render highlighted tokens to an ANSI string for terminal display.
414
+ */
415
+ export function renderAnsi(
416
+ tokens: HighlightToken[],
417
+ useColor: boolean
418
+ ): string {
419
+ if (!useColor) {
420
+ return tokens.map((t) => t.text).join('');
421
+ }
422
+
423
+ let out = '';
424
+ let inStyled = false;
425
+
426
+ for (const token of tokens) {
427
+ const ansi = ROLE_TO_ANSI[token.role];
428
+ if (ansi) {
429
+ if (inStyled) out += ANSI_RESET;
430
+ out += ansi + token.text;
431
+ inStyled = true;
432
+ } else {
433
+ if (inStyled) {
434
+ out += ANSI_RESET;
435
+ inStyled = false;
436
+ }
437
+ out += token.text;
438
+ }
439
+ }
440
+
441
+ // Final reset to prevent terminal style leakage
442
+ out += ANSI_RESET;
443
+ return out;
444
+ }
@@ -0,0 +1,36 @@
1
+ import type { NodePropSource } from '@lezer/common';
2
+ import { styleTags, tags as t } from '@lezer/highlight';
3
+
4
+ /** Maps grammar node names to semantic highlight tags. */
5
+ export const dgmoHighlighting: NodePropSource = styleTags({
6
+ Comment: t.lineComment,
7
+ ChartType: t.typeName,
8
+ TagKeyword: t.definitionKeyword,
9
+ DirectiveKeyword: t.keyword,
10
+ ControlKeyword: t.controlKeyword,
11
+ ModifierKeyword: t.modifier,
12
+ SyncArrow: t.operator,
13
+ AsyncArrow: t.operator,
14
+ Duration: t.number,
15
+ DateLiteral: t.number,
16
+ Number: t.number,
17
+ Percentage: t.number,
18
+ SectionMarker: t.heading,
19
+ OpenBracket: t.squareBracket,
20
+ CloseBracket: t.squareBracket,
21
+ OpenParen: t.paren,
22
+ CloseParen: t.paren,
23
+ OpenAngle: t.angleBracket,
24
+ CloseAngle: t.angleBracket,
25
+ Url: t.url,
26
+ ColorAnnotation: t.atom,
27
+ Pipe: t.separator,
28
+ Colon: t.separator,
29
+ Plus: t.separator,
30
+ Comma: t.punctuation,
31
+ Dash: t.operator,
32
+ Tilde: t.operator,
33
+ Star: t.operator,
34
+ Question: t.operator,
35
+ Punct: t.punctuation,
36
+ });
@@ -0,0 +1,28 @@
1
+ import { LRLanguage, LanguageSupport } from '@codemirror/language';
2
+ import type { Extension } from '@codemirror/state';
3
+ import { parser } from './dgmo.grammar.js';
4
+ import { dgmoHighlighting } from './highlight';
5
+
6
+ export { CHART_TYPES, METADATA_KEYS } from './keywords';
7
+ export { dgmoHighlighting } from './highlight';
8
+
9
+ /** The raw Lezer parser for DGMO. */
10
+ export const dgmoParser = parser.configure({
11
+ props: [dgmoHighlighting],
12
+ });
13
+
14
+ /** LRLanguage wrapper for CodeMirror. */
15
+ export const dgmoLanguage = LRLanguage.define({
16
+ name: 'dgmo',
17
+ parser: dgmoParser,
18
+ });
19
+
20
+ /** Full language support (language + extensions). */
21
+ export const dgmoLanguageSupport = new LanguageSupport(dgmoLanguage);
22
+
23
+ /**
24
+ * Drop-in replacement for the old dgmoExtension.
25
+ * Consumers should add indentationMarkers() separately if desired
26
+ * (from @replit/codemirror-indentation-markers).
27
+ */
28
+ export const dgmoExtension: Extension = dgmoLanguageSupport;