@itsthecsguy/linework 0.0.0-stage → 0.1.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +11 -0
  3. package/README.md +164 -2
  4. package/assets/drawio/LICENSE +201 -0
  5. package/assets/drawio/VERSION +1 -0
  6. package/assets/drawio/favicon.ico +0 -0
  7. package/assets/drawio/images/1x1.png +0 -0
  8. package/assets/drawio/images/2x2.png +0 -0
  9. package/assets/drawio/images/3x3.png +0 -0
  10. package/assets/drawio/images/ajax-loader.gif +0 -0
  11. package/assets/drawio/images/android-chrome-512x512.png +0 -0
  12. package/assets/drawio/images/apple-touch-icon.png +0 -0
  13. package/assets/drawio/images/aui-wait.gif +0 -0
  14. package/assets/drawio/images/broken.png +0 -0
  15. package/assets/drawio/images/browserconfig.xml +9 -0
  16. package/assets/drawio/images/close.png +0 -0
  17. package/assets/drawio/images/cloud-connector-atlas.png +0 -0
  18. package/assets/drawio/images/collapsed.gif +0 -0
  19. package/assets/drawio/images/delete.png +0 -0
  20. package/assets/drawio/images/document-google-drive-icon-80.png +0 -0
  21. package/assets/drawio/images/draw-search.png +0 -0
  22. package/assets/drawio/images/drawio.svg +19 -0
  23. package/assets/drawio/images/drawlogo-color.svg +50 -0
  24. package/assets/drawio/images/drawlogo-gray.svg +18 -0
  25. package/assets/drawio/images/drawlogo-text-bottom.svg +75 -0
  26. package/assets/drawio/images/drawlogo.svg +18 -0
  27. package/assets/drawio/images/drawlogo128.png +0 -0
  28. package/assets/drawio/images/drawlogo144.png +0 -0
  29. package/assets/drawio/images/drawlogo256.png +0 -0
  30. package/assets/drawio/images/drawlogo48-gray.png +0 -0
  31. package/assets/drawio/images/drawlogo48.png +0 -0
  32. package/assets/drawio/images/drawlogo80.png +0 -0
  33. package/assets/drawio/images/droptarget.png +0 -0
  34. package/assets/drawio/images/edit.gif +0 -0
  35. package/assets/drawio/images/edit16.png +0 -0
  36. package/assets/drawio/images/edit32.png +0 -0
  37. package/assets/drawio/images/edit80.png +0 -0
  38. package/assets/drawio/images/embed-icon.png +0 -0
  39. package/assets/drawio/images/expanded.gif +0 -0
  40. package/assets/drawio/images/facebook.png +0 -0
  41. package/assets/drawio/images/favicon-16x16.png +0 -0
  42. package/assets/drawio/images/favicon-32x32.png +0 -0
  43. package/assets/drawio/images/file.png +0 -0
  44. package/assets/drawio/images/folder.png +0 -0
  45. package/assets/drawio/images/gadget-screenshot.png +0 -0
  46. package/assets/drawio/images/gadget-thumb.png +0 -0
  47. package/assets/drawio/images/github-logo-white.svg +6 -0
  48. package/assets/drawio/images/github-logo.svg +17 -0
  49. package/assets/drawio/images/gitlab-logo-white.svg +1 -0
  50. package/assets/drawio/images/gitlab-logo.svg +1 -0
  51. package/assets/drawio/images/glyphicons_blogger.png +0 -0
  52. package/assets/drawio/images/glyphicons_facebook.png +0 -0
  53. package/assets/drawio/images/glyphicons_github.png +0 -0
  54. package/assets/drawio/images/glyphicons_google.png +0 -0
  55. package/assets/drawio/images/glyphicons_star.png +0 -0
  56. package/assets/drawio/images/glyphicons_twitter.png +0 -0
  57. package/assets/drawio/images/glyphicons_youtube.png +0 -0
  58. package/assets/drawio/images/google-drive-connector-atlas.png +0 -0
  59. package/assets/drawio/images/google-drive-connector-atlas.svg +121 -0
  60. package/assets/drawio/images/google-drive-logo-white.svg +9 -0
  61. package/assets/drawio/images/google-drive-logo.svg +17 -0
  62. package/assets/drawio/images/google-share.png +0 -0
  63. package/assets/drawio/images/icon-192-maskable.png +0 -0
  64. package/assets/drawio/images/icon-192.png +0 -0
  65. package/assets/drawio/images/icon-512-maskable.png +0 -0
  66. package/assets/drawio/images/icon-512.png +0 -0
  67. package/assets/drawio/images/icon-search.svg +21 -0
  68. package/assets/drawio/images/img-hi-res.png +0 -0
  69. package/assets/drawio/images/img-lo-res.png +0 -0
  70. package/assets/drawio/images/list.svg +16 -0
  71. package/assets/drawio/images/locked.png +0 -0
  72. package/assets/drawio/images/log.png +0 -0
  73. package/assets/drawio/images/logo-confluence.png +0 -0
  74. package/assets/drawio/images/logo-flat-small.png +0 -0
  75. package/assets/drawio/images/logo-flat.png +0 -0
  76. package/assets/drawio/images/logo-jira.png +0 -0
  77. package/assets/drawio/images/logo-small.gif +0 -0
  78. package/assets/drawio/images/logo-white.png +0 -0
  79. package/assets/drawio/images/logo.png +0 -0
  80. package/assets/drawio/images/manifest.json +25 -0
  81. package/assets/drawio/images/move.png +0 -0
  82. package/assets/drawio/images/notion-logo-white.svg +1 -0
  83. package/assets/drawio/images/notion-logo.svg +1 -0
  84. package/assets/drawio/images/onedrive-connector-atlas.png +0 -0
  85. package/assets/drawio/images/onedrive-connector-atlas.svg +248 -0
  86. package/assets/drawio/images/onedrive-logo-white.svg +29 -0
  87. package/assets/drawio/images/onedrive-logo.svg +29 -0
  88. package/assets/drawio/images/osa_database.png +0 -0
  89. package/assets/drawio/images/osa_drive-harddisk.png +0 -0
  90. package/assets/drawio/images/plant-uml-atlas.png +0 -0
  91. package/assets/drawio/images/samepage-icon-color.svg +37 -0
  92. package/assets/drawio/images/save.png +0 -0
  93. package/assets/drawio/images/sidebar-android.png +0 -0
  94. package/assets/drawio/images/sidebar-archimate.png +0 -0
  95. package/assets/drawio/images/sidebar-arrows.png +0 -0
  96. package/assets/drawio/images/sidebar-arrows2.png +0 -0
  97. package/assets/drawio/images/sidebar-basic.png +0 -0
  98. package/assets/drawio/images/sidebar-c4.png +0 -0
  99. package/assets/drawio/images/sidebar-cabinets.png +0 -0
  100. package/assets/drawio/images/sidebar-cumulus.png +0 -0
  101. package/assets/drawio/images/sidebar-dfd.png +0 -0
  102. package/assets/drawio/images/sidebar-dynamics365.png +0 -0
  103. package/assets/drawio/images/sidebar-eip.png +0 -0
  104. package/assets/drawio/images/sidebar-er.png +0 -0
  105. package/assets/drawio/images/sidebar-floorplans.png +0 -0
  106. package/assets/drawio/images/sidebar-flowchart.png +0 -0
  107. package/assets/drawio/images/sidebar-gcp3.png +0 -0
  108. package/assets/drawio/images/sidebar-general.png +0 -0
  109. package/assets/drawio/images/sidebar-kubernetes.png +0 -0
  110. package/assets/drawio/images/sidebar-leanmapping.png +0 -0
  111. package/assets/drawio/images/sidebar-mockups.png +0 -0
  112. package/assets/drawio/images/sidebar-network.png +0 -0
  113. package/assets/drawio/images/sidebar-network2.png +0 -0
  114. package/assets/drawio/images/sidebar-openstack.png +0 -0
  115. package/assets/drawio/images/sidebar-sitemap.png +0 -0
  116. package/assets/drawio/images/sidebar-threatmodeling.png +0 -0
  117. package/assets/drawio/images/sidebar-uml.png +0 -0
  118. package/assets/drawio/images/sketch-logo128.png +0 -0
  119. package/assets/drawio/images/spin.gif +0 -0
  120. package/assets/drawio/images/sprites.png +0 -0
  121. package/assets/drawio/images/stop-flat-icon-80.png +0 -0
  122. package/assets/drawio/images/submenu.gif +0 -0
  123. package/assets/drawio/images/touch-submenu.png +0 -0
  124. package/assets/drawio/images/transparent.gif +0 -0
  125. package/assets/drawio/images/trello-logo-white-orange.svg +6 -0
  126. package/assets/drawio/images/trello-logo-white.svg +6 -0
  127. package/assets/drawio/images/trello-logo.svg +1 -0
  128. package/assets/drawio/images/tweet.png +0 -0
  129. package/assets/drawio/images/update16.png +0 -0
  130. package/assets/drawio/images/update32.png +0 -0
  131. package/assets/drawio/images/update80.png +0 -0
  132. package/assets/drawio/images/youtube.png +0 -0
  133. package/assets/drawio/img/clipart/Battery_0_128x128.png +0 -0
  134. package/assets/drawio/img/clipart/Battery_100_128x128.png +0 -0
  135. package/assets/drawio/img/clipart/Battery_50_128x128.png +0 -0
  136. package/assets/drawio/img/clipart/Battery_75_128x128.png +0 -0
  137. package/assets/drawio/img/clipart/Battery_allstates_128x128.png +0 -0
  138. package/assets/drawio/img/clipart/Bluetooth_128x128.png +0 -0
  139. package/assets/drawio/img/clipart/Earth_globe_128x128.png +0 -0
  140. package/assets/drawio/img/clipart/Empty_Folder_128x128.png +0 -0
  141. package/assets/drawio/img/clipart/Full_Folder_128x128.png +0 -0
  142. package/assets/drawio/img/clipart/Gear_128x128.png +0 -0
  143. package/assets/drawio/img/clipart/Keys_128x128.png +0 -0
  144. package/assets/drawio/img/clipart/Lock_128x128.png +0 -0
  145. package/assets/drawio/img/clipart/Mouse_Pointer_128x128.png +0 -0
  146. package/assets/drawio/img/clipart/Plug_128x128.png +0 -0
  147. package/assets/drawio/img/clipart/Ships_Wheel_128x128.png +0 -0
  148. package/assets/drawio/img/clipart/Star_128x128.png +0 -0
  149. package/assets/drawio/img/clipart/Tire_128x128.png +0 -0
  150. package/assets/drawio/index.html +92 -0
  151. package/assets/drawio/js/PostConfig.js +7 -0
  152. package/assets/drawio/js/PreConfig.js +13 -0
  153. package/assets/drawio/js/app.min.js +18466 -0
  154. package/assets/drawio/js/bootstrap.js +428 -0
  155. package/assets/drawio/js/diagramly/LibavoidRouting.js +4459 -0
  156. package/assets/drawio/js/export-init.js +21 -0
  157. package/assets/drawio/js/extensions.min.js +10478 -0
  158. package/assets/drawio/js/libavoid-js/LICENSE +505 -0
  159. package/assets/drawio/js/libavoid-js/libavoid-routing.js +1633 -0
  160. package/assets/drawio/js/libavoid-js/libavoid.min.js +59 -0
  161. package/assets/drawio/js/main.js +22 -0
  162. package/assets/drawio/js/rough/rough.min.js +1 -0
  163. package/assets/drawio/js/shapes-14-6-5.min.js +3457 -0
  164. package/assets/drawio/js/stencils.min.js +447 -0
  165. package/assets/drawio/mxgraph/css/common.css +147 -0
  166. package/assets/drawio/resources/dia.txt +2901 -0
  167. package/assets/drawio/styles/dark-default.xml +109 -0
  168. package/assets/drawio/styles/default-old.xml +109 -0
  169. package/assets/drawio/styles/default.xml +203 -0
  170. package/assets/drawio/styles/fonts/ArchitectsDaughter-Regular.ttf +0 -0
  171. package/assets/drawio/styles/grapheditor.css +4501 -0
  172. package/assets/drawio/styles/high-contrast.css +128 -0
  173. package/assets/drawio/styles/thumb_horz.png +0 -0
  174. package/assets/drawio/styles/thumb_vertical.png +0 -0
  175. package/assets/fonts/ibm-plex-mono-LICENSE.txt +93 -0
  176. package/assets/fonts/ibm-plex-sans-LICENSE.txt +93 -0
  177. package/assets/fonts/kalam-400.woff2 +0 -0
  178. package/assets/fonts/kalam-700.woff2 +0 -0
  179. package/assets/fonts/kalam-LICENSE.txt +93 -0
  180. package/assets/fonts/plex-mono-400.woff2 +0 -0
  181. package/assets/fonts/plex-mono-600.woff2 +0 -0
  182. package/assets/fonts/plex-sans-400.woff2 +0 -0
  183. package/assets/fonts/plex-sans-600.woff2 +0 -0
  184. package/dist/api.js +181 -0
  185. package/dist/cli.js +262 -0
  186. package/dist/drawio/icons.js +121 -0
  187. package/dist/drawio/merge.js +309 -0
  188. package/dist/drawio/read.js +180 -0
  189. package/dist/drawio/show.js +501 -0
  190. package/dist/drawio/theme.js +425 -0
  191. package/dist/drawio/write.js +300 -0
  192. package/dist/edit/server.js +212 -0
  193. package/dist/git.js +46 -0
  194. package/dist/import/mermaid.js +569 -0
  195. package/dist/layout/layout.js +212 -0
  196. package/dist/layout/sequence.js +333 -0
  197. package/dist/layout/text.js +73 -0
  198. package/dist/lint/check.js +184 -0
  199. package/dist/lint/fixlabels.js +128 -0
  200. package/dist/mcp/server.js +239 -0
  201. package/dist/publish.js +121 -0
  202. package/dist/render/browser.js +210 -0
  203. package/dist/render/fonts.js +66 -0
  204. package/dist/report.js +36 -0
  205. package/dist/spec/model.js +53 -0
  206. package/dist/spec/parse.js +497 -0
  207. package/package.json +71 -4
  208. package/skills/linework/SKILL.md +214 -0
  209. package/skills/linework/examples/billing-er.drawio.svg +4 -0
  210. package/skills/linework/examples/billing-er.yaml +31 -0
  211. package/skills/linework/examples/checkout.drawio.svg +4 -0
  212. package/skills/linework/examples/checkout.yaml +28 -0
  213. package/skills/linework/examples/pay-sequence.drawio.svg +4 -0
  214. package/skills/linework/examples/pay-sequence.yaml +28 -0
  215. package/skills/linework/examples/payment-state.drawio.svg +4 -0
  216. package/skills/linework/examples/payment-state.yaml +22 -0
  217. package/skills/linework/examples/refund-flow.drawio.svg +4 -0
  218. package/skills/linework/examples/refund-flow.yaml +29 -0
  219. package/skills/linework/examples/serverless-aws.drawio.svg +4 -0
  220. package/skills/linework/examples/serverless-aws.yaml +23 -0
  221. package/skills/linework/references/google-docs.md +46 -0
  222. package/skills/linework/references/spec.md +312 -0
  223. package/skills/linework/references/style-guide.md +53 -0
