mosage 0.2.0 → 0.8.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 (121) hide show
  1. package/README.md +16 -15
  2. package/bin.js +2 -0
  3. package/dist/build-C7NW_3Pk.js +14 -0
  4. package/dist/check-CP4873Wx.js +41 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +228 -0
  7. package/dist/config-DPm1BBAb.js +2619 -0
  8. package/dist/config-TlTe7Ona.d.ts +24 -0
  9. package/dist/context-BqsdSrAQ.js +1084 -0
  10. package/dist/dev-Biz42qlu.js +17 -0
  11. package/dist/diagram-xlVDekYk.js +763 -0
  12. package/dist/export-Bi6nuxjT.js +31 -0
  13. package/dist/import-D2jNB07F.js +25 -0
  14. package/dist/index.d.ts +455 -0
  15. package/dist/index.js +693 -0
  16. package/dist/{cli.js → init-Bbtj2pxF.js} +34 -68
  17. package/dist/preview-CLm51aRt.js +19 -0
  18. package/dist/sdk-DjpX6mCv.js +51 -0
  19. package/dist/vite/index.d.ts +25 -0
  20. package/dist/vite/index.js +2 -0
  21. package/env.d.ts +83 -0
  22. package/package.json +59 -12
  23. package/{template/.agents/skills → skills}/create-doc/SKILL.md +1 -1
  24. package/{template/.agents/skills → skills}/create-theme/SKILL.md +1 -1
  25. package/{template/.agents/skills → skills}/doc-authoring/SKILL.md +8 -9
  26. package/{template/.agents/skills → skills}/doc-authoring/references/assets.md +1 -1
  27. package/{template/.agents/skills → skills}/doc-authoring/references/design-system.md +1 -1
  28. package/{template/.agents/skills → skills}/doc-authoring/references/long-form.md +4 -4
  29. package/{template/.agents/skills → skills}/doc-authoring/references/pagination.md +1 -1
  30. package/src/app/app.tsx +42 -0
  31. package/src/app/components/data-table.tsx +196 -0
  32. package/src/app/components/design-panel/design-panel.tsx +318 -0
  33. package/src/app/components/design-panel/design-provider.tsx +121 -0
  34. package/src/app/components/design-panel/use-design.ts +85 -0
  35. package/src/app/components/diagram.tsx +76 -0
  36. package/src/app/components/doc-assets.tsx +129 -0
  37. package/src/app/components/doc-search.tsx +248 -0
  38. package/src/app/components/doc-sidebar.tsx +162 -0
  39. package/src/app/components/flow-page.tsx +93 -0
  40. package/src/app/components/footnote.tsx +204 -0
  41. package/src/app/components/image-placeholder.tsx +50 -0
  42. package/src/app/components/inspector/inspector.tsx +518 -0
  43. package/src/app/components/numbering.tsx +224 -0
  44. package/src/app/components/page-frame.tsx +70 -0
  45. package/src/app/components/sidebar/folder-item.tsx +212 -0
  46. package/src/app/components/sidebar/icon-picker.tsx +99 -0
  47. package/src/app/components/sidebar/sidebar.tsx +252 -0
  48. package/src/app/components/table-of-contents.tsx +93 -0
  49. package/src/app/components/theme-toggle.tsx +50 -0
  50. package/src/app/components/themes/markdown.tsx +249 -0
  51. package/src/app/components/themes/theme-preview.tsx +74 -0
  52. package/src/app/components/ui/menu.tsx +143 -0
  53. package/src/app/index.html +12 -0
  54. package/src/app/lib/agent-bridge.ts +140 -0
  55. package/src/app/lib/assets.ts +151 -0
  56. package/src/app/lib/design-presets.ts +109 -0
  57. package/src/app/lib/design.ts +88 -0
  58. package/src/app/lib/diagnostics.ts +282 -0
  59. package/src/app/lib/doc-preview.tsx +29 -0
  60. package/src/app/lib/docs.ts +26 -0
  61. package/src/app/lib/docx/extract.ts +1623 -0
  62. package/src/app/lib/docx/fonts.test.ts +136 -0
  63. package/src/app/lib/docx/fonts.ts +166 -0
  64. package/src/app/lib/docx/media.ts +102 -0
  65. package/src/app/lib/docx/model.ts +206 -0
  66. package/src/app/lib/docx/paragraph.test.ts +92 -0
  67. package/src/app/lib/docx/paragraph.ts +107 -0
  68. package/src/app/lib/docx/props.ts +187 -0
  69. package/src/app/lib/docx/styles.ts +306 -0
  70. package/src/app/lib/docx/units.ts +35 -0
  71. package/src/app/lib/docx/write.test.ts +507 -0
  72. package/src/app/lib/docx/write.ts +581 -0
  73. package/src/app/lib/docx/xml.ts +39 -0
  74. package/src/app/lib/export-docx.ts +289 -0
  75. package/src/app/lib/export-dom.ts +318 -0
  76. package/src/app/lib/export-html.ts +156 -0
  77. package/src/app/lib/export-image.ts +70 -0
  78. package/src/app/lib/export-pdf.ts +165 -0
  79. package/src/app/lib/flow-measure.test.ts +31 -0
  80. package/src/app/lib/flow-measure.ts +183 -0
  81. package/src/app/lib/flow.test.ts +110 -0
  82. package/src/app/lib/flow.ts +136 -0
  83. package/src/app/lib/folders.ts +192 -0
  84. package/src/app/lib/footnotes.test.tsx +102 -0
  85. package/src/app/lib/footnotes.ts +94 -0
  86. package/src/app/lib/inspector/fiber.ts +99 -0
  87. package/src/app/lib/labels.test.ts +18 -0
  88. package/src/app/lib/labels.ts +181 -0
  89. package/src/app/lib/outline.ts +118 -0
  90. package/src/app/lib/page-context.tsx +43 -0
  91. package/src/app/lib/page-range.test.ts +95 -0
  92. package/src/app/lib/page-range.ts +90 -0
  93. package/src/app/lib/print-ready.ts +69 -0
  94. package/src/app/lib/rasterize.ts +173 -0
  95. package/src/app/lib/scan.ts +26 -0
  96. package/src/app/lib/sdk.test.ts +32 -0
  97. package/src/app/lib/sdk.ts +115 -0
  98. package/src/app/lib/themes.ts +31 -0
  99. package/src/app/lib/use-doc-module.ts +53 -0
  100. package/src/app/lib/use-doc-pages.ts +147 -0
  101. package/src/app/lib/utils.ts +6 -0
  102. package/src/app/lib/view-mode.test.ts +91 -0
  103. package/src/app/lib/view-mode.ts +104 -0
  104. package/src/app/main.tsx +14 -0
  105. package/src/app/routes/assets.tsx +257 -0
  106. package/src/app/routes/doc.tsx +877 -0
  107. package/src/app/routes/home-shell.tsx +203 -0
  108. package/src/app/routes/home.tsx +269 -0
  109. package/src/app/routes/themes.tsx +121 -0
  110. package/src/app/styles.css +97 -0
  111. package/src/app/virtual.d.ts +30 -0
  112. package/template/AGENTS.md +1 -1
  113. package/template/README.md +24 -52
  114. package/template/docs/getting-started/index.tsx +2 -2
  115. package/template/mosage.config.ts +1 -1
  116. package/template/package.json +1 -1
  117. package/template/tsconfig.json +1 -1
  118. package/README.zh-TW.md +0 -28
  119. /package/{template/.agents/skills → skills}/apply-comments/SKILL.md +0 -0
  120. /package/{template/.agents/skills → skills}/current-doc/SKILL.md +0 -0
  121. /package/{template/.agents/skills → skills}/doc-authoring/references/tables-and-charts.md +0 -0
