sirena 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 (382) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build_deploy.yml +59 -0
  3. data/.github/workflows/links.yml +85 -0
  4. data/.github/workflows/rake.yml +15 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +68 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +14 -0
  9. data/.rubocop_todo.yml +70 -0
  10. data/ARCHITECTURE.md +744 -0
  11. data/Gemfile +12 -0
  12. data/LICENSE +25 -0
  13. data/README.adoc +357 -0
  14. data/Rakefile +11 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/Gemfile +13 -0
  17. data/docs/_config.yml +182 -0
  18. data/docs/_diagram_types/architecture-diagram.adoc +314 -0
  19. data/docs/_diagram_types/block-diagram.adoc +345 -0
  20. data/docs/_diagram_types/c4-diagram.adoc +559 -0
  21. data/docs/_diagram_types/class-diagram.adoc +816 -0
  22. data/docs/_diagram_types/er-diagram.adoc +719 -0
  23. data/docs/_diagram_types/error-diagram.adoc +114 -0
  24. data/docs/_diagram_types/examples/flowchart-examples.adoc +29 -0
  25. data/docs/_diagram_types/flowchart.adoc +488 -0
  26. data/docs/_diagram_types/gantt-chart.adoc +502 -0
  27. data/docs/_diagram_types/git-graph.adoc +600 -0
  28. data/docs/_diagram_types/index.adoc +192 -0
  29. data/docs/_diagram_types/info-diagram.adoc +103 -0
  30. data/docs/_diagram_types/kanban-diagram.adoc +262 -0
  31. data/docs/_diagram_types/mindmap.adoc +603 -0
  32. data/docs/_diagram_types/packet-diagram.adoc +378 -0
  33. data/docs/_diagram_types/pie-chart.adoc +335 -0
  34. data/docs/_diagram_types/quadrant-chart.adoc +406 -0
  35. data/docs/_diagram_types/radar-chart.adoc +528 -0
  36. data/docs/_diagram_types/requirement-diagram.adoc +416 -0
  37. data/docs/_diagram_types/sankey-diagram.adoc +357 -0
  38. data/docs/_diagram_types/sequence-diagram.adoc +664 -0
  39. data/docs/_diagram_types/state-diagram.adoc +658 -0
  40. data/docs/_diagram_types/timeline.adoc +352 -0
  41. data/docs/_diagram_types/treemap-diagram.adoc +462 -0
  42. data/docs/_diagram_types/user-journey.adoc +602 -0
  43. data/docs/_features/index.adoc +129 -0
  44. data/docs/_guides/cli-reference.adoc +203 -0
  45. data/docs/_guides/index.adoc +56 -0
  46. data/docs/_guides/installation.adoc +100 -0
  47. data/docs/_guides/quick-start.adoc +132 -0
  48. data/docs/_pages/comparison.adoc +441 -0
  49. data/docs/_pages/compatibility.adoc +300 -0
  50. data/docs/_pages/index.adoc +39 -0
  51. data/docs/_references/index.adoc +103 -0
  52. data/docs/_tutorials/index.adoc +57 -0
  53. data/docs/index.adoc +166 -0
  54. data/docs/lychee.toml +54 -0
  55. data/examples/.gitignore +10 -0
  56. data/examples/README.adoc +196 -0
  57. data/examples/README.md +64 -0
  58. data/examples/architecture/01-basic-services.mmd +9 -0
  59. data/examples/architecture/01-basic-services.svg +37 -0
  60. data/examples/architecture/02-service-groups.mmd +16 -0
  61. data/examples/architecture/02-service-groups.svg +55 -0
  62. data/examples/architecture/README.adoc +79 -0
  63. data/examples/block/01-basic-blocks.mmd +13 -0
  64. data/examples/block/01-basic-blocks.svg +44 -0
  65. data/examples/block/02-block-shapes.mmd +13 -0
  66. data/examples/block/02-block-shapes.svg +47 -0
  67. data/examples/block/README.adoc +85 -0
  68. data/examples/c4/01-context-diagram.mmd +10 -0
  69. data/examples/c4/01-context-diagram.svg +45 -0
  70. data/examples/c4/02-container-diagram.mmd +24 -0
  71. data/examples/c4/02-container-diagram.svg +105 -0
  72. data/examples/c4/README.adoc +92 -0
  73. data/examples/class_diagram/01-basic-classes.mmd +61 -0
  74. data/examples/class_diagram/01-basic-classes.svg +117 -0
  75. data/examples/class_diagram/02-relationships.mmd +61 -0
  76. data/examples/class_diagram/02-relationships.svg +129 -0
  77. data/examples/class_diagram/README.adoc +93 -0
  78. data/examples/er_diagram/01-basic-entities.mmd +64 -0
  79. data/examples/er_diagram/01-basic-entities.svg +5 -0
  80. data/examples/er_diagram/02-cardinality.mmd +57 -0
  81. data/examples/er_diagram/02-cardinality.svg +125 -0
  82. data/examples/er_diagram/README.adoc +88 -0
  83. data/examples/error/01-basic-error.mmd +1 -0
  84. data/examples/error/01-basic-error.svg +13 -0
  85. data/examples/error/02-error-display.mmd +1 -0
  86. data/examples/error/02-error-display.svg +13 -0
  87. data/examples/error/README.adoc +71 -0
  88. data/examples/error_message_example.svg +13 -0
  89. data/examples/flowchart/00-original.mmd +13 -0
  90. data/examples/flowchart/00-original.svg +5 -0
  91. data/examples/flowchart/01-basic-flow.mmd +7 -0
  92. data/examples/flowchart/01-basic-flow.svg +52 -0
  93. data/examples/flowchart/01-basic-flow.yml +13 -0
  94. data/examples/flowchart/02*.svg +87 -0
  95. data/examples/flowchart/02-node-shapes.mmd +9 -0
  96. data/examples/flowchart/02-node-shapes.svg +33 -0
  97. data/examples/flowchart/03-edge-types.mmd +7 -0
  98. data/examples/flowchart/03-edge-types.svg +53 -0
  99. data/examples/flowchart/04-subgraphs.mmd +9 -0
  100. data/examples/flowchart/04-subgraphs.svg +33 -0
  101. data/examples/flowchart/05-styling.mmd +9 -0
  102. data/examples/flowchart/05-styling.svg +33 -0
  103. data/examples/flowchart/06-complex-flow.mmd +8 -0
  104. data/examples/flowchart/06-complex-flow.svg +59 -0
  105. data/examples/flowchart/README.adoc +167 -0
  106. data/examples/gantt/01-simple-timeline.* +14 -0
  107. data/examples/gantt/01-simple-timeline.mmd +6 -0
  108. data/examples/gantt/01-simple-timeline.svg +26 -0
  109. data/examples/gantt/02-task-dependencies.mmd +6 -0
  110. data/examples/gantt/02-task-dependencies.svg +26 -0
  111. data/examples/gantt/README.adoc +86 -0
  112. data/examples/git_graph/01-linear-history.mmd +12 -0
  113. data/examples/git_graph/01-linear-history.svg +26 -0
  114. data/examples/git_graph/02-branching.mmd +12 -0
  115. data/examples/git_graph/02-branching.svg +26 -0
  116. data/examples/git_graph/README.adoc +73 -0
  117. data/examples/info/02-showinfo.mmd +1 -0
  118. data/examples/info/02-showinfo.svg +10 -0
  119. data/examples/info/README.adoc +58 -0
  120. data/examples/info_showinfo_example.svg +10 -0
  121. data/examples/kanban/01-simple-board.mmd +8 -0
  122. data/examples/kanban/01-simple-board.svg +43 -0
  123. data/examples/kanban/02-workflow.mmd +8 -0
  124. data/examples/kanban/02-workflow.svg +43 -0
  125. data/examples/kanban/README.adoc +79 -0
  126. data/examples/mindmap/01-simple-tree.mmd +19 -0
  127. data/examples/mindmap/01-simple-tree.svg +61 -0
  128. data/examples/mindmap/02-knowledge-map.mmd +19 -0
  129. data/examples/mindmap/02-knowledge-map.svg +61 -0
  130. data/examples/mindmap/README.adoc +77 -0
  131. data/examples/packet/01-basic-packet.* +17 -0
  132. data/examples/packet/01-basic-packet.mmd +4 -0
  133. data/examples/packet/01-basic-packet.svg +82 -0
  134. data/examples/packet/README.adoc +58 -0
  135. data/examples/pie/01-simple-chart.mmd +5 -0
  136. data/examples/pie/01-simple-chart.svg +17 -0
  137. data/examples/pie/02-labeled-slices.mmd +6 -0
  138. data/examples/pie/02-labeled-slices.svg +19 -0
  139. data/examples/pie/README.adoc +75 -0
  140. data/examples/quadrant/01-basic-quadrant.mmd +13 -0
  141. data/examples/quadrant/01-basic-quadrant.svg +33 -0
  142. data/examples/quadrant/02-positioned-items.mmd +14 -0
  143. data/examples/quadrant/02-positioned-items.svg +35 -0
  144. data/examples/quadrant/README.adoc +84 -0
  145. data/examples/radar/01-simple-radar.* +5 -0
  146. data/examples/radar/01-simple-radar.mmd +3 -0
  147. data/examples/radar/01-simple-radar.svg +25 -0
  148. data/examples/radar/02-multiple-curves.mmd +4 -0
  149. data/examples/radar/02-multiple-curves.svg +43 -0
  150. data/examples/radar/README.adoc +75 -0
  151. data/examples/requirement/01-basic-requirements.mmd +23 -0
  152. data/examples/requirement/01-basic-requirements.svg +49 -0
  153. data/examples/requirement/02-risk-levels.mmd +23 -0
  154. data/examples/requirement/02-risk-levels.svg +49 -0
  155. data/examples/requirement/README.adoc +85 -0
  156. data/examples/sankey/01-simple-flow.mmd +7 -0
  157. data/examples/sankey/01-simple-flow.svg +34 -0
  158. data/examples/sankey/02-multi-stage.mmd +11 -0
  159. data/examples/sankey/02-multi-stage.svg +44 -0
  160. data/examples/sankey/README.adoc +74 -0
  161. data/examples/sequence/01-basic-sequence.mmd +27 -0
  162. data/examples/sequence/01-basic-sequence.svg +5 -0
  163. data/examples/sequence/02-activations.mmd +17 -0
  164. data/examples/sequence/02-activations.svg +78 -0
  165. data/examples/sequence/README.adoc +86 -0
  166. data/examples/state_diagram/01-simple-states.mmd +29 -0
  167. data/examples/state_diagram/01-simple-states.svg +5 -0
  168. data/examples/state_diagram/02-composite.mmd +19 -0
  169. data/examples/state_diagram/02-composite.svg +81 -0
  170. data/examples/state_diagram/README.adoc +90 -0
  171. data/examples/timeline/01-simple-timeline.mmd +11 -0
  172. data/examples/timeline/01-simple-timeline.svg +36 -0
  173. data/examples/timeline/02-periods.mmd +15 -0
  174. data/examples/timeline/02-periods.svg +47 -0
  175. data/examples/timeline/README.adoc +78 -0
  176. data/examples/treemap/01-basic-treemap.mmd +12 -0
  177. data/examples/treemap/01-basic-treemap.svg +59 -0
  178. data/examples/treemap/README.adoc +59 -0
  179. data/examples/user_journey/01-simple-journey.mmd +23 -0
  180. data/examples/user_journey/01-simple-journey.svg +5 -0
  181. data/examples/user_journey/02-multi-actor.mmd +18 -0
  182. data/examples/user_journey/02-multi-actor.svg +129 -0
  183. data/examples/user_journey/README.adoc +81 -0
  184. data/examples/xychart/01-line-chart.mmd +5 -0
  185. data/examples/xychart/01-line-chart.svg +43 -0
  186. data/examples/xychart/02-bar-chart.mmd +7 -0
  187. data/examples/xychart/02-bar-chart.svg +48 -0
  188. data/examples/xychart/README.adoc +80 -0
  189. data/exe/sirena +7 -0
  190. data/lib/sirena/cli.rb +138 -0
  191. data/lib/sirena/commands/batch.rb +117 -0
  192. data/lib/sirena/commands/render.rb +80 -0
  193. data/lib/sirena/commands/types.rb +29 -0
  194. data/lib/sirena/commands/version.rb +24 -0
  195. data/lib/sirena/diagram/architecture.rb +46 -0
  196. data/lib/sirena/diagram/base.rb +61 -0
  197. data/lib/sirena/diagram/block.rb +81 -0
  198. data/lib/sirena/diagram/c4.rb +328 -0
  199. data/lib/sirena/diagram/class_diagram.rb +385 -0
  200. data/lib/sirena/diagram/er_diagram.rb +238 -0
  201. data/lib/sirena/diagram/error.rb +38 -0
  202. data/lib/sirena/diagram/flowchart.rb +160 -0
  203. data/lib/sirena/diagram/gantt.rb +71 -0
  204. data/lib/sirena/diagram/git_graph.rb +36 -0
  205. data/lib/sirena/diagram/info.rb +38 -0
  206. data/lib/sirena/diagram/kanban.rb +178 -0
  207. data/lib/sirena/diagram/mindmap.rb +54 -0
  208. data/lib/sirena/diagram/packet.rb +79 -0
  209. data/lib/sirena/diagram/pie.rb +115 -0
  210. data/lib/sirena/diagram/quadrant.rb +138 -0
  211. data/lib/sirena/diagram/radar.rb +52 -0
  212. data/lib/sirena/diagram/requirement.rb +133 -0
  213. data/lib/sirena/diagram/sankey.rb +217 -0
  214. data/lib/sirena/diagram/sequence.rb +242 -0
  215. data/lib/sirena/diagram/state_diagram.rb +237 -0
  216. data/lib/sirena/diagram/timeline.rb +171 -0
  217. data/lib/sirena/diagram/treemap.rb +84 -0
  218. data/lib/sirena/diagram/user_journey.rb +149 -0
  219. data/lib/sirena/diagram/xy_chart.rb +76 -0
  220. data/lib/sirena/diagram.rb +8 -0
  221. data/lib/sirena/diagram_registry.rb +101 -0
  222. data/lib/sirena/engine.rb +292 -0
  223. data/lib/sirena/parser/architecture.rb +41 -0
  224. data/lib/sirena/parser/base.rb +41 -0
  225. data/lib/sirena/parser/block.rb +72 -0
  226. data/lib/sirena/parser/c4.rb +53 -0
  227. data/lib/sirena/parser/class_diagram.rb +63 -0
  228. data/lib/sirena/parser/er_diagram.rb +40 -0
  229. data/lib/sirena/parser/error.rb +49 -0
  230. data/lib/sirena/parser/flowchart.rb +71 -0
  231. data/lib/sirena/parser/gantt.rb +60 -0
  232. data/lib/sirena/parser/git_graph.rb +95 -0
  233. data/lib/sirena/parser/grammars/architecture.rb +145 -0
  234. data/lib/sirena/parser/grammars/block.rb +190 -0
  235. data/lib/sirena/parser/grammars/c4.rb +226 -0
  236. data/lib/sirena/parser/grammars/class_diagram.rb +284 -0
  237. data/lib/sirena/parser/grammars/common.rb +84 -0
  238. data/lib/sirena/parser/grammars/er_diagram.rb +114 -0
  239. data/lib/sirena/parser/grammars/error.rb +40 -0
  240. data/lib/sirena/parser/grammars/flowchart.rb +298 -0
  241. data/lib/sirena/parser/grammars/gantt.rb +252 -0
  242. data/lib/sirena/parser/grammars/git_graph.rb +167 -0
  243. data/lib/sirena/parser/grammars/info.rb +58 -0
  244. data/lib/sirena/parser/grammars/kanban.rb +83 -0
  245. data/lib/sirena/parser/grammars/mindmap.rb +115 -0
  246. data/lib/sirena/parser/grammars/packet.rb +73 -0
  247. data/lib/sirena/parser/grammars/pie.rb +128 -0
  248. data/lib/sirena/parser/grammars/quadrant.rb +199 -0
  249. data/lib/sirena/parser/grammars/radar.rb +150 -0
  250. data/lib/sirena/parser/grammars/requirement.rb +188 -0
  251. data/lib/sirena/parser/grammars/sankey.rb +104 -0
  252. data/lib/sirena/parser/grammars/sequence.rb +247 -0
  253. data/lib/sirena/parser/grammars/state_diagram.rb +172 -0
  254. data/lib/sirena/parser/grammars/timeline.rb +142 -0
  255. data/lib/sirena/parser/grammars/treemap.rb +120 -0
  256. data/lib/sirena/parser/grammars/xy_chart.rb +120 -0
  257. data/lib/sirena/parser/info.rb +49 -0
  258. data/lib/sirena/parser/kanban.rb +97 -0
  259. data/lib/sirena/parser/mindmap.rb +106 -0
  260. data/lib/sirena/parser/packet.rb +76 -0
  261. data/lib/sirena/parser/pie.rb +49 -0
  262. data/lib/sirena/parser/quadrant.rb +57 -0
  263. data/lib/sirena/parser/radar.rb +104 -0
  264. data/lib/sirena/parser/requirement.rb +70 -0
  265. data/lib/sirena/parser/sankey.rb +64 -0
  266. data/lib/sirena/parser/sequence.rb +51 -0
  267. data/lib/sirena/parser/state_diagram.rb +69 -0
  268. data/lib/sirena/parser/timeline.rb +57 -0
  269. data/lib/sirena/parser/transforms/architecture.rb +97 -0
  270. data/lib/sirena/parser/transforms/block.rb +254 -0
  271. data/lib/sirena/parser/transforms/c4.rb +347 -0
  272. data/lib/sirena/parser/transforms/class_diagram.rb +352 -0
  273. data/lib/sirena/parser/transforms/er_diagram.rb +169 -0
  274. data/lib/sirena/parser/transforms/error.rb +58 -0
  275. data/lib/sirena/parser/transforms/flowchart.rb +293 -0
  276. data/lib/sirena/parser/transforms/gantt.rb +215 -0
  277. data/lib/sirena/parser/transforms/git_graph.rb +160 -0
  278. data/lib/sirena/parser/transforms/info.rb +58 -0
  279. data/lib/sirena/parser/transforms/kanban.rb +176 -0
  280. data/lib/sirena/parser/transforms/mindmap.rb +227 -0
  281. data/lib/sirena/parser/transforms/packet.rb +63 -0
  282. data/lib/sirena/parser/transforms/pie.rb +143 -0
  283. data/lib/sirena/parser/transforms/quadrant.rb +177 -0
  284. data/lib/sirena/parser/transforms/radar.rb +126 -0
  285. data/lib/sirena/parser/transforms/requirement.rb +272 -0
  286. data/lib/sirena/parser/transforms/sankey.rb +122 -0
  287. data/lib/sirena/parser/transforms/sequence.rb +342 -0
  288. data/lib/sirena/parser/transforms/state_diagram.rb +292 -0
  289. data/lib/sirena/parser/transforms/timeline.rb +177 -0
  290. data/lib/sirena/parser/transforms/treemap.rb +81 -0
  291. data/lib/sirena/parser/transforms/xy_chart.rb +132 -0
  292. data/lib/sirena/parser/treemap.rb +98 -0
  293. data/lib/sirena/parser/user_journey.rb +120 -0
  294. data/lib/sirena/parser/xy_chart.rb +114 -0
  295. data/lib/sirena/parser.rb +8 -0
  296. data/lib/sirena/renderer/architecture.rb +251 -0
  297. data/lib/sirena/renderer/base.rb +251 -0
  298. data/lib/sirena/renderer/block.rb +286 -0
  299. data/lib/sirena/renderer/c4.rb +490 -0
  300. data/lib/sirena/renderer/class_diagram.rb +499 -0
  301. data/lib/sirena/renderer/er_diagram.rb +417 -0
  302. data/lib/sirena/renderer/error.rb +131 -0
  303. data/lib/sirena/renderer/flowchart.rb +301 -0
  304. data/lib/sirena/renderer/gantt.rb +331 -0
  305. data/lib/sirena/renderer/git_graph.rb +368 -0
  306. data/lib/sirena/renderer/info.rb +93 -0
  307. data/lib/sirena/renderer/kanban.rb +295 -0
  308. data/lib/sirena/renderer/mindmap.rb +396 -0
  309. data/lib/sirena/renderer/packet.rb +239 -0
  310. data/lib/sirena/renderer/pie.rb +235 -0
  311. data/lib/sirena/renderer/quadrant.rb +292 -0
  312. data/lib/sirena/renderer/radar.rb +323 -0
  313. data/lib/sirena/renderer/requirement.rb +371 -0
  314. data/lib/sirena/renderer/sankey.rb +255 -0
  315. data/lib/sirena/renderer/sequence.rb +424 -0
  316. data/lib/sirena/renderer/state_diagram.rb +328 -0
  317. data/lib/sirena/renderer/timeline.rb +304 -0
  318. data/lib/sirena/renderer/treemap.rb +152 -0
  319. data/lib/sirena/renderer/user_journey.rb +331 -0
  320. data/lib/sirena/renderer/xy_chart.rb +452 -0
  321. data/lib/sirena/renderer.rb +8 -0
  322. data/lib/sirena/svg/circle.rb +41 -0
  323. data/lib/sirena/svg/document.rb +103 -0
  324. data/lib/sirena/svg/element.rb +65 -0
  325. data/lib/sirena/svg/ellipse.rb +33 -0
  326. data/lib/sirena/svg/group.rb +71 -0
  327. data/lib/sirena/svg/line.rb +49 -0
  328. data/lib/sirena/svg/path.rb +76 -0
  329. data/lib/sirena/svg/polygon.rb +43 -0
  330. data/lib/sirena/svg/polyline.rb +35 -0
  331. data/lib/sirena/svg/rect.rb +57 -0
  332. data/lib/sirena/svg/style.rb +44 -0
  333. data/lib/sirena/svg/text.rb +72 -0
  334. data/lib/sirena/svg.rb +19 -0
  335. data/lib/sirena/text_measurement.rb +71 -0
  336. data/lib/sirena/theme/builtin/dark.yml +70 -0
  337. data/lib/sirena/theme/builtin/default.yml +80 -0
  338. data/lib/sirena/theme/builtin/high_contrast.yml +70 -0
  339. data/lib/sirena/theme/builtin/light.yml +70 -0
  340. data/lib/sirena/theme/color_palette.rb +48 -0
  341. data/lib/sirena/theme/effect_styles.rb +28 -0
  342. data/lib/sirena/theme/registry.rb +41 -0
  343. data/lib/sirena/theme/shape_styles.rb +28 -0
  344. data/lib/sirena/theme/spacing_config.rb +24 -0
  345. data/lib/sirena/theme/typography.rb +30 -0
  346. data/lib/sirena/theme.rb +69 -0
  347. data/lib/sirena/transform/architecture.rb +273 -0
  348. data/lib/sirena/transform/base.rb +199 -0
  349. data/lib/sirena/transform/block.rb +215 -0
  350. data/lib/sirena/transform/c4.rb +288 -0
  351. data/lib/sirena/transform/class_diagram.rb +296 -0
  352. data/lib/sirena/transform/er_diagram.rb +204 -0
  353. data/lib/sirena/transform/error.rb +39 -0
  354. data/lib/sirena/transform/flowchart.rb +161 -0
  355. data/lib/sirena/transform/gantt.rb +253 -0
  356. data/lib/sirena/transform/git_graph.rb +283 -0
  357. data/lib/sirena/transform/info.rb +39 -0
  358. data/lib/sirena/transform/kanban.rb +180 -0
  359. data/lib/sirena/transform/mindmap.rb +251 -0
  360. data/lib/sirena/transform/packet.rb +185 -0
  361. data/lib/sirena/transform/pie.rb +62 -0
  362. data/lib/sirena/transform/quadrant.rb +167 -0
  363. data/lib/sirena/transform/radar.rb +227 -0
  364. data/lib/sirena/transform/requirement.rb +233 -0
  365. data/lib/sirena/transform/sankey.rb +212 -0
  366. data/lib/sirena/transform/sequence.rb +143 -0
  367. data/lib/sirena/transform/state_diagram.rb +228 -0
  368. data/lib/sirena/transform/timeline.rb +139 -0
  369. data/lib/sirena/transform/treemap.rb +120 -0
  370. data/lib/sirena/transform/user_journey.rb +207 -0
  371. data/lib/sirena/transform/xy_chart.rb +273 -0
  372. data/lib/sirena/transform.rb +8 -0
  373. data/lib/sirena/version.rb +5 -0
  374. data/lib/sirena.rb +328 -0
  375. data/lib/tasks/benchmark.rake +532 -0
  376. data/lib/tasks/examples.rake +468 -0
  377. data/lib/tasks/generate_mermaid_fixtures.rake +363 -0
  378. data/lib/tasks/mermaid_fixtures.rake +46 -0
  379. data/scripts/extract_mermaid_tests.rb +493 -0
  380. data/scripts/rename_to_sirena.rb +73 -0
  381. data/sirena.gemspec +47 -0
  382. metadata +529 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+