@@ -0,0 +1,569 @@
1
+ // Mermaid -> linework Diagram. Covers the subset people actually use in design docs:
2
+ // flowchart/graph, stateDiagram(-v2), sequenceDiagram and erDiagram. Styling
3
+ // directives (classDef, style, linkStyle, click) are dropped with a warning.
4
+ import { humanize } from '../spec/parse.js';
5
+ export function importMermaid(src) {
6
+ const lines = src
7
+ .replace(/^---[\s\S]*?\n---\s*\n/, '') // front matter
8
+ .split('\n')
9
+ .map((l) => l.replace(/%%.*$/, '').trimEnd())
10
+ .filter((l) => l.trim());
11
+ const head = (lines[0] ?? '').trim();
12
+ if (/^(flowchart|graph)\b/i.test(head))
13
+ return flowchart(lines);
14
+ if (/^stateDiagram(-v2)?\b/.test(head))
15
+ return stateDiagram(lines);
16
+ if (/^sequenceDiagram\b/.test(head))
17
+ return sequenceDiagram(lines);
18
+ if (/^erDiagram\b/.test(head))
19
+ return erDiagram(lines);
20
+ throw new Error(`unsupported Mermaid diagram "${head.split(/\s/)[0]}". linework imports flowchart/graph, stateDiagram, sequenceDiagram and erDiagram.`);
21
+ }
22
+ /** Pull ```mermaid blocks out of a Markdown document. */
23
+ export function mermaidBlocks(markdown) {
24
+ return [...markdown.matchAll(/^```mermaid[^\n]*\n([\s\S]*?)^```/gm)].map((m) => m[1]);
25
+ }
26
+ const clean = (s) => s
27
+ .trim()
28
+ .replace(/^"(.*)"$/s, '$1')
29
+ .replace(/^`(.*)`$/s, '$1')
30
+ .replace(/\*\*(.*?)\*\*/g, '$1')
31
+ .replace(/"/g, '"')
32
+ .replace(/#quot;/g, '"')
33
+ .trim();
34
+ const brSplit = (s) => clean(s).split(/<br\s*\/?>|\\n/i).map((x) => x.replace(/<[^>]+>/g, '').trim()).filter(Boolean);
35
+ function safeId(raw, used, prefer) {
36
+ let id = (prefer ?? raw).replace(/[^\w.-]+/g, '_').replace(/^[^A-Za-z_]+/, '');
37
+ if (!id)
38
+ id = 'n';
39
+ const base = id;
40
+ let k = 2;
41
+ while (used.has(id))
42
+ id = `${base}_${k++}`;
43
+ used.add(id);
44
+ return id;
45
+ }
46
+ // ---------- flowchart ----------
47
+ const SHAPES = [
48
+ ['(((', ')))', 'circle'],
49
+ ['([', '])', 'stadium'],
50
+ ['[(', ')]', 'cylinder'],
51
+ ['[[', ']]', 'subroutine'],
52
+ ['((', '))', 'circle'],
53
+ ['{{', '}}', 'hexagon'],
54
+ ['[/', '/]', 'para'],
55
+ ['[\\', '\\]', 'para'],
56
+ ['[/', '\\]', 'trap'],
57
+ ['[\\', '/]', 'trap'],
58
+ ['[', ']', 'rect'],
59
+ ['(', ')', 'round'],
60
+ ['{', '}', 'diamond'],
61
+ ['>', ']', 'flag'],
62
+ ];
63
+ function flowchart(lines) {
64
+ const warnings = new Set();
65
+ const dirTok = /^(?:flowchart|graph)\s+(TD|TB|BT|RL|LR)/i.exec(lines[0].trim())?.[1]?.toUpperCase();
66
+ const direction = dirTok === 'TD' ? 'TB' : (dirTok ?? 'auto');
67
+ const nodes = new Map();
68
+ const order = [];
69
+ const edges = [];
70
+ const groups = [];
71
+ const stack = [];
72
+ const touch = (n) => {
73
+ const cur = nodes.get(n.id);
74
+ if (!cur) {
75
+ nodes.set(n.id, { ...n, group: stack[stack.length - 1] });
76
+ order.push(n.id);
77
+ }
78
+ else {
79
+ if (n.text != null)
80
+ cur.text = n.text;
81
+ if (n.shape)
82
+ cur.shape = n.shape;
83
+ // Mermaid puts a node in the subgraph that mentions it, even if it was used earlier.
84
+ if (!cur.group && stack.length)
85
+ cur.group = stack[stack.length - 1];
86
+ }
87
+ };
88
+ for (const raw of lines.slice(1)) {
89
+ for (const stmt of splitStatements(raw.trim())) {
90
+ if (!stmt)
91
+ continue;
92
+ let m;
93
+ if ((m = /^subgraph\s+(.+)$/.exec(stmt))) {
94
+ const body = m[1].trim();
95
+ const g = /^([\w.-]+)\s*\[(.*)\]$/.exec(body) ?? /^([\w.-]+)\s+\[(.*)\]$/.exec(body);
96
+ const rawId = g ? g[1] : body.replace(/^"|"$/g, '');
97
+ const label = g ? clean(g[2]) : clean(body);
98
+ groups.push({ raw: rawId, label, parent: stack[stack.length - 1] });
99
+ stack.push(rawId);
100
+ continue;
101
+ }
102
+ if (stmt === 'end') {
103
+ stack.pop();
104
+ continue;
105
+ }
106
+ if (/^(classDef|class|style|linkStyle|click|accTitle|accDescr)\b/.test(stmt)) {
107
+ warnings.add('styling directives (classDef, style, click, ...) are dropped; linework applies its own theme');
108
+ continue;
109
+ }
110
+ if (/^direction\s+/.test(stmt))
111
+ continue;
112
+ parseChain(stmt, touch, edges, warnings);
113
+ }
114
+ }
115
+ // Pick a type: diamonds mean a process; cylinders or subgraphs mean architecture.
116
+ const shapes = [...nodes.values()].map((n) => n.shape);
117
+ const type = shapes.includes('diamond') ? 'flow' : shapes.includes('cylinder') || groups.length ? 'architecture' : 'flow';
118
+ const used = new Set();
119
+ const gid = new Map();
120
+ for (const g of groups)
121
+ gid.set(g.raw, safeId(g.raw, used));
122
+ const nid = new Map();
123
+ for (const id of order)
124
+ nid.set(id, safeId(id, used));
125
+ const outGroups = groups.map((g) => ({ id: gid.get(g.raw), label: g.label, parent: g.parent ? gid.get(g.parent) : undefined }));
126
+ const outNodes = order.map((raw) => {
127
+ const n = nodes.get(raw);
128
+ const parts = brSplit(n.text ?? raw);
129
+ const label = parts[0] ?? raw;
130
+ const node = { id: nid.get(raw), label, kind: kindFor(type, n.shape, label), parent: n.group ? gid.get(n.group) : undefined };
131
+ if (type === 'architecture' && parts.length > 1)
132
+ node.tech = parts.slice(1).join(' ');
133
+ else if (parts.length > 1)
134
+ node.label = parts.join(' ');
135
+ return node;
136
+ });
137
+ const counts = new Map();
138
+ const outEdges = edges.map((e) => {
139
+ const from = nid.get(e.from), to = nid.get(e.to);
140
+ const base = `${from}->${to}`;
141
+ const k = (counts.get(base) ?? 0) + 1;
142
+ counts.set(base, k);
143
+ return { id: k === 1 ? base : `${base}#${k}`, from, to, label: e.label, style: e.style, ...(e.highlight ? { highlight: true } : {}) };
144
+ });
145
+ return { diagram: { type, direction, theme: 'clean', groups: outGroups, nodes: outNodes, edges: outEdges }, warnings: [...warnings] };
146
+ }
147
+ function splitStatements(line) {
148
+ // Split on ';' outside brackets and quotes.
149
+ const out = [];
150
+ let depth = 0, q = false, cur = '';
151
+ for (const ch of line) {
152
+ if (ch === '"')
153
+ q = !q;
154
+ if (!q && '[({'.includes(ch))
155
+ depth++;
156
+ if (!q && '])}'.includes(ch))
157
+ depth = Math.max(0, depth - 1);
158
+ if (ch === ';' && !q && depth === 0) {
159
+ out.push(cur.trim());
160
+ cur = '';
161
+ }
162
+ else
163
+ cur += ch;
164
+ }
165
+ out.push(cur.trim());
166
+ return out;
167
+ }
168
+ const ID_AT = /^[\w.À-￿-]+?(?=\s|$|[\[({>&|]|-->|---|-\.|==|--|<--|<==)/u;
169
+ function parseNodeAt(s, pos) {
170
+ const rest = s.slice(pos);
171
+ const idm = ID_AT.exec(rest) ?? /^[\w.-]+/.exec(rest);
172
+ if (!idm)
173
+ return null;
174
+ let end = pos + idm[0].length;
175
+ const node = { id: idm[0] };
176
+ // new-style shape syntax: A@{ shape: cyl, label: "x" }
177
+ const at = /^@\{([^}]*)\}/.exec(s.slice(end));
178
+ if (at) {
179
+ const lab = /label:\s*"([^"]*)"/.exec(at[1]);
180
+ const shape = /shape:\s*([\w-]+)/.exec(at[1])?.[1];
181
+ node.text = lab?.[1];
182
+ node.shape = shape === 'cyl' || shape === 'database' ? 'cylinder' : shape === 'diam' || shape === 'decision' ? 'diamond' : shape === 'stadium' || shape === 'terminal' ? 'stadium' : 'rect';
183
+ return { node, end: end + at[0].length };
184
+ }
185
+ for (const [open, close, shape] of SHAPES) {
186
+ if (!s.startsWith(open, end))
187
+ continue;
188
+ const textStart = end + open.length;
189
+ let i = textStart;
190
+ if (s[i] === '"') {
191
+ const qEnd = s.indexOf('"', i + 1);
192
+ if (qEnd < 0)
193
+ continue;
194
+ i = qEnd + 1;
195
+ }
196
+ const closeAt = s.indexOf(close, i);
197
+ if (closeAt < 0)
198
+ continue;
199
+ node.text = s.slice(textStart, closeAt);
200
+ node.shape = shape;
201
+ end = closeAt + close.length;
202
+ break;
203
+ }
204
+ return { node, end };
205
+ }
206
+ function parseEdgeAt(s, pos) {
207
+ const rest = s.slice(pos);
208
+ // "-- text -->", "== text ==>", "-. text .->"
209
+ let m = /^\s*(<)?(--|==|-\.)\s+([^>|]+?)\s+(-{2,}|={2,}|\.+-)(>|o|x)?(?=\s|[\w"])/.exec(rest);
210
+ if (m)
211
+ return { style: styleOf(m[1], m[2] + m[4], m[5]), label: clean(m[3]), end: pos + m[0].length, thick: m[2] === '==' };
212
+ // "-->", "---", "-.->", "==>", "<-->", "--o", "--x", with optional "|text|"
213
+ m = /^\s*(<)?(-{2,}|={2,}|-\.+-?|\.-+)(>|o|x)?\s*(?:\|([^|]*)\|)?/.exec(rest);
214
+ if (m && /[-=.]/.test(m[2]) && m[0].trim())
215
+ return { style: styleOf(m[1], m[2], m[3]), label: m[4] != null ? clean(m[4]) : undefined, end: pos + m[0].length, thick: m[2].startsWith('=') };
216
+ return null;
217
+ }
218
+ function styleOf(back, line, head) {
219
+ const arrow = head === '>' || head === 'x' || head === 'o';
220
+ if (back && arrow)
221
+ return 'bidir';
222
+ if (!arrow && !back)
223
+ return 'line';
224
+ if (line.includes('.'))
225
+ return 'async';
226
+ return 'sync';
227
+ }
228
+ function parseChain(stmt, touch, edges, warnings) {
229
+ let pos = 0;
230
+ const readGroup = () => {
231
+ const ids = [];
232
+ for (;;) {
233
+ while (stmt[pos] === ' ')
234
+ pos++;
235
+ const r = parseNodeAt(stmt, pos);
236
+ if (!r)
237
+ return ids.length ? ids : null;
238
+ touch(r.node);
239
+ ids.push(r.node.id);
240
+ pos = r.end;
241
+ const amp = /^\s*&\s*/.exec(stmt.slice(pos));
242
+ if (!amp)
243
+ return ids;
244
+ pos += amp[0].length;
245
+ }
246
+ };
247
+ let left = readGroup();
248
+ if (!left) {
249
+ warnings.add(`skipped a line linework couldn't read: "${stmt.slice(0, 60)}"`);
250
+ return;
251
+ }
252
+ for (;;) {
253
+ const e = parseEdgeAt(stmt, pos);
254
+ if (!e)
255
+ break;
256
+ pos = e.end;
257
+ const right = readGroup();
258
+ if (!right)
259
+ break;
260
+ // Mermaid's thick links (==>) mark what matters: linework's highlight.
261
+ for (const a of left)
262
+ for (const b of right)
263
+ edges.push({ from: a, to: b, label: e.label || undefined, style: e.style, ...(e.thick ? { highlight: true } : {}) });
264
+ left = right;
265
+ }
266
+ }
267
+ function kindFor(type, shape, label) {
268
+ if (type === 'flow') {
269
+ if (shape === 'diamond')
270
+ return 'decision';
271
+ if (shape === 'stadium' || shape === 'circle')
272
+ return 'terminal';
273
+ if (shape === 'para' || shape === 'cylinder')
274
+ return 'data';
275
+ if (/\b(error|fail|reject|deny|denied)/i.test(label))
276
+ return 'error';
277
+ return 'step';
278
+ }
279
+ if (shape === 'cylinder')
280
+ return /bucket|s3|blob|storage|lake/i.test(label) ? 'storage' : 'database';
281
+ if (/^(users?|customers?|admins?|shoppers?|operators?|developers?|actors?)$/i.test(label))
282
+ return 'person';
283
+ if (/\b(browser|mobile|frontend|front-end|web ?app|spa|cli|ios|android)\b/i.test(label))
284
+ return 'client';
285
+ if (/\b(queue|kafka|sqs|pub\/?sub|topic|stream|rabbit|kinesis|nats)\b/i.test(label) || shape === 'stadium')
286
+ return 'queue';
287
+ if (/\b(redis|cache|memcache|cdn)\b/i.test(label))
288
+ return 'cache';
289
+ if (/\b(stripe|twilio|sendgrid|auth0|okta|github|slack|third.party|external)\b/i.test(label))
290
+ return 'external';
291
+ return 'service';
292
+ }
293
+ // ---------- state ----------
294
+ function stateDiagram(lines) {
295
+ const warnings = new Set();
296
+ let direction = 'auto';
297
+ const labels = new Map();
298
+ const order = [];
299
+ const parent = new Map();
300
+ const groups = [];
301
+ const stack = [];
302
+ const edges = [];
303
+ const touch = (id) => {
304
+ if (id === '[*]')
305
+ return;
306
+ if (!order.includes(id)) {
307
+ order.push(id);
308
+ parent.set(id, stack[stack.length - 1]);
309
+ }
310
+ };
311
+ for (const raw of lines.slice(1)) {
312
+ const l = raw.trim();
313
+ let m;
314
+ if ((m = /^direction\s+(LR|TB|RL|BT)/.exec(l))) {
315
+ if (!stack.length)
316
+ direction = m[1];
317
+ continue;
318
+ }
319
+ if ((m = /^state\s+"([^"]+)"\s+as\s+([\w.-]+)/.exec(l))) {
320
+ labels.set(m[2], m[1]);
321
+ touch(m[2]);
322
+ continue;
323
+ }
324
+ if ((m = /^state\s+([\w.-]+)\s*\{$/.exec(l))) {
325
+ groups.push({ raw: m[1], parent: stack[stack.length - 1] });
326
+ stack.push(m[1]);
327
+ continue;
328
+ }
329
+ if ((m = /^state\s+([\w.-]+)\s*<<\w+>>/.exec(l))) {
330
+ touch(m[1]);
331
+ warnings.add('choice/fork/join states become plain states');
332
+ continue;
333
+ }
334
+ if (l === '}') {
335
+ stack.pop();
336
+ continue;
337
+ }
338
+ if (/^(note|classDef|class|style)\b/.test(l)) {
339
+ warnings.add('notes and styling in state diagrams are dropped');
340
+ continue;
341
+ }
342
+ if ((m = /^(\[\*\]|[\w.-]+)\s*-->\s*(\[\*\]|[\w.-]+)\s*(?::\s*(.*))?$/.exec(l))) {
343
+ if (stack.length && (m[1] === '[*]' || m[2] === '[*]'))
344
+ warnings.add('start/end markers inside composite states are dropped');
345
+ if (stack.length && (m[1] === '[*]' || m[2] === '[*]')) {
346
+ touch(m[1]);
347
+ touch(m[2]);
348
+ continue;
349
+ }
350
+ touch(m[1]);
351
+ touch(m[2]);
352
+ edges.push({ from: m[1], to: m[2], label: m[3] ? clean(m[3]) : undefined });
353
+ continue;
354
+ }
355
+ if ((m = /^([\w.-]+)\s*:\s*(.+)$/.exec(l))) {
356
+ labels.set(m[1], clean(m[2]));
357
+ touch(m[1]);
358
+ continue;
359
+ }
360
+ if ((m = /^([\w.-]+)$/.exec(l))) {
361
+ touch(m[1]);
362
+ continue;
363
+ }
364
+ warnings.add(`skipped a line linework couldn't read: "${l.slice(0, 60)}"`);
365
+ }
366
+ const groupIds = new Set(groups.map((g) => g.raw));
367
+ const used = new Set();
368
+ const ids = new Map();
369
+ for (const g of groups)
370
+ ids.set(g.raw, safeId(g.raw, used));
371
+ for (const s of order)
372
+ if (!groupIds.has(s))
373
+ ids.set(s, safeId(s, used));
374
+ const nodes = order
375
+ .filter((s) => !groupIds.has(s))
376
+ .map((s) => {
377
+ const label = labels.get(s) ?? (/[a-z][A-Z]/.test(s) ? s.replace(/([a-z])([A-Z])/g, '$1 $2') : humanize(s));
378
+ return { id: ids.get(s), label, kind: /fail|error|reject|declin|cancel|abort/i.test(label) ? 'error' : /complete|succe|paid|done|deliver|captur|settled/i.test(label) ? 'success' : 'state', parent: parent.get(s) ? ids.get(parent.get(s)) : undefined };
379
+ });
380
+ // Edges into a composite state go to its first child instead.
381
+ const firstChild = (g) => nodes.find((n) => n.parent === ids.get(g))?.id;
382
+ let startUsed = false;
383
+ const endFor = new Set();
384
+ const counts = new Map();
385
+ const outEdges = edges.flatMap((e) => {
386
+ let from = e.from === '[*]' ? '_start' : groupIds.has(e.from) ? firstChild(e.from) : ids.get(e.from);
387
+ let to = e.to === '[*]' ? undefined : groupIds.has(e.to) ? firstChild(e.to) : ids.get(e.to);
388
+ if (!from)
389
+ return [];
390
+ if (e.from === '[*]')
391
+ startUsed = true;
392
+ if (e.to === '[*]') {
393
+ endFor.add(from);
394
+ to = `_end_${from === '_start' ? 'start' : from}`;
395
+ }
396
+ if (!to)
397
+ return [];
398
+ const base = `${from}->${to}`;
399
+ const k = (counts.get(base) ?? 0) + 1;
400
+ counts.set(base, k);
401
+ return [{ id: k === 1 ? base : `${base}#${k}`, from, to, label: e.label, style: 'sync' }];
402
+ });
403
+ if (startUsed)
404
+ nodes.unshift({ id: '_start', label: '', kind: 'start' });
405
+ for (const f of endFor)
406
+ nodes.push({ id: `_end_${f === '_start' ? 'start' : f}`, label: '', kind: 'end' });
407
+ const outGroups = groups.map((g) => ({ id: ids.get(g.raw), label: labels.get(g.raw) ?? humanize(g.raw), parent: g.parent ? ids.get(g.parent) : undefined }));
408
+ return { diagram: { type: 'state', direction, theme: 'clean', groups: outGroups, nodes, edges: outEdges }, warnings: [...warnings] };
409
+ }
410
+ // ---------- sequence ----------
411
+ const SEQ_ARROWS = [
412
+ ['-->>', 'reply'],
413
+ ['->>', 'sync'],
414
+ ['--x', 'reply'],
415
+ ['-x', 'sync'],
416
+ ['--)', 'async'],
417
+ ['-)', 'async'],
418
+ ['-->', 'reply'],
419
+ ['->', 'sync'],
420
+ ];
421
+ function sequenceDiagram(lines) {
422
+ const warnings = new Set();
423
+ const parts = new Map();
424
+ const used = new Set();
425
+ const pid = new Map();
426
+ const declare = (raw, label, kind = 'service') => {
427
+ if (pid.has(raw)) {
428
+ if (label)
429
+ parts.get(pid.get(raw)).label = label;
430
+ return pid.get(raw);
431
+ }
432
+ const id = safeId(raw, used);
433
+ pid.set(raw, id);
434
+ parts.set(id, { id, label: label ?? (raw.length <= 3 ? raw : humanize(raw)), kind: inferParticipant(label ?? raw, kind) });
435
+ return id;
436
+ };
437
+ const root = [];
438
+ const stack = [];
439
+ const cur = () => (stack.length ? stack[stack.length - 1].branches[stack[stack.length - 1].branches.length - 1].items : root);
440
+ for (const raw of lines.slice(1)) {
441
+ const l = raw.trim();
442
+ let m;
443
+ if ((m = /^(participant|actor)\s+(\S+?)(?:\s+as\s+(.+))?$/.exec(l))) {
444
+ declare(m[2], m[3] ? clean(m[3]) : undefined, m[1] === 'actor' ? 'person' : 'service');
445
+ continue;
446
+ }
447
+ if ((m = /^(alt|opt|loop|par|critical|break)\b\s*(.*)$/.exec(l))) {
448
+ stack.push({ op: m[1], branches: [{ when: m[2] ? clean(m[2]) : undefined, items: [] }] });
449
+ continue;
450
+ }
451
+ if ((m = /^(else|and|option)\b\s*(.*)$/.exec(l))) {
452
+ stack[stack.length - 1]?.branches.push({ when: m[2] ? clean(m[2]) : undefined, items: [] });
453
+ continue;
454
+ }
455
+ if (/^rect\b/.test(l)) {
456
+ stack.push({ op: 'rect', branches: [{ items: [] }] });
457
+ warnings.add('rect highlights are dropped');
458
+ continue;
459
+ }
460
+ if (l === 'end') {
461
+ const f = stack.pop();
462
+ if (!f)
463
+ continue;
464
+ if (f.op === 'rect')
465
+ cur().push(...f.branches[0].items);
466
+ else
467
+ cur().push({ kind: 'frame', op: f.op, branches: f.branches });
468
+ continue;
469
+ }
470
+ if ((m = /^note\s+(over|left of|right of)\s+([^:]+):\s*(.*)$/i.exec(l))) {
471
+ const over = m[2].split(',').map((s) => declare(s.trim()));
472
+ const side = m[1].toLowerCase().startsWith('left') ? 'left' : m[1].toLowerCase().startsWith('right') ? 'right' : 'over';
473
+ cur().push({ kind: 'note', over, side, text: clean(m[3]).replace(/<br\s*\/?>/gi, ' ') });
474
+ continue;
475
+ }
476
+ if (/^(activate|deactivate|autonumber|box|end box|title|create|destroy|links?|properties|details)\b/.test(l))
477
+ continue;
478
+ let matched = false;
479
+ for (const [tok, style] of SEQ_ARROWS) {
480
+ const i = l.indexOf(tok);
481
+ if (i <= 0)
482
+ continue;
483
+ const from = l.slice(0, i).trim();
484
+ const after = l.slice(i + tok.length);
485
+ const mm = /^\s*[+-]?\s*([^:]+?)\s*:\s*(.*)$/.exec(after) ?? /^\s*[+-]?\s*(\S+)\s*$/.exec(after);
486
+ if (!mm || !from)
487
+ continue;
488
+ cur().push({ kind: 'msg', from: declare(from), to: declare(mm[1].trim()), label: mm[2] ? clean(mm[2]) : undefined, style });
489
+ matched = true;
490
+ break;
491
+ }
492
+ if (!matched)
493
+ warnings.add(`skipped a line linework couldn't read: "${l.slice(0, 60)}"`);
494
+ }
495
+ return { diagram: { type: 'sequence', direction: 'auto', theme: 'clean', groups: [], nodes: [...parts.values()], edges: [], seq: root }, warnings: [...warnings] };
496
+ }
497
+ function inferParticipant(label, kind) {
498
+ if (kind === 'person')
499
+ return 'person';
500
+ if (/\b(db|database|postgres|mysql|redis|dynamo|mongo|cache)\b/i.test(label))
501
+ return 'database';
502
+ if (/\b(queue|kafka|sqs|topic|bus)\b/i.test(label))
503
+ return 'queue';
504
+ if (/\b(browser|client|frontend|web ?app|mobile|ui)\b/i.test(label))
505
+ return 'client';
506
+ if (/\b(stripe|twilio|sendgrid|auth0|okta|github|slack|external|third.party|provider)\b/i.test(label))
507
+ return 'external';
508
+ return 'service';
509
+ }
510
+ // ---------- ER ----------
511
+ const L_CARD = { '|o': 'zero_or_one', '||': 'one', '}o': 'many', '}|': 'one_or_many' };
512
+ const R_CARD = { 'o|': 'zero_or_one', '||': 'one', 'o{': 'many', '|{': 'one_or_many' };
513
+ function erDiagram(lines) {
514
+ const warnings = new Set();
515
+ const ents = new Map();
516
+ const used = new Set();
517
+ const eid = new Map();
518
+ const ent = (raw) => {
519
+ const name = raw.replace(/^"|"$/g, '');
520
+ if (!eid.has(name)) {
521
+ const id = safeId(name.toLowerCase(), used);
522
+ eid.set(name, id);
523
+ ents.set(id, { id, label: name, kind: 'entity', fields: [] });
524
+ }
525
+ return ents.get(eid.get(name));
526
+ };
527
+ const edges = [];
528
+ let open = null;
529
+ for (const raw of lines.slice(1)) {
530
+ const l = raw.trim();
531
+ let m;
532
+ if (open) {
533
+ if (l === '}') {
534
+ open = null;
535
+ continue;
536
+ }
537
+ if ((m = /^([\w()[\],.-]+)\s+([\w.-]+)((?:\s+(?:PK|FK|UK)\s*,?)*)\s*(?:"[^"]*")?$/i.exec(l))) {
538
+ const flags = (m[3] ?? '').split(/[\s,]+/).filter(Boolean).map((f) => (f.toLowerCase() === 'uk' ? 'unique' : f.toLowerCase()));
539
+ open.fields.push({ name: m[2], type: m[1], flags });
540
+ }
541
+ else
542
+ warnings.add(`skipped a field line: "${l.slice(0, 60)}"`);
543
+ continue;
544
+ }
545
+ if ((m = /^("?[\w.-]+"?)\s*(?:\["[^"]*"\])?\s*\{$/.exec(l))) {
546
+ open = ent(m[1]);
547
+ continue;
548
+ }
549
+ if ((m = /^("?[\w.-]+"?)\s+([|}][o|])(--|\.\.)([o|][|{])\s+("?[\w.-]+"?)\s*:\s*(.+)$/.exec(l))) {
550
+ const a = ent(m[1]), b = ent(m[5]);
551
+ const base = `${a.id}->${b.id}`;
552
+ const k = edges.filter((e) => e.id.startsWith(base)).length + 1;
553
+ edges.push({ id: k === 1 ? base : `${base}#${k}`, from: a.id, to: b.id, label: clean(m[6]) || undefined, style: 'line', card: { from: L_CARD[m[2]], to: R_CARD[m[4]], identifying: m[3] === '--' } });
554
+ continue;
555
+ }
556
+ if ((m = /^("?[\w.-]+"?)$/.exec(l))) {
557
+ ent(m[1]);
558
+ continue;
559
+ }
560
+ if (/^(direction|title|classDef|class|style)\b/.test(l))
561
+ continue;
562
+ warnings.add(`skipped a line linework couldn't read: "${l.slice(0, 60)}"`);
563
+ }
564
+ // Mermaid entity names are often UPPERCASE; show them as written.
565
+ for (const n of ents.values())
566
+ if (!n.fields?.length)
567
+ delete n.fields;
568
+ return { diagram: { type: 'er', direction: 'auto', theme: 'clean', groups: [], nodes: [...ents.values()], edges }, warnings: [...warnings] };
569
+ }