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,70 @@
1
+ name: high_contrast
2
+ description: "High contrast theme for accessibility"
3
+
4
+ colors:
5
+ background: "#000000"
6
+ surface: "#000000"
7
+ surface_variant: "#1a1a1a"
8
+
9
+ foreground: "#ffffff"
10
+ foreground_secondary: "#ffffff"
11
+
12
+ primary: "#ffff00"
13
+ secondary: "#00ffff"
14
+ accent: "#00ff00"
15
+ error: "#ff0000"
16
+ warning: "#ff8800"
17
+ success: "#00ff00"
18
+ info: "#00ffff"
19
+
20
+ node_fill: "#000000"
21
+ node_stroke: "#ffffff"
22
+ edge_stroke: "#ffffff"
23
+ label_text: "#ffffff"
24
+
25
+ hover_fill: "#1a1a1a"
26
+ active_fill: "#333333"
27
+ disabled_fill: "#666666"
28
+
29
+ typography:
30
+ font_family: "Arial, Helvetica, sans-serif"
31
+ font_family_monospace: "Courier New, monospace"
32
+ font_size_small: 14.0
33
+ font_size_normal: 16.0
34
+ font_size_large: 18.0
35
+ font_size_title: 24.0
36
+ font_weight_normal: "bold"
37
+ font_weight_bold: "bold"
38
+ line_height: 1.6
39
+ text_transform: "none"
40
+
41
+ shapes:
42
+ stroke_width: 3.0
43
+ stroke_width_thick: 4.0
44
+ stroke_width_thin: 2.0
45
+ fill_opacity: 1.0
46
+ stroke_opacity: 1.0
47
+ corner_radius: 0.0
48
+ corner_radius_large: 0.0
49
+ dash_pattern_dotted: "4,4"
50
+ dash_pattern_dashed: "8,8"
51
+
52
+ spacing:
53
+ node_padding_x: 15.0
54
+ node_padding_y: 8.0
55
+ node_margin: 25.0
56
+ edge_spacing: 15.0
57
+ edge_label_offset: 8.0
58
+ rank_spacing: 120.0
59
+ node_spacing: 60.0
60
+
61
+ effects:
62
+ shadow_enabled: false
63
+ shadow_color: "#000000"
64
+ shadow_blur: 0.0
65
+ shadow_offset_x: 0.0
66
+ shadow_offset_y: 0.0
67
+ gradient_enabled: false
68
+ gradient_type: "linear"
69
+ arrow_size: 12.0
70
+ arrow_style: "filled"
@@ -0,0 +1,70 @@
1
+ name: light
2
+ description: "Light theme with softer colors"
3
+
4
+ colors:
5
+ background: "#fafafa"
6
+ surface: "#ffffff"
7
+ surface_variant: "#f0f0f0"
8
+
9
+ foreground: "#333333"
10
+ foreground_secondary: "#757575"
11
+
12
+ primary: "#1976d2"
13
+ secondary: "#757575"
14
+ accent: "#388e3c"
15
+ error: "#d32f2f"
16
+ warning: "#f57c00"
17
+ success: "#388e3c"
18
+ info: "#0288d1"
19
+
20
+ node_fill: "#ffffff"
21
+ node_stroke: "#757575"
22
+ edge_stroke: "#757575"
23
+ label_text: "#333333"
24
+
25
+ hover_fill: "#f5f5f5"
26
+ active_fill: "#eeeeee"
27
+ disabled_fill: "#e0e0e0"
28
+
29
+ typography:
30
+ font_family: "Arial, Helvetica, sans-serif"
31
+ font_family_monospace: "Courier New, monospace"
32
+ font_size_small: 12.0
33
+ font_size_normal: 14.0
34
+ font_size_large: 16.0
35
+ font_size_title: 20.0
36
+ font_weight_normal: "normal"
37
+ font_weight_bold: "bold"
38
+ line_height: 1.5
39
+ text_transform: "none"
40
+
41
+ shapes:
42
+ stroke_width: 1.5
43
+ stroke_width_thick: 2.5
44
+ stroke_width_thin: 1.0
45
+ fill_opacity: 1.0
46
+ stroke_opacity: 1.0
47
+ corner_radius: 4.0
48
+ corner_radius_large: 8.0
49
+ dash_pattern_dotted: "2,2"
50
+ dash_pattern_dashed: "5,5"
51
+
52
+ spacing:
53
+ node_padding_x: 12.0
54
+ node_padding_y: 6.0
55
+ node_margin: 20.0
56
+ edge_spacing: 10.0
57
+ edge_label_offset: 5.0
58
+ rank_spacing: 100.0
59
+ node_spacing: 50.0
60
+
61
+ effects:
62
+ shadow_enabled: false
63
+ shadow_color: "#00000033"
64
+ shadow_blur: 4.0
65
+ shadow_offset_x: 2.0
66
+ shadow_offset_y: 2.0
67
+ gradient_enabled: false
68
+ gradient_type: "linear"
69
+ arrow_size: 10.0
70
+ arrow_style: "filled"
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ # Represents the color palette for diagram theming
6
+ class Sirena::Theme::ColorPalette < Lutaml::Model::Serializable
7
+ attribute :background, :string
8
+ attribute :surface, :string
9
+ attribute :surface_variant, :string
10
+ attribute :foreground, :string
11
+ attribute :foreground_secondary, :string
12
+ attribute :primary, :string
13
+ attribute :secondary, :string
14
+ attribute :accent, :string
15
+ attribute :error, :string
16
+ attribute :warning, :string
17
+ attribute :success, :string
18
+ attribute :info, :string
19
+ attribute :node_fill, :string
20
+ attribute :node_stroke, :string
21
+ attribute :edge_stroke, :string
22
+ attribute :label_text, :string
23
+ attribute :hover_fill, :string
24
+ attribute :active_fill, :string
25
+ attribute :disabled_fill, :string
26
+
27
+ yaml do
28
+ map "background", to: :background
29
+ map "surface", to: :surface
30
+ map "surface_variant", to: :surface_variant
31
+ map "foreground", to: :foreground
32
+ map "foreground_secondary", to: :foreground_secondary
33
+ map "primary", to: :primary
34
+ map "secondary", to: :secondary
35
+ map "accent", to: :accent
36
+ map "error", to: :error
37
+ map "warning", to: :warning
38
+ map "success", to: :success
39
+ map "info", to: :info
40
+ map "node_fill", to: :node_fill
41
+ map "node_stroke", to: :node_stroke
42
+ map "edge_stroke", to: :edge_stroke
43
+ map "label_text", to: :label_text
44
+ map "hover_fill", to: :hover_fill
45
+ map "active_fill", to: :active_fill
46
+ map "disabled_fill", to: :disabled_fill
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ # Represents effect styles for diagram theming
6
+ class Sirena::Theme::EffectStyles < Lutaml::Model::Serializable
7
+ attribute :shadow_enabled, :boolean
8
+ attribute :shadow_color, :string
9
+ attribute :shadow_blur, :float
10
+ attribute :shadow_offset_x, :float
11
+ attribute :shadow_offset_y, :float
12
+ attribute :gradient_enabled, :boolean
13
+ attribute :gradient_type, :string
14
+ attribute :arrow_size, :float
15
+ attribute :arrow_style, :string
16
+
17
+ yaml do
18
+ map "shadow_enabled", to: :shadow_enabled
19
+ map "shadow_color", to: :shadow_color
20
+ map "shadow_blur", to: :shadow_blur
21
+ map "shadow_offset_x", to: :shadow_offset_x
22
+ map "shadow_offset_y", to: :shadow_offset_y
23
+ map "gradient_enabled", to: :gradient_enabled
24
+ map "gradient_type", to: :gradient_type
25
+ map "arrow_size", to: :arrow_size
26
+ map "arrow_style", to: :arrow_style
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../theme"
4
+
5
+ # Registry for pre-defined and custom themes
6
+ class Sirena::Theme::Registry
7
+ @themes = {}
8
+
9
+ class << self
10
+ # Register a theme
11
+ def register(name, theme)
12
+ @themes[name.to_sym] = theme
13
+ end
14
+
15
+ # Get a theme by name
16
+ def get(name)
17
+ @themes[name.to_sym]
18
+ end
19
+
20
+ # List all registered theme names
21
+ def list
22
+ @themes.keys
23
+ end
24
+
25
+ # Load all built-in themes
26
+ def load_builtin_themes
27
+ builtin_dir = File.join(__dir__, "builtin")
28
+ return unless Dir.exist?(builtin_dir)
29
+
30
+ Dir.glob(File.join(builtin_dir, "*.yml")).each do |path|
31
+ theme = Sirena::Theme.load(path)
32
+ register(theme.name, theme)
33
+ end
34
+ end
35
+
36
+ # Clear all registered themes (useful for testing)
37
+ def clear
38
+ @themes = {}
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ # Represents shape styling settings for diagram theming
6
+ class Sirena::Theme::ShapeStyles < Lutaml::Model::Serializable
7
+ attribute :stroke_width, :float
8
+ attribute :stroke_width_thick, :float
9
+ attribute :stroke_width_thin, :float
10
+ attribute :fill_opacity, :float
11
+ attribute :stroke_opacity, :float
12
+ attribute :corner_radius, :float
13
+ attribute :corner_radius_large, :float
14
+ attribute :dash_pattern_dotted, :string
15
+ attribute :dash_pattern_dashed, :string
16
+
17
+ yaml do
18
+ map "stroke_width", to: :stroke_width
19
+ map "stroke_width_thick", to: :stroke_width_thick
20
+ map "stroke_width_thin", to: :stroke_width_thin
21
+ map "fill_opacity", to: :fill_opacity
22
+ map "stroke_opacity", to: :stroke_opacity
23
+ map "corner_radius", to: :corner_radius
24
+ map "corner_radius_large", to: :corner_radius_large
25
+ map "dash_pattern_dotted", to: :dash_pattern_dotted
26
+ map "dash_pattern_dashed", to: :dash_pattern_dashed
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ # Represents spacing configuration for diagram theming
6
+ class Sirena::Theme::SpacingConfig < Lutaml::Model::Serializable
7
+ attribute :node_padding_x, :float
8
+ attribute :node_padding_y, :float
9
+ attribute :node_margin, :float
10
+ attribute :edge_spacing, :float
11
+ attribute :edge_label_offset, :float
12
+ attribute :rank_spacing, :float
13
+ attribute :node_spacing, :float
14
+
15
+ yaml do
16
+ map "node_padding_x", to: :node_padding_x
17
+ map "node_padding_y", to: :node_padding_y
18
+ map "node_margin", to: :node_margin
19
+ map "edge_spacing", to: :edge_spacing
20
+ map "edge_label_offset", to: :edge_label_offset
21
+ map "rank_spacing", to: :rank_spacing
22
+ map "node_spacing", to: :node_spacing
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ # Represents typography settings for diagram theming
6
+ class Sirena::Theme::Typography < Lutaml::Model::Serializable
7
+ attribute :font_family, :string
8
+ attribute :font_family_monospace, :string
9
+ attribute :font_size_small, :float
10
+ attribute :font_size_normal, :float
11
+ attribute :font_size_large, :float
12
+ attribute :font_size_title, :float
13
+ attribute :font_weight_normal, :string
14
+ attribute :font_weight_bold, :string
15
+ attribute :line_height, :float
16
+ attribute :text_transform, :string
17
+
18
+ yaml do
19
+ map "font_family", to: :font_family
20
+ map "font_family_monospace", to: :font_family_monospace
21
+ map "font_size_small", to: :font_size_small
22
+ map "font_size_normal", to: :font_size_normal
23
+ map "font_size_large", to: :font_size_large
24
+ map "font_size_title", to: :font_size_title
25
+ map "font_weight_normal", to: :font_weight_normal
26
+ map "font_weight_bold", to: :font_weight_bold
27
+ map "line_height", to: :line_height
28
+ map "text_transform", to: :text_transform
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Sirena
6
+ # Represents a complete visual theme for diagram rendering
7
+ class Theme < Lutaml::Model::Serializable
8
+ end
9
+ end
10
+
11
+ # Now load sub-models after Theme class is defined
12
+ require_relative "theme/color_palette"
13
+ require_relative "theme/typography"
14
+ require_relative "theme/shape_styles"
15
+ require_relative "theme/spacing_config"
16
+ require_relative "theme/effect_styles"
17
+
18
+ module Sirena
19
+ class Theme
20
+ attribute :name, :string
21
+ attribute :description, :string
22
+ attribute :colors, ColorPalette
23
+ attribute :typography, Typography
24
+ attribute :shapes, ShapeStyles
25
+ attribute :spacing, SpacingConfig
26
+ attribute :effects, EffectStyles
27
+
28
+ yaml do
29
+ map "name", to: :name
30
+ map "description", to: :description
31
+ map "colors", to: :colors
32
+ map "typography", to: :typography
33
+ map "shapes", to: :shapes
34
+ map "spacing", to: :spacing
35
+ map "effects", to: :effects
36
+ end
37
+
38
+ # Load theme from YAML file
39
+ def self.load(path)
40
+ yaml_content = File.read(path)
41
+ from_yaml(yaml_content)
42
+ end
43
+
44
+ # Merge with another theme (for overrides)
45
+ def merge(other_theme)
46
+ merged = self.class.new(
47
+ name: other_theme.name || name,
48
+ description: other_theme.description || description,
49
+ colors: merge_attribute(colors, other_theme.colors),
50
+ typography: merge_attribute(typography, other_theme.typography),
51
+ shapes: merge_attribute(shapes, other_theme.shapes),
52
+ spacing: merge_attribute(spacing, other_theme.spacing),
53
+ effects: merge_attribute(effects, other_theme.effects)
54
+ )
55
+ merged
56
+ end
57
+
58
+ private
59
+
60
+ def merge_attribute(base, override)
61
+ return base if override.nil?
62
+ return override if base.nil?
63
+
64
+ # For now, just use override if provided, base otherwise
65
+ # Full deep merge would require iterating over attributes
66
+ override
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../diagram/architecture"
5
+
6
+ module Sirena
7
+ module Transform
8
+ # Architecture diagram transformer for converting architecture models to positioned layouts
9
+ class ArchitectureTransform < Base
10
+ # Default dimensions
11
+ DEFAULT_SERVICE_WIDTH = 120
12
+ DEFAULT_SERVICE_HEIGHT = 80
13
+ DEFAULT_GROUP_PADDING = 30
14
+ DEFAULT_SPACING = 40
15
+ DEFAULT_ICON_SIZE = 24
16
+
17
+ # Converts an architecture diagram to a positioned layout structure
18
+ #
19
+ # @param diagram [Diagram::ArchitectureDiagram] the diagram to transform
20
+ # @return [Hash] positioned layout hash
21
+ # @raise [TransformError] if diagram is invalid
22
+ def to_graph(diagram)
23
+ raise TransformError, "Diagram cannot be nil" if diagram.nil?
24
+
25
+ # Build hierarchy
26
+ hierarchy = build_hierarchy(diagram)
27
+
28
+ # Calculate positions
29
+ service_positions = position_services(diagram, hierarchy)
30
+ group_bounds = calculate_group_bounds(diagram, service_positions)
31
+ edge_positions = position_edges(diagram, service_positions)
32
+
33
+ {
34
+ services: service_positions,
35
+ groups: group_bounds,
36
+ edges: edge_positions,
37
+ width: calculate_total_width(service_positions, group_bounds),
38
+ height: calculate_total_height(service_positions, group_bounds),
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def build_hierarchy(diagram)
45
+ hierarchy = {
46
+ groups: {},
47
+ services_by_group: {},
48
+ }
49
+
50
+ # Map groups
51
+ diagram.groups.each do |group|
52
+ parent_id = group.parent_id || :root
53
+ hierarchy[:groups][group.id] = {
54
+ group: group,
55
+ parent_id: parent_id,
56
+ children: [],
57
+ }
58
+ end
59
+
60
+ # Build parent-child relationships for groups
61
+ diagram.groups.each do |group|
62
+ if group.parent_id && hierarchy[:groups][group.parent_id]
63
+ hierarchy[:groups][group.parent_id][:children] << group.id
64
+ end
65
+ end
66
+
67
+ # Map services to groups
68
+ diagram.services.each do |service|
69
+ group_id = service.group_id || :root
70
+ hierarchy[:services_by_group][group_id] ||= []
71
+ hierarchy[:services_by_group][group_id] << service
72
+ end
73
+
74
+ hierarchy
75
+ end
76
+
77
+ def position_services(diagram, hierarchy)
78
+ positions = {}
79
+ current_x = DEFAULT_SPACING
80
+ current_y = DEFAULT_SPACING
81
+
82
+ # Group services by their group
83
+ groups_to_layout = [:root] + diagram.groups.map(&:id)
84
+
85
+ groups_to_layout.each do |group_id|
86
+ services = hierarchy[:services_by_group][group_id] || []
87
+ next if services.empty?
88
+
89
+ # Position services in this group
90
+ services.each_with_index do |service, index|
91
+ dims = calculate_service_dimensions(service)
92
+
93
+ positions[service.id] = {
94
+ service: service,
95
+ x: current_x,
96
+ y: current_y,
97
+ width: dims[:width],
98
+ height: dims[:height],
99
+ group_id: group_id,
100
+ }
101
+
102
+ current_x += dims[:width] + DEFAULT_SPACING
103
+ end
104
+
105
+ # Move to next row
106
+ current_x = DEFAULT_SPACING
107
+ current_y += DEFAULT_SERVICE_HEIGHT + DEFAULT_SPACING * 2
108
+ end
109
+
110
+ # Apply edge-based adjustments if needed
111
+ adjust_positions_for_edges(diagram, positions)
112
+
113
+ positions
114
+ end
115
+
116
+ def adjust_positions_for_edges(diagram, positions)
117
+ # This is a simple adjustment - could be enhanced with force-directed layout
118
+ diagram.edges.each do |edge|
119
+ from = positions[edge.from_id]
120
+ to = positions[edge.to_id]
121
+ next unless from && to
122
+
123
+ # If positions suggest directional hints, try to respect them
124
+ if edge.from_position == "R" && edge.to_position == "L"
125
+ # From should be to the left of to
126
+ if from[:x] > to[:x]
127
+ temp_x = from[:x]
128
+ from[:x] = to[:x]
129
+ to[:x] = temp_x
130
+ end
131
+ elsif edge.from_position == "L" && edge.to_position == "R"
132
+ # From should be to the right of to
133
+ if from[:x] < to[:x]
134
+ temp_x = from[:x]
135
+ from[:x] = to[:x]
136
+ to[:x] = temp_x
137
+ end
138
+ elsif edge.from_position == "T" && edge.to_position == "B"
139
+ # From should be above to
140
+ if from[:y] > to[:y]
141
+ temp_y = from[:y]
142
+ from[:y] = to[:y]
143
+ to[:y] = temp_y
144
+ end
145
+ elsif edge.from_position == "B" && edge.to_position == "T"
146
+ # From should be below to
147
+ if from[:y] < to[:y]
148
+ temp_y = from[:y]
149
+ from[:y] = to[:y]
150
+ to[:y] = temp_y
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ def calculate_service_dimensions(service)
157
+ label = service.label || service.id
158
+ label_dims = measure_text(label, font_size: 14)
159
+
160
+ width = [label_dims[:width] + 40, DEFAULT_SERVICE_WIDTH].max
161
+ height = DEFAULT_SERVICE_HEIGHT
162
+
163
+ {
164
+ width: width,
165
+ height: height,
166
+ }
167
+ end
168
+
169
+ def calculate_group_bounds(diagram, service_positions)
170
+ bounds = {}
171
+
172
+ diagram.groups.each do |group|
173
+ # Find all services in this group
174
+ group_services = service_positions.values.select do |pos|
175
+ pos[:group_id] == group.id
176
+ end
177
+
178
+ # Find all child groups
179
+ child_groups = diagram.groups.select { |g| g.parent_id == group.id }
180
+
181
+ if group_services.empty? && child_groups.empty?
182
+ next
183
+ end
184
+
185
+ # Calculate bounding box
186
+ if group_services.any?
187
+ min_x = group_services.map { |s| s[:x] }.min
188
+ min_y = group_services.map { |s| s[:y] }.min
189
+ max_x = group_services.map { |s| s[:x] + s[:width] }.max
190
+ max_y = group_services.map { |s| s[:y] + s[:height] }.max
191
+ else
192
+ # Use child group bounds
193
+ min_x = Float::INFINITY
194
+ min_y = Float::INFINITY
195
+ max_x = -Float::INFINITY
196
+ max_y = -Float::INFINITY
197
+
198
+ child_groups.each do |child|
199
+ child_bounds = bounds[child.id]
200
+ next unless child_bounds
201
+
202
+ min_x = [min_x, child_bounds[:x]].min
203
+ min_y = [min_y, child_bounds[:y]].min
204
+ max_x = [max_x, child_bounds[:x] + child_bounds[:width]].max
205
+ max_y = [max_y, child_bounds[:y] + child_bounds[:height]].max
206
+ end
207
+ end
208
+
209
+ bounds[group.id] = {
210
+ group: group,
211
+ x: min_x - DEFAULT_GROUP_PADDING,
212
+ y: min_y - DEFAULT_GROUP_PADDING,
213
+ width: max_x - min_x + DEFAULT_GROUP_PADDING * 2,
214
+ height: max_y - min_y + DEFAULT_GROUP_PADDING * 2,
215
+ }
216
+ end
217
+
218
+ bounds
219
+ end
220
+
221
+ def position_edges(diagram, service_positions)
222
+ diagram.edges.map do |edge|
223
+ from = service_positions[edge.from_id]
224
+ to = service_positions[edge.to_id]
225
+
226
+ next unless from && to
227
+
228
+ # Calculate connection points based on position hints
229
+ from_point = calculate_connection_point(from, edge.from_position || "R")
230
+ to_point = calculate_connection_point(to, edge.to_position || "L")
231
+
232
+ {
233
+ edge: edge,
234
+ from_x: from_point[:x],
235
+ from_y: from_point[:y],
236
+ to_x: to_point[:x],
237
+ to_y: to_point[:y],
238
+ }
239
+ end.compact
240
+ end
241
+
242
+ def calculate_connection_point(service_pos, position)
243
+ case position
244
+ when "L"
245
+ { x: service_pos[:x], y: service_pos[:y] + service_pos[:height] / 2 }
246
+ when "R"
247
+ { x: service_pos[:x] + service_pos[:width], y: service_pos[:y] + service_pos[:height] / 2 }
248
+ when "T"
249
+ { x: service_pos[:x] + service_pos[:width] / 2, y: service_pos[:y] }
250
+ when "B"
251
+ { x: service_pos[:x] + service_pos[:width] / 2, y: service_pos[:y] + service_pos[:height] }
252
+ else
253
+ # Default to right
254
+ { x: service_pos[:x] + service_pos[:width], y: service_pos[:y] + service_pos[:height] / 2 }
255
+ end
256
+ end
257
+
258
+ def calculate_total_width(service_positions, group_bounds)
259
+ max_service_x = service_positions.values.map { |s| s[:x] + s[:width] }.max || 0
260
+ max_group_x = group_bounds.values.map { |g| g[:x] + g[:width] }.max || 0
261
+
262
+ [max_service_x, max_group_x].max + DEFAULT_SPACING
263
+ end
264
+
265
+ def calculate_total_height(service_positions, group_bounds)
266
+ max_service_y = service_positions.values.map { |s| s[:y] + s[:height] }.max || 0
267
+ max_group_y = group_bounds.values.map { |g| g[:y] + g[:height] }.max || 0
268
+
269
+ [max_service_y, max_group_y].max + DEFAULT_SPACING
270
+ end
271
+ end
272
+ end
273
+ end