4
+ require_relative "base"
5
+ require_relative "../diagram/sankey"
6
+
7
+ module Sirena
8
+ module Transform
9
+ # Sankey transformer for converting Sankey models to renderable structure.
10
+ #
11
+ # Handles layer assignment, node positioning, flow path calculation,
12
+ # and flow width proportional to values.
13
+ #
14
+ # @example Transform a Sankey diagram
15
+ # transform = SankeyTransform.new
16
+ # data = transform.to_graph(sankey_diagram)
17
+ class SankeyTransform < Base
18
+ # Node height and spacing
19
+ NODE_HEIGHT = 40
20
+ NODE_SPACING = 30
21
+ LAYER_SPACING = 150
22
+ MIN_NODE_WIDTH = 20
23
+ MAX_NODE_WIDTH = 40
24
+
25
+ # Converts a Sankey diagram to a layout structure with calculated positions.
26
+ #
27
+ # @param diagram [Diagram::SankeyDiagram] the sankey diagram to transform
28
+ # @return [Hash] data structure for rendering
29
+ # @raise [TransformError] if diagram is invalid
30
+ def to_graph(diagram)
31
+ raise TransformError, "Invalid diagram" unless diagram.valid?
32
+
33
+ @diagram = diagram
34
+ @node_layers = {}
35
+ @node_positions = {}
36
+
37
+ # Assign nodes to layers (left to right)
38
+ assign_layers
39
+
40
+ # Calculate vertical positions within layers
41
+ calculate_positions
42
+
43
+ # Build transformation result
44
+ {
45
+ id: "sankey",
46
+ title: diagram.title,
47
+ acc_title: diagram.acc_title,
48
+ acc_description: diagram.acc_description,
49
+ nodes: transform_nodes,
50
+ flows: transform_flows,
51
+ metadata: {
52
+ node_count: diagram.nodes.length,
53
+ flow_count: diagram.flows.length,
54
+ total_flow: diagram.total_flow,
55
+ max_flow: diagram.max_flow,
56
+ layer_count: @node_layers.values.max.to_i + 1
57
+ }
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def assign_layers
64
+ # Use topological sorting to assign layers
65
+ # Source nodes (no inflow) start at layer 0
66
+ # Each subsequent layer is one step from previous
67
+
68
+ visited = Set.new
69
+ node_ids = @diagram.all_node_ids
70
+
71
+ # Start with source nodes at layer 0
72
+ source_nodes = @diagram.source_nodes
73
+ source_nodes.each do |node_id|
74
+ @node_layers[node_id] = 0
75
+ visited.add(node_id)
76
+ end
77
+
78
+ # BFS to assign layers
79
+ queue = source_nodes.dup
80
+ while !queue.empty?
81
+ current_id = queue.shift
82
+ current_layer = @node_layers[current_id]
83
+
84
+ # Process all flows from this node
85
+ @diagram.flows_from(current_id).each do |flow|
86
+ target_id = flow.target
87
+ next if visited.include?(target_id)
88
+
89
+ # Assign target to next layer
90
+ target_layer = current_layer + 1
91
+ @node_layers[target_id] = [
92
+ @node_layers[target_id] || 0,
93
+ target_layer
94
+ ].max
95
+
96
+ unless queue.include?(target_id)
97
+ queue << target_id
98
+ visited.add(target_id)
99
+ end
100
+ end
101
+ end
102
+
103
+ # Handle any nodes not reachable from sources (cycles, isolated nodes)
104
+ node_ids.each do |node_id|
105
+ unless @node_layers.key?(node_id)
106
+ @node_layers[node_id] = 0
107
+ end
108
+ end
109
+ end
110
+
111
+ def calculate_positions
112
+ # Group nodes by layer
113
+ layers = Hash.new { |h, k| h[k] = [] }
114
+ @node_layers.each do |node_id, layer|
115
+ layers[layer] << node_id
116
+ end
117
+
118
+ # Calculate vertical positions for each layer
119
+ layers.each do |layer_num, node_ids|
120
+ # Sort nodes by total flow magnitude for better layout
121
+ sorted_nodes = node_ids.sort_by do |id|
122
+ -(@diagram.total_inflow(id) + @diagram.total_outflow(id))
123
+ end
124
+
125
+ # Position nodes vertically with spacing
126
+ y_offset = 0
127
+ sorted_nodes.each do |node_id|
128
+ x = layer_num * LAYER_SPACING
129
+ y = y_offset
130
+
131
+ @node_positions[node_id] = {
132
+ x: x,
133
+ y: y,
134
+ width: calculate_node_width(node_id),
135
+ height: NODE_HEIGHT
136
+ }
137
+
138
+ y_offset += NODE_HEIGHT + NODE_SPACING
139
+ end
140
+ end
141
+ end
142
+
143
+ def calculate_node_width(node_id)
144
+ # Node width proportional to flow through it
145
+ total_flow = @diagram.total_inflow(node_id) +
146
+ @diagram.total_outflow(node_id)
147
+
148
+ if total_flow > 0 && @diagram.max_flow > 0
149
+ ratio = total_flow / (@diagram.max_flow * 2)
150
+ width = MIN_NODE_WIDTH + (ratio * (MAX_NODE_WIDTH - MIN_NODE_WIDTH))
151
+ width.round
152
+ else
153
+ MIN_NODE_WIDTH
154
+ end
155
+ end
156
+
157
+ def transform_nodes
158
+ @diagram.nodes.map do |node|
159
+ position = @node_positions[node.id] || { x: 0, y: 0, width: MIN_NODE_WIDTH, height: NODE_HEIGHT }
160
+
161
+ {
162
+ id: node.id,
163
+ label: node.display_label,
164
+ layer: @node_layers[node.id] || 0,
165
+ x: position[:x],
166
+ y: position[:y],
167
+ width: position[:width],
168
+ height: position[:height],
169
+ inflow: @diagram.total_inflow(node.id),
170
+ outflow: @diagram.total_outflow(node.id)
171
+ }
172
+ end
173
+ end
174
+
175
+ def transform_flows
176
+ @diagram.flows.map.with_index do |flow, index|
177
+ source_pos = @node_positions[flow.source]
178
+ target_pos = @node_positions[flow.target]
179
+
180
+ # Calculate flow width proportional to value
181
+ flow_width = calculate_flow_width(flow.value)
182
+
183
+ {
184
+ id: "flow_#{index}",
185
+ source: flow.source,
186
+ target: flow.target,
187
+ value: flow.value,
188
+ label: flow.label,
189
+ width: flow_width,
190
+ source_x: source_pos ? source_pos[:x] + source_pos[:width] : 0,
191
+ source_y: source_pos ? source_pos[:y] + (source_pos[:height] / 2.0) : 0,
192
+ target_x: target_pos ? target_pos[:x] : LAYER_SPACING,
193
+ target_y: target_pos ? target_pos[:y] + (target_pos[:height] / 2.0) : 0,
194
+ self_loop: flow.self_loop?
195
+ }
196
+ end
197
+ end
198
+
199
+ def calculate_flow_width(value)
200
+ # Flow width proportional to value
201
+ return 1 if @diagram.max_flow.zero?
202
+
203
+ ratio = value / @diagram.max_flow
204
+ min_width = 2
205
+ max_width = 50
206
+
207
+ width = min_width + (ratio * (max_width - min_width))
208
+ [width.round, min_width].max
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/sequence'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # Sequence transformer for converting sequence models to graphs.
9
+ #
10
+ # Converts a typed sequence diagram model into a generic graph structure
11
+ # suitable for layout computation by elkrb. Handles participant
12
+ # positioning, message routing, and lifeline calculation.
13
+ #
14
+ # @example Transform a sequence diagram
15
+ # transform = SequenceTransform.new
16
+ # graph = transform.to_graph(sequence_diagram)
17
+ class SequenceTransform < Base
18
+ # Default font size for text measurement
19
+ DEFAULT_FONT_SIZE = 14
20
+
21
+ # Minimum spacing between participants
22
+ PARTICIPANT_SPACING = 150
23
+
24
+ # Width of participant box
25
+ PARTICIPANT_WIDTH = 120
26
+
27
+ # Height of participant box
28
+ PARTICIPANT_HEIGHT = 40
29
+
30
+ # Vertical spacing between messages
31
+ MESSAGE_SPACING = 60
32
+
33
+ # Converts a sequence diagram to a graph structure.
34
+ #
35
+ # @param diagram [Diagram::Sequence] the sequence diagram to transform
36
+ # @return [Hash] elkrb-compatible graph hash
37
+ # @raise [TransformError] if diagram is invalid
38
+ def to_graph(diagram)
39
+ raise TransformError, 'Invalid diagram' unless diagram.valid?
40
+
41
+ {
42
+ id: diagram.id || 'sequence',
43
+ children: transform_participants(diagram),
44
+ edges: transform_messages(diagram),
45
+ layoutOptions: layout_options(diagram),
46
+ metadata: {
47
+ participants: diagram.participants.map(&:id),
48
+ message_count: diagram.messages.length,
49
+ notes: diagram.notes
50
+ }
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ def transform_participants(diagram)
57
+ diagram.participants.map.with_index do |participant, index|
58
+ {
59
+ id: participant.id,
60
+ width: PARTICIPANT_WIDTH,
61
+ height: PARTICIPANT_HEIGHT,
62
+ labels: [
63
+ {
64
+ text: participant.label,
65
+ width: measure_text(participant.label,
66
+ font_size: DEFAULT_FONT_SIZE)[:width],
67
+ height: measure_text(participant.label,
68
+ font_size: DEFAULT_FONT_SIZE)[:height]
69
+ }
70
+ ],
71
+ metadata: {
72
+ actor_type: participant.actor_type,
73
+ index: index,
74
+ lifeline_length: calculate_lifeline_length(diagram)
75
+ }
76
+ }
77
+ end
78
+ end
79
+
80
+ def transform_messages(diagram)
81
+ return [] if diagram.messages.nil? || diagram.messages.empty?
82
+
83
+ diagram.messages.map.with_index do |message, index|
84
+ {
85
+ id: "msg_#{index}",
86
+ sources: [message.from_id],
87
+ targets: [message.to_id],
88
+ labels: message_labels(message),
89
+ metadata: {
90
+ arrow_type: message.arrow_type,
91
+ message_index: index,
92
+ message_text: message.message_text
93
+ }
94
+ }
95
+ end
96
+ end
97
+
98
+ def message_labels(message)
99
+ return [] if message.message_text.nil? || message.message_text.empty?
100
+
101
+ label_dims = measure_text(
102
+ message.message_text,
103
+ font_size: DEFAULT_FONT_SIZE
104
+ )
105
+
106
+ [
107
+ {
108
+ text: message.message_text,
109
+ width: label_dims[:width],
110
+ height: label_dims[:height]
111
+ }
112
+ ]
113
+ end
114
+
115
+ def calculate_lifeline_length(diagram)
116
+ # Calculate total height needed for all messages
117
+ message_count = diagram.messages.length
118
+ base_height = message_count * MESSAGE_SPACING
119
+
120
+ # Add extra space for notes if present
121
+ note_count = diagram.notes&.length || 0
122
+ base_height + (note_count * 30)
123
+ end
124
+
125
+ def layout_options(_diagram)
126
+ # Sequence diagrams use layered algorithm for vertical timeline
127
+ # DIRECTION_DOWN ensures participants are arranged horizontally at top
128
+ # with messages flowing downward in chronological order
129
+ build_elk_options(
130
+ algorithm: ALGORITHM_LAYERED,
131
+ direction: DIRECTION_DOWN,
132
+ ElkOptions::NODE_NODE_SPACING => PARTICIPANT_SPACING,
133
+ ElkOptions::LAYER_SPACING => MESSAGE_SPACING,
134
+ ElkOptions::EDGE_NODE_SPACING => 20,
135
+ ElkOptions::EDGE_EDGE_SPACING => 15,
136
+ # SIMPLE node placement maintains participant order
137
+ ElkOptions::NODE_PLACEMENT => 'SIMPLE',
138
+ ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES'
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/state_diagram'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # State diagram transformer for converting state diagram models to graphs.
9
+ #
10
+ # Converts a typed state diagram model into a generic graph structure
11
+ # suitable for layout computation by elkrb. Handles state node dimension
12
+ # calculation, transition mapping, and layout configuration.
13
+ #
14
+ # @example Transform a state diagram
15
+ # transform = StateDiagramTransform.new
16
+ # graph = transform.to_graph(state_diagram)
17
+ class StateDiagramTransform < Base
18
+ # Default font size for text measurement
19
+ DEFAULT_FONT_SIZE = 14
20
+
21
+ # Converts a state diagram to a graph structure.
22
+ #
23
+ # @param diagram [Diagram::StateDiagram] the state diagram to transform
24
+ # @return [Hash] elkrb-compatible graph hash
25
+ # @raise [TransformError] if diagram is invalid
26
+ def to_graph(diagram)
27
+ raise TransformError, 'Invalid diagram' unless diagram.valid?
28
+
29
+ {
30
+ id: diagram.id || 'state_diagram',
31
+ children: transform_states(diagram),
32
+ edges: transform_transitions(diagram),
33
+ layoutOptions: layout_options(diagram)
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ def transform_states(diagram)
40
+ diagram.states.map do |state|
41
+ dims = calculate_state_dimensions(state)
42
+
43
+ {
44
+ id: state.id,
45
+ width: dims[:width],
46
+ height: dims[:height],
47
+ labels: state_labels(state, dims),
48
+ metadata: {
49
+ state_type: state.state_type,
50
+ description: state.description
51
+ }
52
+ }
53
+ end
54
+ end
55
+
56
+ def transform_transitions(diagram)
57
+ return [] if diagram.transitions.nil? || diagram.transitions.empty?
58
+
59
+ diagram.transitions.map do |transition|
60
+ {
61
+ id: "#{transition.from_id}_to_#{transition.to_id}",
62
+ sources: [transition.from_id],
63
+ targets: [transition.to_id],
64
+ labels: transition_labels(transition),
65
+ metadata: {
66
+ trigger: transition.trigger,
67
+ guard_condition: transition.guard_condition
68
+ }
69
+ }
70
+ end
71
+ end
72
+
73
+ def state_labels(state, dims)
74
+ labels = []
75
+
76
+ # Add state label
77
+ if state.label && !state.label.empty?
78
+ labels << {
79
+ text: state.label,
80
+ width: dims[:label_width],
81
+ height: dims[:label_height]
82
+ }
83
+ end
84
+
85
+ # Add description if present
86
+ if state.description && !state.description.empty?
87
+ desc_dims = measure_text(
88
+ state.description,
89
+ font_size: DEFAULT_FONT_SIZE - 2
90
+ )
91
+ labels << {
92
+ text: state.description,
93
+ width: desc_dims[:width],
94
+ height: desc_dims[:height]
95
+ }
96
+ end
97
+
98
+ labels
99
+ end
100
+
101
+ def transition_labels(transition)
102
+ label = transition.label
103
+ return [] if label.nil? || label.empty?
104
+
105
+ label_dims = measure_text(label, font_size: DEFAULT_FONT_SIZE)
106
+
107
+ [
108
+ {
109
+ text: label,
110
+ width: label_dims[:width],
111
+ height: label_dims[:height]
112
+ }
113
+ ]
114
+ end
115
+
116
+ def calculate_state_dimensions(state)
117
+ label_text = state.label || state.id
118
+ label_dims = measure_text(
119
+ label_text,
120
+ font_size: DEFAULT_FONT_SIZE
121
+ )
122
+
123
+ # Adjust dimensions based on state type
124
+ state_dims = case state.state_type
125
+ when 'start', 'end'
126
+ calculate_terminal_dimensions
127
+ when 'choice'
128
+ calculate_choice_dimensions(label_dims)
129
+ when 'fork', 'join'
130
+ calculate_fork_join_dimensions
131
+ else
132
+ calculate_normal_state_dimensions(
133
+ label_dims,
134
+ state.description
135
+ )
136
+ end
137
+
138
+ {
139
+ width: state_dims[:width],
140
+ height: state_dims[:height],
141
+ label_width: label_dims[:width],
142
+ label_height: label_dims[:height]
143
+ }
144
+ end
145
+
146
+ def calculate_terminal_dimensions
147
+ # Start and end states are small circles
148
+ {
149
+ width: 30,
150
+ height: 30
151
+ }
152
+ end
153
+
154
+ def calculate_choice_dimensions(label_dims)
155
+ # Choice states are diamonds
156
+ size = [label_dims[:width], label_dims[:height]].max + 40
157
+ {
158
+ width: size,
159
+ height: size
160
+ }
161
+ end
162
+
163
+ def calculate_fork_join_dimensions
164
+ # Fork and join are represented as thick bars
165
+ {
166
+ width: 100,
167
+ height: 10
168
+ }
169
+ end
170
+
171
+ def calculate_normal_state_dimensions(label_dims, description)
172
+ # Normal states are rounded rectangles
173
+ width = label_dims[:width] + 40
174
+ height = label_dims[:height] + 30
175
+
176
+ # Add extra height if there's a description
177
+ if description && !description.empty?
178
+ desc_dims = measure_text(
179
+ description,
180
+ font_size: DEFAULT_FONT_SIZE - 2
181
+ )
182
+ height += desc_dims[:height] + 10
183
+ width = [width, desc_dims[:width] + 40].max
184
+ end
185
+
186
+ # Minimum dimensions
187
+ width = [width, 100].max
188
+ height = [height, 50].max
189
+
190
+ {
191
+ width: width,
192
+ height: height
193
+ }
194
+ end
195
+
196
+ def layout_options(diagram)
197
+ # State diagrams use layered algorithm for state machine flow
198
+ # This ensures proper hierarchical layout of states with clear
199
+ # transition paths from start to end states
200
+ build_elk_options(
201
+ algorithm: ALGORITHM_LAYERED,
202
+ direction: direction_to_layout(diagram.direction),
203
+ ElkOptions::NODE_NODE_SPACING => 60,
204
+ ElkOptions::LAYER_SPACING => 60,
205
+ ElkOptions::EDGE_NODE_SPACING => 40,
206
+ ElkOptions::EDGE_EDGE_SPACING => 30,
207
+ # SIMPLE node placement for predictable state flow
208
+ ElkOptions::NODE_PLACEMENT => 'SIMPLE'
209
+ )
210
+ end
211
+
212
+ def direction_to_layout(direction)
213
+ case direction
214
+ when 'TD', 'TB'
215
+ DIRECTION_DOWN
216
+ when 'LR'
217
+ DIRECTION_RIGHT
218
+ when 'RL'
219
+ DIRECTION_LEFT
220
+ when 'BT'
221
+ DIRECTION_UP
222
+ else
223
+ DIRECTION_DOWN # Default direction
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end