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,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/quadrant'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # Quadrant chart transformer for converting quadrant models to
9
+ # renderable structure.
10
+ #
11
+ # Like pie charts, quadrant charts have a fixed layout structure
12
+ # (2x2 grid). This transformer validates the diagram and prepares
13
+ # the data with computed SVG coordinates for rendering.
14
+ #
15
+ # @example Transform a quadrant chart
16
+ # transform = QuadrantTransform.new
17
+ # data = transform.to_graph(quadrant_diagram)
18
+ class QuadrantTransform < Base
19
+ # Default dimensions for quadrant chart
20
+ DEFAULT_WIDTH = 800
21
+ DEFAULT_HEIGHT = 600
22
+ DEFAULT_MARGIN = 80
23
+
24
+ # Converts a quadrant diagram to a renderable data structure.
25
+ #
26
+ # @param diagram [Diagram::QuadrantChart] the quadrant diagram
27
+ # @return [Hash] data structure for rendering with SVG coordinates
28
+ # @raise [TransformError] if diagram is invalid
29
+ def to_graph(diagram)
30
+ raise TransformError, 'Invalid diagram' unless diagram.valid?
31
+
32
+ # Calculate dimensions
33
+ width = DEFAULT_WIDTH
34
+ height = DEFAULT_HEIGHT
35
+ margin = DEFAULT_MARGIN
36
+
37
+ # Chart area (excluding margins)
38
+ chart_width = width - (margin * 2)
39
+ chart_height = height - (margin * 2)
40
+
41
+ {
42
+ id: diagram.id || 'quadrant',
43
+ title: diagram.title,
44
+ dimensions: {
45
+ width: width,
46
+ height: height,
47
+ margin: margin,
48
+ chart_width: chart_width,
49
+ chart_height: chart_height,
50
+ chart_x: margin,
51
+ chart_y: margin
52
+ },
53
+ axes: {
54
+ x_left: diagram.x_axis_left || '',
55
+ x_right: diagram.x_axis_right || '',
56
+ y_bottom: diagram.y_axis_bottom || '',
57
+ y_top: diagram.y_axis_top || ''
58
+ },
59
+ quadrants: {
60
+ q1: {
61
+ label: diagram.quadrant_1_label,
62
+ number: 1,
63
+ bounds: calculate_quadrant_bounds(1, margin, chart_width,
64
+ chart_height)
65
+ },
66
+ q2: {
67
+ label: diagram.quadrant_2_label,
68
+ number: 2,
69
+ bounds: calculate_quadrant_bounds(2, margin, chart_width,
70
+ chart_height)
71
+ },
72
+ q3: {
73
+ label: diagram.quadrant_3_label,
74
+ number: 3,
75
+ bounds: calculate_quadrant_bounds(3, margin, chart_width,
76
+ chart_height)
77
+ },
78
+ q4: {
79
+ label: diagram.quadrant_4_label,
80
+ number: 4,
81
+ bounds: calculate_quadrant_bounds(4, margin, chart_width,
82
+ chart_height)
83
+ }
84
+ },
85
+ points: transform_points(diagram, margin, chart_width, chart_height)
86
+ }
87
+ end
88
+
89
+ private
90
+
91
+ # Calculate bounds for a specific quadrant.
92
+ #
93
+ # @param quadrant [Integer] quadrant number (1-4)
94
+ # @param margin [Float] chart margin
95
+ # @param chart_width [Float] width of chart area
96
+ # @param chart_height [Float] height of chart area
97
+ # @return [Hash] bounds with x, y, width, height
98
+ def calculate_quadrant_bounds(quadrant, margin, chart_width,
99
+ chart_height)
100
+ half_width = chart_width / 2.0
101
+ half_height = chart_height / 2.0
102
+
103
+ case quadrant
104
+ when 1 # Top-right
105
+ {
106
+ x: margin + half_width,
107
+ y: margin,
108
+ width: half_width,
109
+ height: half_height
110
+ }
111
+ when 2 # Top-left
112
+ {
113
+ x: margin,
114
+ y: margin,
115
+ width: half_width,
116
+ height: half_height
117
+ }
118
+ when 3 # Bottom-left
119
+ {
120
+ x: margin,
121
+ y: margin + half_height,
122
+ width: half_width,
123
+ height: half_height
124
+ }
125
+ when 4 # Bottom-right
126
+ {
127
+ x: margin + half_width,
128
+ y: margin + half_height,
129
+ width: half_width,
130
+ height: half_height
131
+ }
132
+ end
133
+ end
134
+
135
+ # Transform points with calculated SVG coordinates.
136
+ #
137
+ # @param diagram [Diagram::QuadrantChart] the diagram
138
+ # @param margin [Float] chart margin
139
+ # @param chart_width [Float] width of chart area
140
+ # @param chart_height [Float] height of chart area
141
+ # @return [Array<Hash>] points with SVG coordinates
142
+ def transform_points(diagram, margin, chart_width, chart_height)
143
+ diagram.points.map.with_index do |point, index|
144
+ # Convert normalized coordinates (0-1) to SVG coordinates
145
+ # Note: Y-axis is inverted in SVG (0 at top)
146
+ svg_x = margin + (point.x * chart_width)
147
+ svg_y = margin + ((1.0 - point.y) * chart_height)
148
+
149
+ {
150
+ id: "point_#{index}",
151
+ label: point.label,
152
+ x: point.x,
153
+ y: point.y,
154
+ svg_x: svg_x,
155
+ svg_y: svg_y,
156
+ quadrant: point.quadrant,
157
+ radius: point.radius || 6,
158
+ color: point.color,
159
+ stroke_color: point.stroke_color,
160
+ stroke_width: point.stroke_width || 2,
161
+ index: index
162
+ }
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sirena
4
+ module Transform
5
+ # Transforms a RadarChart diagram into a positioned layout structure.
6
+ #
7
+ # The layout algorithm handles:
8
+ # - Radial axis positioning (360° / num_axes)
9
+ # - Data point plotting on each axis
10
+ # - Value normalization and scaling
11
+ # - Polar to cartesian coordinate conversion
12
+ # - Polygon formation for each dataset
13
+ #
14
+ # @example Transform a radar chart
15
+ # transform = Transform::Radar.new
16
+ # layout = transform.to_graph(diagram)
17
+ class Radar
18
+ # Default radius of the chart
19
+ DEFAULT_RADIUS = 200
20
+
21
+ # Padding around the chart
22
+ PADDING = 80
23
+
24
+ # Label distance from the chart center
25
+ LABEL_OFFSET = 30
26
+
27
+ # Number of grid circles to draw
28
+ GRID_CIRCLES = 5
29
+
30
+ # Transforms the diagram into a layout structure.
31
+ #
32
+ # @param diagram [Diagram::RadarChart] the radar chart diagram
33
+ # @return [Hash] layout data with axes, curves, and dimensions
34
+ def to_graph(diagram)
35
+ num_axes = diagram.axes.length
36
+ return empty_layout if num_axes == 0
37
+
38
+ # Calculate value range
39
+ min_value, max_value = calculate_value_range(diagram)
40
+
41
+ # Position axes radially
42
+ positioned_axes = position_axes(diagram.axes, num_axes)
43
+
44
+ # Position data points for each curve
45
+ positioned_curves = position_curves(
46
+ diagram.curves,
47
+ positioned_axes,
48
+ min_value,
49
+ max_value
50
+ )
51
+
52
+ # Calculate grid circles for reference
53
+ grid_circles = calculate_grid_circles(min_value, max_value)
54
+
55
+ {
56
+ axes: positioned_axes,
57
+ curves: positioned_curves,
58
+ grid_circles: grid_circles,
59
+ center_x: DEFAULT_RADIUS + PADDING,
60
+ center_y: DEFAULT_RADIUS + PADDING,
61
+ radius: DEFAULT_RADIUS,
62
+ width: (DEFAULT_RADIUS + PADDING) * 2,
63
+ height: (DEFAULT_RADIUS + PADDING) * 2,
64
+ min_value: min_value,
65
+ max_value: max_value
66
+ }
67
+ end
68
+
69
+ private
70
+
71
+ # Returns an empty layout structure.
72
+ #
73
+ # @return [Hash] empty layout
74
+ def empty_layout
75
+ {
76
+ axes: [],
77
+ curves: [],
78
+ grid_circles: [],
79
+ center_x: PADDING,
80
+ center_y: PADDING,
81
+ radius: DEFAULT_RADIUS,
82
+ width: PADDING * 2,
83
+ height: PADDING * 2,
84
+ min_value: 0,
85
+ max_value: 0
86
+ }
87
+ end
88
+
89
+ # Calculates the value range from all curves.
90
+ #
91
+ # @param diagram [Diagram::RadarChart] diagram
92
+ # @return [Array<Numeric, Numeric>] min and max values
93
+ def calculate_value_range(diagram)
94
+ all_values = diagram.curves.flat_map { |c| c.values.values }
95
+
96
+ # Use configured min/max if available
97
+ min_value = diagram.options[:min] || all_values.min || 0
98
+ max_value = diagram.options[:max] || all_values.max || 100
99
+
100
+ # Ensure max > min
101
+ max_value = min_value + 1 if max_value <= min_value
102
+
103
+ [min_value, max_value]
104
+ end
105
+
106
+ # Positions axes radially around the center.
107
+ #
108
+ # @param axes [Array<Diagram::RadarAxis>] axes
109
+ # @param num_axes [Integer] number of axes
110
+ # @return [Array<Hash>] positioned axes
111
+ def position_axes(axes, num_axes)
112
+ positioned = []
113
+ angle_step = 360.0 / num_axes
114
+
115
+ axes.each_with_index do |axis, idx|
116
+ # Calculate angle (start at top, go clockwise)
117
+ angle_degrees = idx * angle_step - 90 # -90 to start at top
118
+ angle_radians = angle_degrees * Math::PI / 180.0
119
+
120
+ # Calculate end point of axis line
121
+ end_x = Math.cos(angle_radians) * DEFAULT_RADIUS
122
+ end_y = Math.sin(angle_radians) * DEFAULT_RADIUS
123
+
124
+ # Calculate label position (beyond the end point)
125
+ label_radius = DEFAULT_RADIUS + LABEL_OFFSET
126
+ label_x = Math.cos(angle_radians) * label_radius
127
+ label_y = Math.sin(angle_radians) * label_radius
128
+
129
+ positioned << {
130
+ id: axis.id,
131
+ label: axis.label,
132
+ angle_degrees: angle_degrees,
133
+ angle_radians: angle_radians,
134
+ end_x: end_x,
135
+ end_y: end_y,
136
+ label_x: label_x,
137
+ label_y: label_y,
138
+ index: idx
139
+ }
140
+ end
141
+
142
+ positioned
143
+ end
144
+
145
+ # Positions data points for all curves.
146
+ #
147
+ # @param curves [Array<Diagram::RadarCurve>] curves
148
+ # @param positioned_axes [Array<Hash>] positioned axes
149
+ # @param min_value [Numeric] minimum value
150
+ # @param max_value [Numeric] maximum value
151
+ # @return [Array<Hash>] positioned curves with points
152
+ def position_curves(curves, positioned_axes, min_value, max_value)
153
+ positioned = []
154
+
155
+ curves.each do |curve|
156
+ points = []
157
+
158
+ positioned_axes.each do |axis|
159
+ value = curve.value_for(axis[:id])
160
+
161
+ # Normalize value to 0-1 range
162
+ normalized = normalize_value(value, min_value, max_value)
163
+
164
+ # Calculate radius for this value
165
+ radius = normalized * DEFAULT_RADIUS
166
+
167
+ # Convert to cartesian coordinates
168
+ x = Math.cos(axis[:angle_radians]) * radius
169
+ y = Math.sin(axis[:angle_radians]) * radius
170
+
171
+ points << {
172
+ axis_id: axis[:id],
173
+ value: value,
174
+ normalized: normalized,
175
+ x: x,
176
+ y: y,
177
+ angle: axis[:angle_radians]
178
+ }
179
+ end
180
+
181
+ positioned << {
182
+ id: curve.id,
183
+ label: curve.label,
184
+ points: points
185
+ }
186
+ end
187
+
188
+ positioned
189
+ end
190
+
191
+ # Normalizes a value to the 0-1 range.
192
+ #
193
+ # @param value [Numeric] value to normalize
194
+ # @param min_value [Numeric] minimum value
195
+ # @param max_value [Numeric] maximum value
196
+ # @return [Numeric] normalized value
197
+ def normalize_value(value, min_value, max_value)
198
+ return 0 if max_value == min_value
199
+
200
+ ((value - min_value).to_f / (max_value - min_value)).clamp(0, 1)
201
+ end
202
+
203
+ # Calculates grid circle positions.
204
+ #
205
+ # @param min_value [Numeric] minimum value
206
+ # @param max_value [Numeric] maximum value
207
+ # @return [Array<Hash>] grid circles with radius and label
208
+ def calculate_grid_circles(min_value, max_value)
209
+ circles = []
210
+
211
+ GRID_CIRCLES.times do |i|
212
+ fraction = (i + 1).to_f / GRID_CIRCLES
213
+ radius = DEFAULT_RADIUS * fraction
214
+ value = min_value + (max_value - min_value) * fraction
215
+
216
+ circles << {
217
+ radius: radius,
218
+ value: value,
219
+ fraction: fraction
220
+ }
221
+ end
222
+
223
+ circles
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/requirement'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # Requirement diagram transformer for converting requirement models to positioned layouts.
9
+ #
10
+ # Converts a requirement diagram model into a positioned layout structure.
11
+ # Handles requirement and element positioning, relationship routing,
12
+ # and hierarchical layout based on dependencies.
13
+ #
14
+ # @example Transform a requirement diagram
15
+ # transform = RequirementTransform.new
16
+ # layout = transform.to_layout(requirement_diagram)
17
+ class RequirementTransform < Base
18
+ # Default dimensions
19
+ DEFAULT_REQ_WIDTH = 180
20
+ DEFAULT_REQ_HEIGHT = 140
21
+ DEFAULT_ELEM_WIDTH = 150
22
+ DEFAULT_ELEM_HEIGHT = 80
23
+ DEFAULT_SPACING_X = 100
24
+ DEFAULT_SPACING_Y = 80
25
+ DEFAULT_PADDING = 20
26
+
27
+ # Converts a requirement diagram to a positioned layout structure.
28
+ #
29
+ # @param diagram [Diagram::RequirementDiagram] the requirement diagram to transform
30
+ # @return [Hash] positioned layout hash
31
+ # @raise [TransformError] if diagram is invalid
32
+ def to_graph(diagram)
33
+ raise TransformError, 'Diagram cannot be nil' if diagram.nil?
34
+
35
+ # Calculate positions for requirements and elements
36
+ nodes_layout = calculate_node_positions(diagram)
37
+
38
+ # Calculate relationship routes
39
+ relationships_layout = calculate_relationships(diagram, nodes_layout)
40
+
41
+ {
42
+ requirements: nodes_layout[:requirements],
43
+ elements: nodes_layout[:elements],
44
+ relationships: relationships_layout,
45
+ width: nodes_layout[:width],
46
+ height: nodes_layout[:height]
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def calculate_node_positions(diagram)
53
+ requirements = diagram.requirements
54
+ elements = diagram.elements
55
+ relationships = diagram.relationships
56
+
57
+ # Build dependency graph to determine layout
58
+ levels = build_dependency_levels(requirements, elements, relationships)
59
+
60
+ positioned_requirements = {}
61
+ positioned_elements = {}
62
+
63
+ current_y = DEFAULT_PADDING
64
+ max_width = 0
65
+
66
+ levels.each_with_index do |level_nodes, level_idx|
67
+ current_x = DEFAULT_PADDING
68
+ max_height = 0
69
+
70
+ level_nodes.each do |node|
71
+ if node[:type] == :requirement
72
+ req = node[:object]
73
+ dims = calculate_requirement_dimensions(req)
74
+
75
+ positioned_requirements[req.name] = {
76
+ requirement: req,
77
+ x: current_x,
78
+ y: current_y,
79
+ width: dims[:width],
80
+ height: dims[:height],
81
+ level: level_idx
82
+ }
83
+
84
+ current_x += dims[:width] + DEFAULT_SPACING_X
85
+ max_height = [max_height, dims[:height]].max
86
+ else
87
+ elem = node[:object]
88
+ dims = calculate_element_dimensions(elem)
89
+
90
+ positioned_elements[elem.name] = {
91
+ element: elem,
92
+ x: current_x,
93
+ y: current_y,
94
+ width: dims[:width],
95
+ height: dims[:height],
96
+ level: level_idx
97
+ }
98
+
99
+ current_x += dims[:width] + DEFAULT_SPACING_X
100
+ max_height = [max_height, dims[:height]].max
101
+ end
102
+ end
103
+
104
+ max_width = [max_width, current_x].max
105
+ current_y += max_height + DEFAULT_SPACING_Y
106
+ end
107
+
108
+ {
109
+ requirements: positioned_requirements,
110
+ elements: positioned_elements,
111
+ width: max_width + DEFAULT_PADDING,
112
+ height: current_y + DEFAULT_PADDING
113
+ }
114
+ end
115
+
116
+ def build_dependency_levels(requirements, elements, relationships)
117
+ # Build a simple level-based layout
118
+ # Level 0: Elements
119
+ # Level 1: Requirements that depend on elements
120
+ # Level 2+: Requirements that depend on other requirements
121
+
122
+ nodes_by_name = {}
123
+ requirements.each { |r| nodes_by_name[r.name] = { type: :requirement, object: r, level: nil } }
124
+ elements.each { |e| nodes_by_name[e.name] = { type: :element, object: e, level: nil } }
125
+
126
+ # Start with elements at level 0
127
+ elements.each { |e| nodes_by_name[e.name][:level] = 0 }
128
+
129
+ # Build dependency map
130
+ dependencies = Hash.new { |h, k| h[k] = [] }
131
+ relationships.each do |rel|
132
+ # Source depends on target (arrow direction)
133
+ dependencies[rel.source] << rel.target
134
+ end
135
+
136
+ # Calculate levels for requirements
137
+ changed = true
138
+ max_iterations = 10
139
+ iterations = 0
140
+
141
+ while changed && iterations < max_iterations
142
+ changed = false
143
+ iterations += 1
144
+
145
+ requirements.each do |req|
146
+ node = nodes_by_name[req.name]
147
+ next if node[:level]
148
+
149
+ deps = dependencies[req.name]
150
+ if deps.empty?
151
+ # No dependencies, place at level 1
152
+ node[:level] = 1
153
+ changed = true
154
+ else
155
+ # Check if all dependencies have levels
156
+ dep_levels = deps.map { |d| nodes_by_name[d]&.dig(:level) }.compact
157
+ if dep_levels.size == deps.size
158
+ # All dependencies have levels
159
+ max_dep_level = dep_levels.max || 0
160
+ node[:level] = max_dep_level + 1
161
+ changed = true
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Assign default level to any remaining nodes
168
+ nodes_by_name.each do |_name, node|
169
+ node[:level] ||= 1 if node[:type] == :requirement
170
+ end
171
+
172
+ # Group by level
173
+ levels = []
174
+ nodes_by_name.values.group_by { |n| n[:level] }.sort.each do |_level, nodes|
175
+ levels << nodes
176
+ end
177
+
178
+ levels
179
+ end
180
+
181
+ def calculate_requirement_dimensions(requirement)
182
+ # Calculate based on text content
183
+ text = requirement.text || ''
184
+ text_lines = text.length > 0 ? ((text.length / 25.0).ceil) : 1
185
+
186
+ width = DEFAULT_REQ_WIDTH
187
+ height = DEFAULT_REQ_HEIGHT + (text_lines - 1) * 20
188
+
189
+ {
190
+ width: width,
191
+ height: [height, DEFAULT_REQ_HEIGHT].max
192
+ }
193
+ end
194
+
195
+ def calculate_element_dimensions(element)
196
+ {
197
+ width: DEFAULT_ELEM_WIDTH,
198
+ height: DEFAULT_ELEM_HEIGHT
199
+ }
200
+ end
201
+
202
+ def calculate_relationships(diagram, nodes_layout)
203
+ requirements = nodes_layout[:requirements]
204
+ elements = nodes_layout[:elements]
205
+ all_nodes = requirements.merge(elements)
206
+
207
+ diagram.relationships.map do |rel|
208
+ source_node = all_nodes[rel.source]
209
+ target_node = all_nodes[rel.target]
210
+
211
+ next unless source_node && target_node
212
+
213
+ # Calculate connection points
214
+ source_x = source_node[:x] + source_node[:width] / 2
215
+ source_y = source_node[:y] + source_node[:height]
216
+ target_x = target_node[:x] + target_node[:width] / 2
217
+ target_y = target_node[:y]
218
+
219
+ {
220
+ relationship: rel,
221
+ source: rel.source,
222
+ target: rel.target,
223
+ type: rel.type,
224
+ from_x: source_x,
225
+ from_y: source_y,
226
+ to_x: target_x,
227
+ to_y: target_y
228
+ }
229
+ end.compact
230
+ end
231
+ end
232
+ end
233
+ end