@@ -0,0 +1,763 @@
1
+ //#region src/app/lib/design.ts
2
+ function designToCssVars(d) {
3
+ return {
4
+ "--od-bg": d.palette.bg,
5
+ "--od-text": d.palette.text,
6
+ "--od-muted": d.palette.muted,
7
+ "--od-accent": d.palette.accent,
8
+ "--od-rule": d.palette.rule,
9
+ "--od-font-heading": d.fonts.heading,
10
+ "--od-font-body": d.fonts.body,
11
+ "--od-font-mono": d.fonts.mono,
12
+ "--od-size-title": `${d.typeScale.title}px`,
13
+ "--od-size-h1": `${d.typeScale.h1}px`,
14
+ "--od-size-h2": `${d.typeScale.h2}px`,
15
+ "--od-size-h3": `${d.typeScale.h3}px`,
16
+ "--od-size-body": `${d.typeScale.body}px`,
17
+ "--od-size-caption": `${d.typeScale.caption}px`,
18
+ "--od-margin": `${d.margin}px`,
19
+ "--od-leading": String(d.leading),
20
+ "--od-radius": `${d.radius}px`
21
+ };
22
+ }
23
+ function cssVarsToString(vars) {
24
+ return Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
25
+ }
26
+ const defaultDesign = {
27
+ palette: {
28
+ bg: "#ffffff",
29
+ text: "#16181d",
30
+ muted: "#6b7280",
31
+ accent: "#2563eb",
32
+ rule: "#e5e7eb"
33
+ },
34
+ fonts: {
35
+ heading: "-apple-system, BlinkMacSystemFont, \"Inter\", system-ui, sans-serif",
36
+ body: "-apple-system, BlinkMacSystemFont, \"Inter\", system-ui, sans-serif",
37
+ mono: "ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, monospace"
38
+ },
39
+ typeScale: {
40
+ title: 44,
41
+ h1: 28,
42
+ h2: 20,
43
+ h3: 16,
44
+ body: 14,
45
+ caption: 10
46
+ },
47
+ margin: 76,
48
+ leading: 1.55,
49
+ radius: 6
50
+ };
51
+ //#endregion
52
+ //#region src/data/delimited.ts
53
+ /**
54
+ * RFC 4180 with the parts that actually show up in exported data: quoted
55
+ * fields, doubled quotes inside them, embedded newlines, and CRLF.
56
+ *
57
+ * Hand-written because `core` ships to every user and a CSV dependency is a lot
58
+ * of install size for something a report needs in one place.
59
+ */
60
+ function splitRecords(text, delimiter) {
61
+ const records = [];
62
+ let row = [];
63
+ let field = "";
64
+ let quoted = false;
65
+ let started = false;
66
+ const endField = () => {
67
+ row.push(field);
68
+ field = "";
69
+ started = false;
70
+ };
71
+ const endRow = () => {
72
+ endField();
73
+ records.push(row);
74
+ row = [];
75
+ };
76
+ for (let i = 0; i < text.length; i++) {
77
+ const char = text[i];
78
+ if (quoted) {
79
+ if (char === "\"") {
80
+ if (text[i + 1] === "\"") {
81
+ field += "\"";
82
+ i++;
83
+ continue;
84
+ }
85
+ quoted = false;
86
+ continue;
87
+ }
88
+ field += char;
89
+ continue;
90
+ }
91
+ if (char === "\"" && !started) {
92
+ quoted = true;
93
+ started = true;
94
+ continue;
95
+ }
96
+ if (char === delimiter) {
97
+ endField();
98
+ continue;
99
+ }
100
+ if (char === "\r") {
101
+ if (text[i + 1] === "\n") i++;
102
+ endRow();
103
+ continue;
104
+ }
105
+ if (char === "\n") {
106
+ endRow();
107
+ continue;
108
+ }
109
+ field += char;
110
+ started = true;
111
+ }
112
+ if (field !== "" || row.length > 0) endRow();
113
+ return records;
114
+ }
115
+ /** Numbers stay numbers so a table can align and format them; nothing else is guessed at. */
116
+ function coerce(raw) {
117
+ const value = raw.trim();
118
+ if (value === "") return null;
119
+ if (/^-?(?:\d+|\d*\.\d+)(?:[eE][-+]?\d+)?$/.test(value)) {
120
+ const n = Number(value);
121
+ if (Number.isFinite(n)) return n;
122
+ }
123
+ return value;
124
+ }
125
+ function parseDelimited(text, opts = {}) {
126
+ const delimiter = opts.delimiter ?? ",";
127
+ const records = splitRecords(text.replace(/^/, ""), delimiter).filter((record) => record.length > 1 || (record[0] ?? "") !== "");
128
+ if (records.length === 0) return {
129
+ columns: [],
130
+ rows: []
131
+ };
132
+ const header = opts.noHeader ? records[0].map((_, index) => `column${index + 1}`) : records[0].map((name, index) => name.trim() || `column${index + 1}`);
133
+ return {
134
+ columns: header,
135
+ rows: (opts.noHeader ? records : records.slice(1)).map((record) => {
136
+ const row = {};
137
+ header.forEach((name, index) => {
138
+ row[name] = coerce(record[index] ?? "");
139
+ });
140
+ return row;
141
+ })
142
+ };
143
+ }
144
+ //#endregion
145
+ //#region src/diagram/layout.ts
146
+ const DEFAULTS = {
147
+ fontSize: 13,
148
+ rankGap: 48,
149
+ nodeGap: 28,
150
+ maxLabelWidth: 190,
151
+ padding: 8
152
+ };
153
+ const MIN_WIDTH = 76;
154
+ const LINE_HEIGHT$1 = 1.35;
155
+ /**
156
+ * Text has to be measured without a DOM: the SVG is produced in the Vite
157
+ * plugin, at build time, where there is no browser to ask. Full-width
158
+ * characters take about one em and Latin about half of one, which is close
159
+ * enough that a box never clips its own label.
160
+ */
161
+ function measureText(text, fontSize) {
162
+ let width = 0;
163
+ for (const char of text) {
164
+ const code = char.codePointAt(0) ?? 0;
165
+ width += code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 ? fontSize : fontSize * .53;
166
+ }
167
+ return width;
168
+ }
169
+ /** Wraps on spaces where there are any, and between characters where there are not. */
170
+ function wrapLabel(text, fontSize, maxWidth) {
171
+ const explicit = text.split(/<br\s*\/?>/);
172
+ const lines = [];
173
+ for (const segment of explicit) {
174
+ const trimmed = segment.trim();
175
+ if (!trimmed) continue;
176
+ if (measureText(trimmed, fontSize) <= maxWidth) {
177
+ lines.push(trimmed);
178
+ continue;
179
+ }
180
+ const words = trimmed.includes(" ") ? trimmed.split(/\s+/) : [...trimmed];
181
+ const joiner = trimmed.includes(" ") ? " " : "";
182
+ let current = "";
183
+ for (const word of words) {
184
+ const candidate = current ? current + joiner + word : word;
185
+ if (current && measureText(candidate, fontSize) > maxWidth) {
186
+ lines.push(current);
187
+ current = word;
188
+ } else current = candidate;
189
+ }
190
+ if (current) lines.push(current);
191
+ }
192
+ return lines.length > 0 ? lines : [""];
193
+ }
194
+ function sizeOf(node, lines, fontSize) {
195
+ const textWidth = Math.max(...lines.map((line) => measureText(line, fontSize)));
196
+ const textHeight = lines.length * fontSize * LINE_HEIGHT$1;
197
+ let width = Math.max(MIN_WIDTH, textWidth + 28);
198
+ let height = textHeight + 20;
199
+ if (node.shape === "diamond") {
200
+ width = textWidth * 2 + 28;
201
+ height = textHeight * 2 + 20;
202
+ }
203
+ if (node.shape === "circle") {
204
+ const diameter = Math.max(textWidth, textHeight) + 28;
205
+ width = diameter;
206
+ height = diameter;
207
+ }
208
+ return {
209
+ width: Math.round(width),
210
+ height: Math.round(height)
211
+ };
212
+ }
213
+ /**
214
+ * Ranks by longest path. Edges that close a cycle are excluded from ranking —
215
+ * a feedback arrow in an architecture diagram is normal, and it must not make
216
+ * the graph unrankable.
217
+ */
218
+ function assignRanks(nodes, edges) {
219
+ const ids = nodes.map((n) => n.id);
220
+ const outgoing = new Map(ids.map((id) => [id, []]));
221
+ for (const edge of edges) outgoing.get(edge.from)?.push(edge.to);
222
+ const state = /* @__PURE__ */ new Map();
223
+ const backEdges = /* @__PURE__ */ new Set();
224
+ const key = (from, to) => `${from} ${to}`;
225
+ const visit = (id) => {
226
+ state.set(id, 1);
227
+ for (const next of outgoing.get(id) ?? []) {
228
+ const seen = state.get(next);
229
+ if (seen === 1) backEdges.add(key(id, next));
230
+ else if (seen === void 0) visit(next);
231
+ }
232
+ state.set(id, 2);
233
+ };
234
+ for (const id of ids) if (state.get(id) === void 0) visit(id);
235
+ const forward = edges.filter((e) => !backEdges.has(key(e.from, e.to)));
236
+ const incoming = new Map(ids.map((id) => [id, []]));
237
+ for (const edge of forward) incoming.get(edge.to)?.push(edge.from);
238
+ const rank = /* @__PURE__ */ new Map();
239
+ const resolve = (id, guard) => {
240
+ const cached = rank.get(id);
241
+ if (cached !== void 0) return cached;
242
+ if (guard.has(id)) return 0;
243
+ guard.add(id);
244
+ const parents = incoming.get(id) ?? [];
245
+ const value = parents.length === 0 ? 0 : Math.max(...parents.map((p) => resolve(p, guard) + 1));
246
+ guard.delete(id);
247
+ rank.set(id, value);
248
+ return value;
249
+ };
250
+ for (const id of ids) resolve(id, /* @__PURE__ */ new Set());
251
+ return rank;
252
+ }
253
+ /** Barycenter sweeps — cheap, and enough to untangle the graphs a report contains. */
254
+ function orderRanks(ranks, edges) {
255
+ const neighbours = /* @__PURE__ */ new Map();
256
+ for (const rank of ranks) for (const id of rank) neighbours.set(id, {
257
+ up: [],
258
+ down: []
259
+ });
260
+ for (const edge of edges) {
261
+ neighbours.get(edge.to)?.up.push(edge.from);
262
+ neighbours.get(edge.from)?.down.push(edge.to);
263
+ }
264
+ const positions = /* @__PURE__ */ new Map();
265
+ const index = () => {
266
+ for (const rank of ranks) rank.forEach((id, i) => {
267
+ positions.set(id, i);
268
+ });
269
+ };
270
+ index();
271
+ const sweep = (side) => {
272
+ for (const rank of ranks) {
273
+ const scored = rank.map((id, i) => {
274
+ const related = neighbours.get(id)?.[side] ?? [];
275
+ const known = [];
276
+ for (const other of related) {
277
+ const at = positions.get(other);
278
+ if (at !== void 0) known.push(at);
279
+ }
280
+ return {
281
+ id,
282
+ score: known.length ? known.reduce((a, b) => a + b, 0) / known.length : i
283
+ };
284
+ });
285
+ scored.sort((a, b) => a.score - b.score);
286
+ rank.splice(0, rank.length, ...scored.map((s) => s.id));
287
+ }
288
+ index();
289
+ };
290
+ for (let pass = 0; pass < 4; pass += 1) {
291
+ sweep("up");
292
+ sweep("down");
293
+ }
294
+ }
295
+ /**
296
+ * The point a given fraction along the polyline by length. Taking a *vertex*
297
+ * instead would park every label on an elbow, where two of them meet.
298
+ */
299
+ function pointAlong(points, fraction) {
300
+ let total = 0;
301
+ const spans = [];
302
+ for (let i = 1; i < points.length; i += 1) {
303
+ const span = Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
304
+ spans.push(span);
305
+ total += span;
306
+ }
307
+ const target = total * fraction;
308
+ let walked = 0;
309
+ for (let i = 0; i < spans.length; i += 1) {
310
+ if (walked + spans[i] >= target) {
311
+ const into = spans[i] === 0 ? 0 : (target - walked) / spans[i];
312
+ return {
313
+ x: points[i].x + (points[i + 1].x - points[i].x) * into,
314
+ y: points[i].y + (points[i + 1].y - points[i].y) * into
315
+ };
316
+ }
317
+ walked += spans[i];
318
+ }
319
+ return points[points.length - 1];
320
+ }
321
+ /** Where along an edge a label may sit, tried in order of how central it is. */
322
+ const LABEL_STOPS = [
323
+ .5,
324
+ .34,
325
+ .66,
326
+ .24,
327
+ .76
328
+ ];
329
+ /**
330
+ * Two edges leaving the same node reach their midpoints at the same height, so
331
+ * their labels land on top of each other. Each label slides along its own line
332
+ * until it clears the ones already placed.
333
+ */
334
+ function placeLabels(edges, fontSize) {
335
+ const size = fontSize * .85;
336
+ const height = size * 1.7;
337
+ const placed = [];
338
+ const hits = (a) => placed.some((b) => Math.abs(a.x - b.x) * 2 < a.w + b.w && Math.abs(a.y - b.y) * 2 < a.h + b.h);
339
+ for (const edge of edges) {
340
+ if (!edge.label) continue;
341
+ const width = measureText(edge.label, size) + 10;
342
+ let chosen = pointAlong(edge.points, LABEL_STOPS[0]);
343
+ for (const stop of LABEL_STOPS) {
344
+ const candidate = pointAlong(edge.points, stop);
345
+ if (!hits({
346
+ ...candidate,
347
+ w: width,
348
+ h: height
349
+ })) {
350
+ chosen = candidate;
351
+ break;
352
+ }
353
+ }
354
+ placed.push({
355
+ ...chosen,
356
+ w: width,
357
+ h: height
358
+ });
359
+ edge.labelAt = chosen;
360
+ }
361
+ }
362
+ /** Straight where the two boxes line up, a single elbow where they do not. */
363
+ function route(from, to, horizontal) {
364
+ const fromCentre = {
365
+ x: from.x + from.width / 2,
366
+ y: from.y + from.height / 2
367
+ };
368
+ const toCentre = {
369
+ x: to.x + to.width / 2,
370
+ y: to.y + to.height / 2
371
+ };
372
+ if (horizontal) {
373
+ const start = {
374
+ x: from.x + from.width,
375
+ y: fromCentre.y
376
+ };
377
+ const end = {
378
+ x: to.x,
379
+ y: toCentre.y
380
+ };
381
+ if (Math.abs(start.y - end.y) < 1 || end.x <= start.x) return [start, end];
382
+ const midX = (start.x + end.x) / 2;
383
+ return [
384
+ start,
385
+ {
386
+ x: midX,
387
+ y: start.y
388
+ },
389
+ {
390
+ x: midX,
391
+ y: end.y
392
+ },
393
+ end
394
+ ];
395
+ }
396
+ const start = {
397
+ x: fromCentre.x,
398
+ y: from.y + from.height
399
+ };
400
+ const end = {
401
+ x: toCentre.x,
402
+ y: to.y
403
+ };
404
+ if (Math.abs(start.x - end.x) < 1 || end.y <= start.y) return [start, end];
405
+ const midY = (start.y + end.y) / 2;
406
+ return [
407
+ start,
408
+ {
409
+ x: start.x,
410
+ y: midY
411
+ },
412
+ {
413
+ x: end.x,
414
+ y: midY
415
+ },
416
+ end
417
+ ];
418
+ }
419
+ function layoutDiagram(diagram, options = {}) {
420
+ const opts = {
421
+ ...DEFAULTS,
422
+ ...options
423
+ };
424
+ const horizontal = diagram.direction === "LR";
425
+ const lines = /* @__PURE__ */ new Map();
426
+ const sizes = /* @__PURE__ */ new Map();
427
+ for (const node of diagram.nodes) {
428
+ const limit = node.shape === "diamond" ? Math.max(84, opts.maxLabelWidth * .5) : opts.maxLabelWidth;
429
+ const wrapped = wrapLabel(node.label, opts.fontSize, limit);
430
+ lines.set(node.id, wrapped);
431
+ sizes.set(node.id, sizeOf(node, wrapped, opts.fontSize));
432
+ }
433
+ const rankOf = assignRanks(diagram.nodes, diagram.edges);
434
+ const depth = Math.max(...rankOf.values()) + 1;
435
+ const ranks = Array.from({ length: depth }, () => []);
436
+ for (const node of diagram.nodes) ranks[rankOf.get(node.id) ?? 0].push(node.id);
437
+ orderRanks(ranks, diagram.edges);
438
+ const rankOffset = [];
439
+ let cursor = opts.padding;
440
+ for (const rank of ranks) {
441
+ rankOffset.push(cursor);
442
+ const extent = Math.max(...rank.map((id) => {
443
+ const size = sizes.get(id);
444
+ return horizontal ? size?.width ?? 0 : size?.height ?? 0;
445
+ }));
446
+ cursor += extent + opts.rankGap;
447
+ }
448
+ const cross = /* @__PURE__ */ new Map();
449
+ const rankSpan = [];
450
+ ranks.forEach((rank, i) => {
451
+ let offset = 0;
452
+ for (const id of rank) {
453
+ const size = sizes.get(id);
454
+ const extent = horizontal ? size?.height ?? 0 : size?.width ?? 0;
455
+ cross.set(id, offset + extent / 2);
456
+ offset += extent + opts.nodeGap;
457
+ }
458
+ rankSpan[i] = Math.max(0, offset - opts.nodeGap);
459
+ });
460
+ const widest = Math.max(...rankSpan, 0);
461
+ ranks.forEach((rank, i) => {
462
+ const shift = (widest - rankSpan[i]) / 2 + opts.padding;
463
+ for (const id of rank) cross.set(id, (cross.get(id) ?? 0) + shift);
464
+ });
465
+ const nodes = diagram.nodes.map((node) => {
466
+ const size = sizes.get(node.id) ?? {
467
+ width: MIN_WIDTH,
468
+ height: 40
469
+ };
470
+ const along = rankOffset[rankOf.get(node.id) ?? 0];
471
+ const crossCentre = cross.get(node.id) ?? 0;
472
+ return {
473
+ ...node,
474
+ lines: lines.get(node.id) ?? [node.label],
475
+ width: size.width,
476
+ height: size.height,
477
+ x: horizontal ? along : crossCentre - size.width / 2,
478
+ y: horizontal ? crossCentre - size.height / 2 : along
479
+ };
480
+ });
481
+ const byId = new Map(nodes.map((n) => [n.id, n]));
482
+ const edges = [];
483
+ for (const edge of diagram.edges) {
484
+ const from = byId.get(edge.from);
485
+ const to = byId.get(edge.to);
486
+ if (!from || !to) continue;
487
+ const points = route(from, to, horizontal);
488
+ edges.push({
489
+ ...edge,
490
+ points,
491
+ labelAt: null
492
+ });
493
+ }
494
+ placeLabels(edges, opts.fontSize);
495
+ const width = Math.max(...nodes.map((n) => n.x + n.width)) + opts.padding;
496
+ const height = Math.max(...nodes.map((n) => n.y + n.height)) + opts.padding;
497
+ return {
498
+ direction: diagram.direction,
499
+ nodes,
500
+ edges,
501
+ width,
502
+ height
503
+ };
504
+ }
505
+ //#endregion
506
+ //#region src/diagram/parse.ts
507
+ var DiagramSyntaxError = class extends Error {
508
+ line;
509
+ constructor(message, line) {
510
+ super(`line ${line}: ${message}`);
511
+ this.name = "DiagramSyntaxError";
512
+ this.line = line;
513
+ }
514
+ };
515
+ const HEADER_RE = /^(?:flowchart|graph)(?:\s+(TB|TD|BT|LR|RL))?$/i;
516
+ /** `A[Label]`, `A(Label)`, `A([Label])`, `A{Label}`, `A((Label))`, or a bare `A`. */
517
+ const NODE_RE = /^([A-Za-z0-9_.-]+)(?:(\(\(|\(\[|\[|\(|\{)([\s\S]*?)(\)\)|\]\)|\]|\)|\}))?$/;
518
+ const SHAPES = {
519
+ "[": "rect",
520
+ "(": "round",
521
+ "([": "stadium",
522
+ "{": "diamond",
523
+ "((": "circle"
524
+ };
525
+ /**
526
+ * Links, longest first: `-.->` must be tried before `-->` or the dash prefix
527
+ * would match and leave a stray dot in the label.
528
+ */
529
+ const LINKS = [
530
+ {
531
+ re: /-\.->/,
532
+ style: "dashed",
533
+ arrow: true
534
+ },
535
+ {
536
+ re: /-\.-/,
537
+ style: "dashed",
538
+ arrow: false
539
+ },
540
+ {
541
+ re: /==>/,
542
+ style: "thick",
543
+ arrow: true
544
+ },
545
+ {
546
+ re: /===/,
547
+ style: "thick",
548
+ arrow: false
549
+ },
550
+ {
551
+ re: /-->/,
552
+ style: "solid",
553
+ arrow: true
554
+ },
555
+ {
556
+ re: /---/,
557
+ style: "solid",
558
+ arrow: false
559
+ }
560
+ ];
561
+ function normalizeDirection(raw) {
562
+ const value = (raw ?? "TD").toUpperCase();
563
+ return value === "LR" || value === "RL" ? "LR" : "TD";
564
+ }
565
+ function stripComment(line) {
566
+ const at = line.indexOf("%%");
567
+ return at === -1 ? line : line.slice(0, at);
568
+ }
569
+ /** Splits `A --> B --> C` into consecutive pairs, keeping each link's own style. */
570
+ function splitChain(text) {
571
+ const parts = [];
572
+ let rest = text;
573
+ for (;;) {
574
+ let best = null;
575
+ for (const link of LINKS) {
576
+ const match = link.re.exec(rest);
577
+ if (!match) continue;
578
+ if (!best || match.index < best.index) best = {
579
+ index: match.index,
580
+ link,
581
+ length: match[0].length
582
+ };
583
+ }
584
+ if (!best) {
585
+ parts.push({
586
+ raw: rest,
587
+ label: null
588
+ });
589
+ return parts;
590
+ }
591
+ const head = rest.slice(0, best.index);
592
+ let tail = rest.slice(best.index + best.length);
593
+ let label = null;
594
+ const labelled = /^\s*\|([\s\S]*?)\|/.exec(tail);
595
+ if (labelled) {
596
+ label = labelled[1].trim();
597
+ tail = tail.slice(labelled[0].length);
598
+ }
599
+ parts.push({
600
+ raw: head,
601
+ link: best.link,
602
+ label
603
+ });
604
+ rest = tail;
605
+ }
606
+ }
607
+ function parseNode(raw, line) {
608
+ const text = raw.trim();
609
+ if (!text) throw new DiagramSyntaxError("empty node", line);
610
+ const match = NODE_RE.exec(text);
611
+ if (!match) throw new DiagramSyntaxError(`cannot read node "${text}"`, line);
612
+ const [, id, open, label] = match;
613
+ if (!open) return {
614
+ id,
615
+ label: id,
616
+ shape: "rect"
617
+ };
618
+ const shape = SHAPES[open];
619
+ if (!shape) throw new DiagramSyntaxError(`unknown node shape "${open}"`, line);
620
+ return {
621
+ id,
622
+ label: unquote(label ?? ""),
623
+ shape
624
+ };
625
+ }
626
+ function unquote(label) {
627
+ const trimmed = label.trim();
628
+ if (trimmed.length > 1 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) return trimmed.slice(1, -1);
629
+ return trimmed;
630
+ }
631
+ /**
632
+ * A practical subset of Mermaid's flowchart syntax — the shapes and links a
633
+ * report actually uses. Agents already write this dialect fluently, which is
634
+ * the whole reason to borrow it rather than invent one; the renderer is ours,
635
+ * so the output follows the document's theme instead of Mermaid's.
636
+ */
637
+ function parseDiagram(source) {
638
+ const lines = source.split("\n");
639
+ let direction = "TD";
640
+ let sawHeader = false;
641
+ const nodes = /* @__PURE__ */ new Map();
642
+ const edges = [];
643
+ const remember = (node) => {
644
+ const existing = nodes.get(node.id);
645
+ if (!existing || existing.label === existing.id && node.label !== node.id) nodes.set(node.id, node);
646
+ return node.id;
647
+ };
648
+ lines.forEach((rawLine, index) => {
649
+ const lineNumber = index + 1;
650
+ const line = stripComment(rawLine).trim();
651
+ if (!line) return;
652
+ if (!sawHeader) {
653
+ const header = HEADER_RE.exec(line);
654
+ if (header) {
655
+ direction = normalizeDirection(header[1]);
656
+ sawHeader = true;
657
+ return;
658
+ }
659
+ }
660
+ const chain = splitChain(line);
661
+ if (chain.length === 1) {
662
+ remember(parseNode(chain[0].raw, lineNumber));
663
+ return;
664
+ }
665
+ for (let i = 0; i < chain.length - 1; i += 1) {
666
+ const left = chain[i];
667
+ const right = chain[i + 1];
668
+ const from = remember(parseNode(left.raw, lineNumber));
669
+ const to = remember(parseNode(right.raw, lineNumber));
670
+ const link = left.link;
671
+ if (!link) throw new DiagramSyntaxError("malformed link", lineNumber);
672
+ edges.push({
673
+ from,
674
+ to,
675
+ label: left.label,
676
+ style: link.style,
677
+ arrow: link.arrow
678
+ });
679
+ }
680
+ });
681
+ if (nodes.size === 0) throw new DiagramSyntaxError("diagram has no nodes", 1);
682
+ return {
683
+ direction,
684
+ nodes: [...nodes.values()],
685
+ edges
686
+ };
687
+ }
688
+ //#endregion
689
+ //#region src/diagram/render.ts
690
+ const LINE_HEIGHT = 1.35;
691
+ function escapeXml(text) {
692
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
693
+ }
694
+ /**
695
+ * Every colour and face is a `--od-*` variable rather than a literal, which is
696
+ * the entire reason this renderer exists: the drawing inherits the document's
697
+ * theme, in the viewer and in the PDF, instead of arriving with a palette of
698
+ * its own.
699
+ */
700
+ function shapePath(node) {
701
+ const { x, y, width: w, height: h } = node;
702
+ const common = "fill=\"var(--od-bg)\" stroke=\"var(--od-text)\" stroke-width=\"1.25\"";
703
+ switch (node.shape) {
704
+ case "diamond": return `<polygon points="${[
705
+ `${x + w / 2},${y}`,
706
+ `${x + w},${y + h / 2}`,
707
+ `${x + w / 2},${y + h}`,
708
+ `${x},${y + h / 2}`
709
+ ].join(" ")}" ${common} />`;
710
+ case "circle": return `<ellipse cx="${x + w / 2}" cy="${y + h / 2}" rx="${w / 2}" ry="${h / 2}" ${common} />`;
711
+ case "stadium": return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${h / 2}" ry="${h / 2}" ${common} />`;
712
+ case "round": return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="10" ry="10" ${common} />`;
713
+ default: return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="var(--od-radius, 2)" ${common} />`;
714
+ }
715
+ }
716
+ function nodeText(node, fontSize) {
717
+ const firstBaseline = node.y + node.height / 2 - node.lines.length * fontSize * LINE_HEIGHT / 2 + fontSize * .98;
718
+ return node.lines.map((line, i) => {
719
+ const y = firstBaseline + i * fontSize * LINE_HEIGHT;
720
+ return `<text x="${node.x + node.width / 2}" y="${y}" text-anchor="middle" font-family="var(--od-font-body)" font-size="${fontSize}" fill="var(--od-text)">${escapeXml(line)}</text>`;
721
+ }).join("");
722
+ }
723
+ function edgePath(edge, marker) {
724
+ const d = edge.points.map((point, i) => `${i === 0 ? "M" : "L"}${round(point.x)},${round(point.y)}`).join(" ");
725
+ const dash = edge.style === "dashed" ? " stroke-dasharray=\"5 4\"" : "";
726
+ return `<path d="${d}" fill="none" stroke="var(--od-text)" stroke-width="${edge.style === "thick" ? 2.25 : 1.25}"${dash} stroke-linejoin="round"${edge.arrow ? ` marker-end="url(#${marker})"` : ""} />`;
727
+ }
728
+ function edgeLabel(edge, fontSize) {
729
+ if (!edge.labelAt || !edge.label) return "";
730
+ const size = fontSize * .85;
731
+ const width = measureText(edge.label, size) + 10;
732
+ return `<rect x="${round(edge.labelAt.x - width / 2)}" y="${round(edge.labelAt.y - size * .8)}" width="${round(width)}" height="${round(size * 1.6)}" fill="var(--od-bg)" /><text x="${round(edge.labelAt.x)}" y="${round(edge.labelAt.y + size * .34)}" text-anchor="middle" font-family="var(--od-font-body)" font-size="${size}" fill="var(--od-muted)">${escapeXml(edge.label)}</text>`;
733
+ }
734
+ function round(value) {
735
+ return Math.round(value * 100) / 100;
736
+ }
737
+ function renderDiagram(diagram, options = {}) {
738
+ const fontSize = options.fontSize ?? 13;
739
+ const marker = `od-arrow${options.idSuffix ? `-${options.idSuffix}` : ""}`;
740
+ const defs = `<defs><marker id="${marker}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,1 L10,5 L0,9 z" fill="var(--od-text)" /></marker></defs>`;
741
+ const body = [
742
+ ...diagram.edges.map((edge) => edgePath(edge, marker)),
743
+ ...diagram.nodes.map((node) => shapePath(node)),
744
+ ...diagram.nodes.map((node) => nodeText(node, fontSize)),
745
+ ...diagram.edges.map((edge) => edgeLabel(edge, fontSize))
746
+ ].join("");
747
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${round(diagram.width)} ${round(diagram.height)}" width="${round(diagram.width)}" height="${round(diagram.height)}" role="img">${defs}${body}</svg>`;
748
+ }
749
+ //#endregion
750
+ //#region src/diagram/index.ts
751
+ /** Mermaid-flavoured text in, themed SVG out. One call, no browser. */
752
+ function compileDiagram(source, options = {}) {
753
+ const parsed = parseDiagram(source);
754
+ const laidOut = layoutDiagram(parsed, options);
755
+ return {
756
+ svg: renderDiagram(laidOut, options),
757
+ width: laidOut.width,
758
+ height: laidOut.height,
759
+ source: parsed
760
+ };
761
+ }
762
+ //#endregion
763
+ export { defaultDesign as a, cssVarsToString as i, DiagramSyntaxError as n, designToCssVars as o, parseDelimited as r, compileDiagram as t };