paseo-plugin-genui 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 (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +407 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +122 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * A parser for the Mermaid flowchart subset that actually shows up in agent
3
+ * output and documentation. Pure and dependency-free: Mermaid itself needs a DOM
4
+ * and cannot run in a Paseo client bundle, and the transformer must stay
5
+ * synchronous, so the grammar is walked by hand.
6
+ *
7
+ * Anything it fails to recognise is reported rather than guessed at, and the
8
+ * renderer falls back to showing the source.
9
+ */
10
+
11
+ export type Direction = "TD" | "TB" | "BT" | "LR" | "RL";
12
+ export type NodeShape = "rect" | "round" | "stadium" | "subroutine" | "diamond" | "circle" | "asymmetric";
13
+ export type EdgeStyle = "solid" | "dotted" | "thick";
14
+
15
+ export interface FlowNode {
16
+ id: string;
17
+ label: string;
18
+ shape: NodeShape;
19
+ }
20
+
21
+ export interface FlowEdge {
22
+ from: string;
23
+ to: string;
24
+ label: string | null;
25
+ style: EdgeStyle;
26
+ arrow: boolean;
27
+ }
28
+
29
+ export interface Flowchart {
30
+ direction: Direction;
31
+ nodes: FlowNode[];
32
+ edges: FlowEdge[];
33
+ /** Lines the parser chose not to interpret, so the UI can say so. */
34
+ skipped: string[];
35
+ }
36
+
37
+ const HEADER = /^\s*(?:flowchart|graph)(?:\s+(TD|TB|BT|LR|RL))?\s*$/i;
38
+
39
+ /** Longest first: `-.->` must win over `-.-`. */
40
+ const OPERATORS: { token: string; style: EdgeStyle; arrow: boolean }[] = [
41
+ { token: "-.->", style: "dotted", arrow: true },
42
+ { token: "==>", style: "thick", arrow: true },
43
+ { token: "-->", style: "solid", arrow: true },
44
+ { token: "-.-", style: "dotted", arrow: false },
45
+ { token: "===", style: "thick", arrow: false },
46
+ { token: "---", style: "solid", arrow: false },
47
+ ];
48
+
49
+ const OPENERS: { open: string; close: string; shape: NodeShape }[] = [
50
+ { open: "([", close: "])", shape: "stadium" },
51
+ { open: "[[", close: "]]", shape: "subroutine" },
52
+ { open: "((", close: "))", shape: "circle" },
53
+ { open: "{{", close: "}}", shape: "diamond" },
54
+ { open: "[", close: "]", shape: "rect" },
55
+ { open: "(", close: ")", shape: "round" },
56
+ { open: "{", close: "}", shape: "diamond" },
57
+ { open: ">", close: "]", shape: "asymmetric" },
58
+ ];
59
+
60
+ /**
61
+ * Mermaid writes an edge label two ways. Rewriting `A -- yes --> B` into
62
+ * `A -->|yes| B` lets the splitter handle one form.
63
+ */
64
+ export function normalizeEdgeLabels(line: string): string {
65
+ return line
66
+ .replace(/--\s+([^->|]+?)\s+-->/g, "-->|$1|")
67
+ .replace(/-\.\s+([^.|>]+?)\s+\.->/g, "-.->|$1|")
68
+ .replace(/==\s+([^=|>]+?)\s+==>/g, "==>|$1|");
69
+ }
70
+
71
+ function stripQuotes(value: string): string {
72
+ const trimmed = value.trim();
73
+ const quoted = /^"(.*)"$/s.exec(trimmed) ?? /^'(.*)'$/s.exec(trimmed);
74
+ return (quoted ? quoted[1] : trimmed).replace(/<br\s*\/?>/gi, "\n").trim();
75
+ }
76
+
77
+ /** Reads `id`, `id[Label]`, `id{Decision}`, `id((Circle))` and friends. */
78
+ export function parseNodeExpression(raw: string): FlowNode | null {
79
+ const text = raw.trim();
80
+ if (text.length === 0) return null;
81
+
82
+ for (const { open, close, shape } of OPENERS) {
83
+ const start = text.indexOf(open);
84
+ if (start <= 0) continue;
85
+ if (!text.endsWith(close)) continue;
86
+ const id = text.slice(0, start).trim();
87
+ if (!/^[\w.-]+$/.test(id)) continue;
88
+ const label = stripQuotes(text.slice(start + open.length, text.length - close.length));
89
+ return { id, label: label.length > 0 ? label : id, shape };
90
+ }
91
+
92
+ if (!/^[\w.-]+$/.test(text)) return null;
93
+ return { id: text, label: text, shape: "rect" };
94
+ }
95
+
96
+ interface Split {
97
+ parts: string[];
98
+ operators: { style: EdgeStyle; arrow: boolean; label: string | null }[];
99
+ }
100
+
101
+ /** Splits `A -->|yes| B --> C` into its nodes and the operators between them. */
102
+ export function splitChain(line: string): Split | null {
103
+ const parts: string[] = [];
104
+ const operators: Split["operators"] = [];
105
+ let rest = line;
106
+
107
+ for (;;) {
108
+ let best: { index: number; op: (typeof OPERATORS)[number] } | null = null;
109
+ for (const op of OPERATORS) {
110
+ const index = rest.indexOf(op.token);
111
+ if (index < 0) continue;
112
+ if (!best || index < best.index || (index === best.index && op.token.length > best.op.token.length)) {
113
+ best = { index, op };
114
+ }
115
+ }
116
+ if (!best) break;
117
+
118
+ parts.push(rest.slice(0, best.index));
119
+ let after = rest.slice(best.index + best.op.token.length);
120
+
121
+ let label: string | null = null;
122
+ const labelled = /^\s*\|([^|]*)\|/.exec(after);
123
+ if (labelled) {
124
+ label = stripQuotes(labelled[1]);
125
+ after = after.slice(labelled[0].length);
126
+ }
127
+
128
+ operators.push({ style: best.op.style, arrow: best.op.arrow, label });
129
+ rest = after;
130
+ }
131
+
132
+ if (operators.length === 0) return null;
133
+ parts.push(rest);
134
+ return { parts, operators };
135
+ }
136
+
137
+ export function parseFlowchart(source: string): Flowchart | null {
138
+ const lines = source.split("\n");
139
+ let direction: Direction | null = null;
140
+ const nodes = new Map<string, FlowNode>();
141
+ const edges: FlowEdge[] = [];
142
+ const skipped: string[] = [];
143
+
144
+ const remember = (node: FlowNode) => {
145
+ const existing = nodes.get(node.id);
146
+ // A later definition carrying a real label wins over a bare id mention.
147
+ if (!existing || (existing.label === existing.id && node.label !== node.id)) {
148
+ nodes.set(node.id, node);
149
+ }
150
+ };
151
+
152
+ for (const original of lines) {
153
+ const line = original.replace(/%%.*$/, "").trim();
154
+ if (line.length === 0) continue;
155
+
156
+ const header = HEADER.exec(line);
157
+ if (header) {
158
+ direction = (header[1]?.toUpperCase() as Direction) ?? "TD";
159
+ continue;
160
+ }
161
+ if (direction === null) {
162
+ // Not a flowchart at all; let another parser have it.
163
+ return null;
164
+ }
165
+
166
+ // Subgraphs are not laid out yet; their contents still render as nodes.
167
+ if (/^subgraph\b/i.test(line) || /^end$/i.test(line)) {
168
+ skipped.push(original.trim());
169
+ continue;
170
+ }
171
+ if (/^(?:style|classDef|class|click|linkStyle)\b/i.test(line)) {
172
+ skipped.push(original.trim());
173
+ continue;
174
+ }
175
+
176
+ const chain = splitChain(normalizeEdgeLabels(line));
177
+ if (!chain) {
178
+ const single = parseNodeExpression(line.replace(/;$/, ""));
179
+ if (single) remember(single);
180
+ else skipped.push(original.trim());
181
+ continue;
182
+ }
183
+
184
+ const parsed = chain.parts.map((part) => parseNodeExpression(part.replace(/;$/, "")));
185
+ if (parsed.some((node) => node === null)) {
186
+ skipped.push(original.trim());
187
+ continue;
188
+ }
189
+
190
+ for (const node of parsed) remember(node as FlowNode);
191
+ for (let index = 0; index < chain.operators.length; index += 1) {
192
+ const operator = chain.operators[index];
193
+ edges.push({
194
+ from: (parsed[index] as FlowNode).id,
195
+ to: (parsed[index + 1] as FlowNode).id,
196
+ label: operator.label,
197
+ style: operator.style,
198
+ arrow: operator.arrow,
199
+ });
200
+ }
201
+ }
202
+
203
+ if (direction === null) return null;
204
+ return { direction, nodes: [...nodes.values()], edges, skipped };
205
+ }
@@ -0,0 +1,440 @@
1
+ import type { Direction, Flowchart, FlowEdge, FlowNode } from "./flowchart";
2
+
3
+ /**
4
+ * Layered layout for a flowchart, computed here rather than in the renderer so
5
+ * the geometry can be tested without mounting anything. React Native has no SVG,
6
+ * so the renderer places plain Views at these coordinates and draws edges as
7
+ * orthogonal segments.
8
+ */
9
+
10
+ export interface LaidOutNode extends FlowNode {
11
+ layer: number;
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ export interface Segment {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ export interface LaidOutEdge extends FlowEdge {
26
+ segments: Segment[];
27
+ /** Where the arrowhead sits and which way it points. */
28
+ arrowAt: { x: number; y: number; direction: "up" | "down" | "left" | "right" } | null;
29
+ labelAt: { x: number; y: number } | null;
30
+ }
31
+
32
+ export interface FlowLayout {
33
+ direction: Direction;
34
+ nodes: LaidOutNode[];
35
+ edges: LaidOutEdge[];
36
+ width: number;
37
+ height: number;
38
+ }
39
+
40
+ const CHAR_WIDTH = 7.1;
41
+ const LINE_HEIGHT = 17;
42
+ const PADDING_X = 14;
43
+ const PADDING_Y = 10;
44
+ const MIN_WIDTH = 56;
45
+ const LAYER_GAP = 56;
46
+ const NODE_GAP = 22;
47
+ const LINE = 1.5;
48
+ /** Breadth reserved for each lane an edge that skips a layer runs through. */
49
+ const LANE_GAP = 26;
50
+
51
+ export function measureNode(node: FlowNode): { width: number; height: number } {
52
+ const lines = node.label.split("\n");
53
+ const longest = Math.max(...lines.map((line) => line.length), 1);
54
+ const width = Math.max(MIN_WIDTH, Math.round(longest * CHAR_WIDTH) + PADDING_X * 2);
55
+ const height = lines.length * LINE_HEIGHT + PADDING_Y * 2;
56
+ // A diamond is a square rotated 45 degrees, so its box has to be square too:
57
+ // the rendered corners then touch the box edges exactly, and an edge routed to
58
+ // the box lands on the shape instead of stopping short of it.
59
+ if (node.shape === "diamond") {
60
+ const side = Math.round(Math.max(width, height) * Math.SQRT2 * 0.78);
61
+ return { width: side, height: side };
62
+ }
63
+ if (node.shape === "circle") {
64
+ const side = Math.max(width, height);
65
+ return { width: side, height: side };
66
+ }
67
+ return { width, height };
68
+ }
69
+
70
+ /**
71
+ * Longest-path layering. Back edges are found first and left out of the ranking,
72
+ * so a cycle produces a readable graph instead of an infinite descent.
73
+ */
74
+ export function assignLayers(nodes: FlowNode[], edges: FlowEdge[]): Map<string, number> {
75
+ const ids = new Set(nodes.map((node) => node.id));
76
+ const outgoing = new Map<string, string[]>();
77
+ for (const edge of edges) {
78
+ if (!ids.has(edge.from) || !ids.has(edge.to)) continue;
79
+ outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to]);
80
+ }
81
+
82
+ const back = new Set<FlowEdge>();
83
+ const state = new Map<string, "open" | "done">();
84
+ const walk = (id: string) => {
85
+ state.set(id, "open");
86
+ for (const next of outgoing.get(id) ?? []) {
87
+ const seen = state.get(next);
88
+ if (seen === "open") {
89
+ for (const edge of edges) if (edge.from === id && edge.to === next) back.add(edge);
90
+ continue;
91
+ }
92
+ if (seen === undefined) walk(next);
93
+ }
94
+ state.set(id, "done");
95
+ };
96
+ for (const node of nodes) if (!state.has(node.id)) walk(node.id);
97
+
98
+ const forward = edges.filter((edge) => !back.has(edge) && ids.has(edge.from) && ids.has(edge.to));
99
+ const layer = new Map<string, number>(nodes.map((node) => [node.id, 0]));
100
+
101
+ // Relax until stable; the graph is a DAG now, so this settles in at most |V| rounds.
102
+ for (let round = 0; round < nodes.length; round += 1) {
103
+ let moved = false;
104
+ for (const edge of forward) {
105
+ const candidate = (layer.get(edge.from) ?? 0) + 1;
106
+ if (candidate > (layer.get(edge.to) ?? 0)) {
107
+ layer.set(edge.to, candidate);
108
+ moved = true;
109
+ }
110
+ }
111
+ if (!moved) break;
112
+ }
113
+ return layer;
114
+ }
115
+
116
+ /** One barycentre pass: each node drifts toward the average position of its parents. */
117
+ export function orderLayers(
118
+ nodes: FlowNode[],
119
+ edges: FlowEdge[],
120
+ layers: Map<string, number>,
121
+ ): string[][] {
122
+ const depth = Math.max(...[...layers.values()], 0) + 1;
123
+ const rows: string[][] = Array.from({ length: depth }, () => []);
124
+ for (const node of nodes) rows[layers.get(node.id) ?? 0].push(node.id);
125
+
126
+ for (let index = 1; index < rows.length; index += 1) {
127
+ const previous = new Map(rows[index - 1].map((id, position) => [id, position]));
128
+ const score = new Map<string, number>();
129
+ for (const id of rows[index]) {
130
+ const parents = edges
131
+ .filter((edge) => edge.to === id && previous.has(edge.from))
132
+ .map((edge) => previous.get(edge.from) as number);
133
+ score.set(id, parents.length > 0 ? parents.reduce((a, b) => a + b, 0) / parents.length : Number.MAX_SAFE_INTEGER);
134
+ }
135
+ const original = new Map(rows[index].map((id, position) => [id, position]));
136
+ rows[index] = [...rows[index]].sort(
137
+ (left, right) =>
138
+ (score.get(left) as number) - (score.get(right) as number) ||
139
+ (original.get(left) as number) - (original.get(right) as number),
140
+ );
141
+ }
142
+ return rows;
143
+ }
144
+
145
+ const horizontal = (direction: Direction) => direction === "LR" || direction === "RL";
146
+
147
+ export function layoutFlowchart(chart: Flowchart): FlowLayout {
148
+ const layers = assignLayers(chart.nodes, chart.edges);
149
+ const rows = orderLayers(chart.nodes, chart.edges, layers);
150
+ const sizes = new Map(chart.nodes.map((node) => [node.id, measureNode(node)]));
151
+ const byId = new Map(chart.nodes.map((node) => [node.id, node]));
152
+ const across = horizontal(chart.direction);
153
+
154
+ // "Depth" runs along the flow, "breadth" across it; transposing at the end is
155
+ // what makes LR fall out of the same maths as TD.
156
+ const depthOffsets: number[] = [];
157
+ let depthCursor = 0;
158
+ const rowBreadth = rows.map((row) => {
159
+ const total = row.reduce((sum, id) => {
160
+ const size = sizes.get(id) as { width: number; height: number };
161
+ return sum + (across ? size.height : size.width);
162
+ }, 0);
163
+ return total + Math.max(0, row.length - 1) * NODE_GAP;
164
+ });
165
+ const widestRow = Math.max(...rowBreadth, 0);
166
+
167
+ for (const row of rows) {
168
+ depthOffsets.push(depthCursor);
169
+ const deepest = Math.max(
170
+ ...row.map((id) => {
171
+ const size = sizes.get(id) as { width: number; height: number };
172
+ return across ? size.width : size.height;
173
+ }),
174
+ 0,
175
+ );
176
+ depthCursor += deepest + LAYER_GAP;
177
+ }
178
+ const totalDepth = Math.max(0, depthCursor - LAYER_GAP);
179
+
180
+ const placed = new Map<string, LaidOutNode>();
181
+ rows.forEach((row, rowIndex) => {
182
+ let breadthCursor = (widestRow - rowBreadth[rowIndex]) / 2;
183
+ for (const id of row) {
184
+ const size = sizes.get(id) as { width: number; height: number };
185
+ const node = byId.get(id) as FlowNode;
186
+ const breadthSpan = across ? size.height : size.width;
187
+ const depthStart = depthOffsets[rowIndex];
188
+
189
+ placed.set(id, {
190
+ ...node,
191
+ layer: rowIndex,
192
+ width: size.width,
193
+ height: size.height,
194
+ x: across ? depthStart : breadthCursor,
195
+ y: across ? breadthCursor : depthStart,
196
+ });
197
+ breadthCursor += breadthSpan + NODE_GAP;
198
+ }
199
+ });
200
+
201
+ const flip = chart.direction === "BT" || chart.direction === "RL";
202
+ const nodes = [...placed.values()].map((node) =>
203
+ flip
204
+ ? across
205
+ ? { ...node, x: totalDepth - node.x - node.width }
206
+ : { ...node, y: totalDepth - node.y - node.height }
207
+ : node,
208
+ );
209
+ const finals = new Map(nodes.map((node) => [node.id, node]));
210
+
211
+ // An edge that skips a layer cannot be routed between its endpoints: the
212
+ // straight path crosses whatever sits in the layers between them. Those edges
213
+ // get a lane outside the node band instead, one per edge so they do not stack.
214
+ const spans = (edge: FlowEdge): boolean => {
215
+ const from = finals.get(edge.from);
216
+ const to = finals.get(edge.to);
217
+ return !!from && !!to && Math.abs(to.layer - from.layer) > 1;
218
+ };
219
+ const longEdges = chart.edges.filter(spans);
220
+ const laneBand = longEdges.length * LANE_GAP + (longEdges.length > 0 ? LANE_GAP : 0);
221
+
222
+ const contentBreadth = across ? widestRow : totalDepth;
223
+ const lanes = new Map(longEdges.map((edge, index) => [edge, contentBreadth + LANE_GAP * (index + 1)]));
224
+
225
+ const edges = chart.edges.map((edge) =>
226
+ route(edge, finals.get(edge.from), finals.get(edge.to), across, lanes.get(edge)),
227
+ );
228
+
229
+ return {
230
+ direction: chart.direction,
231
+ nodes,
232
+ edges,
233
+ width: across ? totalDepth : widestRow + laneBand,
234
+ height: across ? widestRow + laneBand : totalDepth,
235
+ };
236
+ }
237
+
238
+ /** Three orthogonal segments between neighbours; five through a lane when not. */
239
+ function route(
240
+ edge: FlowEdge,
241
+ from: LaidOutNode | undefined,
242
+ to: LaidOutNode | undefined,
243
+ across: boolean,
244
+ lane?: number,
245
+ ): LaidOutEdge {
246
+ if (!from || !to) return { ...edge, segments: [], arrowAt: null, labelAt: null };
247
+ if (lane !== undefined) return routeThroughLane(edge, from, to, across, lane);
248
+
249
+ if (across) {
250
+ const forward = to.x >= from.x;
251
+ const startX = forward ? from.x + from.width : from.x;
252
+ const endX = forward ? to.x : to.x + to.width;
253
+ const startY = from.y + from.height / 2;
254
+ const endY = to.y + to.height / 2;
255
+ const midX = (startX + endX) / 2;
256
+
257
+ return {
258
+ ...edge,
259
+ segments: [
260
+ span(Math.min(startX, midX), startY, Math.abs(midX - startX), LINE),
261
+ span(midX - LINE / 2, Math.min(startY, endY), LINE, Math.abs(endY - startY)),
262
+ span(Math.min(midX, endX), endY, Math.abs(endX - midX), LINE),
263
+ ],
264
+ arrowAt: edge.arrow ? { x: endX, y: endY, direction: forward ? "right" : "left" } : null,
265
+ labelAt: { x: midX, y: (startY + endY) / 2 },
266
+ };
267
+ }
268
+
269
+ const forward = to.y >= from.y;
270
+ const startY = forward ? from.y + from.height : from.y;
271
+ const endY = forward ? to.y : to.y + to.height;
272
+ const startX = from.x + from.width / 2;
273
+ const endX = to.x + to.width / 2;
274
+ const midY = (startY + endY) / 2;
275
+
276
+ return {
277
+ ...edge,
278
+ segments: [
279
+ span(startX - LINE / 2, Math.min(startY, midY), LINE, Math.abs(midY - startY)),
280
+ span(Math.min(startX, endX), midY, Math.abs(endX - startX), LINE),
281
+ span(endX - LINE / 2, Math.min(midY, endY), LINE, Math.abs(endY - midY)),
282
+ ],
283
+ arrowAt: edge.arrow ? { x: endX, y: endY, direction: forward ? "down" : "up" } : null,
284
+ labelAt: { x: (startX + endX) / 2, y: midY },
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Out of the source, down to a lane clear of every node, across, and back up
290
+ * into the target. Longer than a direct route, and the only one that is honest
291
+ * about not passing through the boxes in between.
292
+ */
293
+ function routeThroughLane(
294
+ edge: FlowEdge,
295
+ from: LaidOutNode,
296
+ to: LaidOutNode,
297
+ across: boolean,
298
+ lane: number,
299
+ ): LaidOutEdge {
300
+ if (across) {
301
+ const startX = from.x + from.width;
302
+ const endX = to.x;
303
+ const startY = from.y + from.height / 2;
304
+ const endY = to.y + to.height / 2;
305
+ const outX = startX + LANE_GAP / 2;
306
+ const inX = endX - LANE_GAP / 2;
307
+
308
+ return {
309
+ ...edge,
310
+ segments: [
311
+ span(startX, startY, outX - startX, LINE),
312
+ span(outX - LINE / 2, Math.min(startY, lane), LINE, Math.abs(lane - startY)),
313
+ span(Math.min(outX, inX), lane, Math.abs(inX - outX), LINE),
314
+ span(inX - LINE / 2, Math.min(endY, lane), LINE, Math.abs(lane - endY)),
315
+ span(inX, endY, endX - inX, LINE),
316
+ ],
317
+ arrowAt: edge.arrow ? { x: endX, y: endY, direction: "right" } : null,
318
+ labelAt: { x: (outX + inX) / 2, y: lane },
319
+ };
320
+ }
321
+
322
+ const startY = from.y + from.height;
323
+ const endY = to.y;
324
+ const startX = from.x + from.width / 2;
325
+ const endX = to.x + to.width / 2;
326
+ const outY = startY + LANE_GAP / 2;
327
+ const inY = endY - LANE_GAP / 2;
328
+
329
+ return {
330
+ ...edge,
331
+ segments: [
332
+ span(startX - LINE / 2, startY, LINE, outY - startY),
333
+ span(Math.min(startX, lane), outY, Math.abs(lane - startX), LINE),
334
+ span(lane - LINE / 2, Math.min(outY, inY), LINE, Math.abs(inY - outY)),
335
+ span(Math.min(lane, endX), inY, Math.abs(endX - lane), LINE),
336
+ span(endX - LINE / 2, inY, LINE, endY - inY),
337
+ ],
338
+ arrowAt: edge.arrow ? { x: endX, y: endY, direction: "down" } : null,
339
+ labelAt: { x: lane, y: (outY + inY) / 2 },
340
+ };
341
+ }
342
+
343
+ const span = (x: number, y: number, width: number, height: number): Segment => ({
344
+ x,
345
+ y,
346
+ width: Math.max(width, LINE),
347
+ height: Math.max(height, LINE),
348
+ });
349
+
350
+ /* -------------------------------------------------------------------------- */
351
+ /* Sequence diagrams */
352
+ /* -------------------------------------------------------------------------- */
353
+
354
+ export interface SequenceColumn {
355
+ id: string;
356
+ label: string;
357
+ actor: boolean;
358
+ centerX: number;
359
+ width: number;
360
+ }
361
+
362
+ export interface SequenceRow {
363
+ y: number;
364
+ height: number;
365
+ }
366
+
367
+ export interface SequenceLayout {
368
+ columns: SequenceColumn[];
369
+ rows: SequenceRow[];
370
+ headerHeight: number;
371
+ width: number;
372
+ height: number;
373
+ }
374
+
375
+ const SEQ_ROW = 44;
376
+ const SEQ_HEADER = 40;
377
+ const SEQ_GAP = 40;
378
+ const SEQ_MIN_COLUMN = 96;
379
+
380
+ /**
381
+ * Columns are sized to their own label, not to a shared width: one participant
382
+ * called "Payment gateway" should not push every other column that wide.
383
+ */
384
+ export function layoutSequence(
385
+ participants: { id: string; label: string; actor: boolean }[],
386
+ eventCount: number,
387
+ ): SequenceLayout {
388
+ const columns: SequenceColumn[] = [];
389
+ let cursor = 0;
390
+
391
+ for (const participant of participants) {
392
+ const width = Math.max(SEQ_MIN_COLUMN, Math.round(participant.label.length * CHAR_WIDTH) + PADDING_X * 2);
393
+ columns.push({ ...participant, width, centerX: cursor + width / 2 });
394
+ cursor += width + SEQ_GAP;
395
+ }
396
+
397
+ const rows = Array.from({ length: eventCount }, (_, index) => ({
398
+ y: SEQ_HEADER + index * SEQ_ROW,
399
+ height: SEQ_ROW,
400
+ }));
401
+
402
+ return {
403
+ columns,
404
+ rows,
405
+ headerHeight: SEQ_HEADER,
406
+ width: Math.max(0, cursor - SEQ_GAP),
407
+ height: SEQ_HEADER + eventCount * SEQ_ROW + 12,
408
+ };
409
+ }
410
+
411
+ /* -------------------------------------------------------------------------- */
412
+ /* Fitting a drawing to the space it gets */
413
+ /* -------------------------------------------------------------------------- */
414
+
415
+ /** Below this the labels stop being readable; scroll instead of shrinking further. */
416
+ export const MIN_SCALE = 0.55;
417
+ /** Above this a three-node diagram would look like a poster. */
418
+ export const MAX_SCALE = 1.3;
419
+
420
+ /**
421
+ * How much to scale a drawing for the width it was actually given.
422
+ *
423
+ * A diagram has no business being rendered at whatever size its node labels
424
+ * happened to add up to. Wide ones shrink to fit, small ones grow a little so
425
+ * they do not sit lost in a wide row, and the shrinking stops where the text
426
+ * stops being readable — past that the caller scrolls instead.
427
+ */
428
+ export function fitScale(available: number, natural: number): number {
429
+ if (!(available > 0) || !(natural > 0)) return 1;
430
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, available / natural));
431
+ }
432
+
433
+ /**
434
+ * True when the drawing still does not fit after shrinking as far as it may,
435
+ * or when the reader has zoomed a fitted drawing past the row.
436
+ */
437
+ export function overflowsAfterFit(available: number, natural: number, zoom = 1): boolean {
438
+ if (!(available > 0) || !(natural > 0)) return false;
439
+ return natural * fitScale(available, natural) * zoom > available + 0.5;
440
+ }