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,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sirena
4
+ module Transform
5
+ # Abstract base class for diagram transformers.
6
+ #
7
+ # Transformers convert typed diagram models into generic graph structures
8
+ # suitable for layout computation by elkrb. Each diagram type has its own
9
+ # transformer that maps diagram-specific elements to graph nodes and edges.
10
+ #
11
+ # The transformer is also responsible for calculating node dimensions
12
+ # using TextMeasurement and setting appropriate layout options.
13
+ #
14
+ # @example Define a custom transformer
15
+ # class FlowchartTransform < Transform::Base
16
+ # def to_graph(diagram)
17
+ # graph = create_graph
18
+ # # Add nodes and edges based on diagram structure
19
+ # graph
20
+ # end
21
+ # end
22
+ #
23
+ # @abstract Subclass and implement #to_graph
24
+ class Base
25
+ # ELK layout algorithms supported (matching mermaid-js)
26
+ # @see https://www.eclipse.org/elk/reference/algorithms.html
27
+ ALGORITHM_LAYERED = 'layered'
28
+ ALGORITHM_STRESS = 'stress'
29
+ ALGORITHM_FORCE = 'force'
30
+ ALGORITHM_MRTREE = 'mrtree'
31
+ ALGORITHM_SPORE_OVERLAP = 'sporeOverlap'
32
+
33
+ # ELK layout directions
34
+ DIRECTION_DOWN = 'DOWN'
35
+ DIRECTION_UP = 'UP'
36
+ DIRECTION_LEFT = 'LEFT'
37
+ DIRECTION_RIGHT = 'RIGHT'
38
+
39
+ # Default spacing values (in pixels)
40
+ DEFAULT_NODE_SPACING = 50
41
+ DEFAULT_EDGE_SPACING = 30
42
+ DEFAULT_LAYER_SPACING = 50
43
+
44
+ # ELK option keys for consistent configuration
45
+ # @see https://www.eclipse.org/elk/reference/options.html
46
+ module ElkOptions
47
+ ALGORITHM = 'algorithm'
48
+ DIRECTION = 'elk.direction'
49
+
50
+ # Spacing options
51
+ NODE_NODE_SPACING = 'elk.spacing.nodeNode'
52
+ EDGE_NODE_SPACING = 'elk.spacing.edgeNode'
53
+ EDGE_EDGE_SPACING = 'elk.spacing.edgeEdge'
54
+ LAYER_SPACING = 'elk.layered.spacing.nodeNodeBetweenLayers'
55
+
56
+ # Layered algorithm options
57
+ NODE_PLACEMENT = 'elk.layered.nodePlacement.strategy'
58
+ CROSSING_MINIMIZATION =
59
+ 'elk.layered.crossingMinimization.strategy'
60
+ MODEL_ORDER = 'elk.layered.considerModelOrder.strategy'
61
+ COMPACTION = 'elk.layered.compaction.postCompaction.strategy'
62
+
63
+ # Hierarchy and grouping
64
+ HIERARCHY_HANDLING = 'elk.hierarchyHandling'
65
+
66
+ # Edge routing
67
+ EDGE_ROUTING = 'elk.edgeRouting'
68
+ end
69
+
70
+ # Converts a diagram model to an elkrb graph structure.
71
+ #
72
+ # This method should be overridden by subclasses to implement
73
+ # diagram-specific graph conversion logic.
74
+ #
75
+ # @param diagram [Diagram::Base] the diagram model to convert
76
+ # @return [Object] elkrb graph object with nodes and edges
77
+ # @raise [NotImplementedError] if not implemented by subclass
78
+ def to_graph(diagram)
79
+ raise NotImplementedError,
80
+ "#{self.class} must implement #to_graph(diagram)"
81
+ end
82
+
83
+ protected
84
+
85
+ # Measures text dimensions for node sizing.
86
+ #
87
+ # @param text [String] the text to measure
88
+ # @param font_size [Numeric] the font size in points
89
+ # @param width [Numeric, nil] optional width override
90
+ # @param height [Numeric, nil] optional height override
91
+ # @return [Hash] hash with :width and :height keys
92
+ def measure_text(text, font_size:, width: nil, height: nil)
93
+ TextMeasurement.measure(text,
94
+ font_size: font_size,
95
+ width: width,
96
+ height: height)
97
+ end
98
+
99
+ # Creates ELK layout options with proper configuration.
100
+ #
101
+ # This method provides sensible defaults based on mermaid-js patterns
102
+ # and can be overridden by subclasses for diagram-specific needs.
103
+ #
104
+ # @param algorithm [String] ELK algorithm to use
105
+ # @param direction [String] layout direction
106
+ # @param options [Hash] additional ELK options to merge
107
+ # @return [Hash] complete ELK layout options
108
+ def build_elk_options(algorithm: ALGORITHM_LAYERED,
109
+ direction: DIRECTION_DOWN,
110
+ **options)
111
+ base_options = {
112
+ ElkOptions::ALGORITHM => algorithm,
113
+ ElkOptions::DIRECTION => direction
114
+ }
115
+
116
+ # Add algorithm-specific defaults
117
+ case algorithm
118
+ when ALGORITHM_LAYERED
119
+ base_options.merge!(layered_algorithm_options)
120
+ when ALGORITHM_STRESS, ALGORITHM_FORCE
121
+ base_options.merge!(force_based_algorithm_options)
122
+ end
123
+
124
+ base_options.merge(options)
125
+ end
126
+
127
+ # Default layout options for layered algorithm.
128
+ #
129
+ # The layered algorithm is optimal for hierarchical diagrams like
130
+ # flowcharts, sequence diagrams, and class diagrams. It minimizes
131
+ # edge crossings and places nodes in distinct layers.
132
+ #
133
+ # @return [Hash] layered algorithm options
134
+ def layered_algorithm_options
135
+ {
136
+ # Node and edge spacing
137
+ ElkOptions::NODE_NODE_SPACING => DEFAULT_NODE_SPACING,
138
+ ElkOptions::EDGE_NODE_SPACING => DEFAULT_EDGE_SPACING,
139
+ ElkOptions::EDGE_EDGE_SPACING => DEFAULT_EDGE_SPACING,
140
+ ElkOptions::LAYER_SPACING => DEFAULT_LAYER_SPACING,
141
+
142
+ # Use SIMPLE node placement for predictable layouts
143
+ ElkOptions::NODE_PLACEMENT => 'SIMPLE',
144
+
145
+ # Consider model order for consistent positioning
146
+ ElkOptions::MODEL_ORDER => 'NODES_AND_EDGES'
147
+ }
148
+ end
149
+
150
+ # Default layout options for force-based algorithms.
151
+ #
152
+ # Force-based algorithms (stress, force) are optimal for graphs
153
+ # without clear hierarchy, like ER diagrams or network diagrams.
154
+ #
155
+ # @return [Hash] force-based algorithm options
156
+ def force_based_algorithm_options
157
+ {
158
+ ElkOptions::NODE_NODE_SPACING => DEFAULT_NODE_SPACING * 1.5,
159
+ ElkOptions::EDGE_NODE_SPACING => DEFAULT_EDGE_SPACING,
160
+ ElkOptions::EDGE_EDGE_SPACING => DEFAULT_EDGE_SPACING
161
+ }
162
+ end
163
+
164
+ # Calculates node padding based on content type.
165
+ #
166
+ # @param node_type [Symbol] the type of node
167
+ # @return [Hash] padding hash with :top, :bottom, :left, :right
168
+ def node_padding(node_type)
169
+ case node_type
170
+ when :rect
171
+ { top: 10, bottom: 10, left: 15, right: 15 }
172
+ when :circle
173
+ { top: 15, bottom: 15, left: 15, right: 15 }
174
+ when :diamond
175
+ { top: 20, bottom: 20, left: 20, right: 20 }
176
+ else
177
+ { top: 10, bottom: 10, left: 10, right: 10 }
178
+ end
179
+ end
180
+
181
+ # Calculates total node dimensions including padding.
182
+ #
183
+ # @param content_width [Numeric] width of node content
184
+ # @param content_height [Numeric] height of node content
185
+ # @param node_type [Symbol] the type of node
186
+ # @return [Hash] dimensions hash with :width and :height
187
+ def calculate_node_dimensions(content_width, content_height, node_type)
188
+ padding = node_padding(node_type)
189
+ {
190
+ width: content_width + padding[:left] + padding[:right],
191
+ height: content_height + padding[:top] + padding[:bottom]
192
+ }
193
+ end
194
+ end
195
+
196
+ # Error raised during transformation.
197
+ class TransformError < StandardError; end
198
+ end
199
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/block'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # Block diagram transformer for converting block models to positioned layouts.
9
+ #
10
+ # Converts a typed block diagram model into a column-based layout structure.
11
+ # Handles block dimension calculation, column-based positioning, and
12
+ # connection routing.
13
+ #
14
+ # @example Transform a block diagram
15
+ # transform = BlockTransform.new
16
+ # layout = transform.to_layout(block_diagram)
17
+ class BlockTransform < Base
18
+ # Default dimensions
19
+ DEFAULT_BLOCK_WIDTH = 100
20
+ DEFAULT_BLOCK_HEIGHT = 60
21
+ DEFAULT_SPACING = 20
22
+ DEFAULT_COMPOUND_PADDING = 20
23
+
24
+ # Converts a block diagram to a positioned layout structure.
25
+ #
26
+ # @param diagram [Diagram::BlockDiagram] the block diagram to transform
27
+ # @return [Hash] positioned layout hash
28
+ # @raise [TransformError] if diagram is invalid
29
+ def to_graph(diagram)
30
+ raise TransformError, 'Diagram cannot be nil' if diagram.nil?
31
+
32
+ blocks_layout = calculate_column_layout(diagram)
33
+ connections_layout = calculate_connections(diagram, blocks_layout)
34
+
35
+ {
36
+ blocks: blocks_layout,
37
+ connections: connections_layout,
38
+ columns: diagram.columns,
39
+ width: calculate_total_width(blocks_layout, diagram.columns),
40
+ height: calculate_total_height(blocks_layout)
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def calculate_column_layout(diagram)
47
+ columns = diagram.columns
48
+ blocks = diagram.blocks
49
+ positioned_blocks = {}
50
+
51
+ current_row = 0
52
+ current_col = 0
53
+ row_heights = []
54
+ col_widths = Array.new(columns, 0)
55
+
56
+ blocks.each do |block|
57
+ # Handle space blocks
58
+ if block.space?
59
+ current_col += 1
60
+ if current_col >= columns
61
+ current_col = 0
62
+ current_row += 1
63
+ end
64
+ next
65
+ end
66
+
67
+ # Calculate block dimensions
68
+ dims = calculate_block_dimensions(block)
69
+ block_width = block.width || 1
70
+
71
+ # Check if block fits in current row
72
+ if current_col + block_width > columns
73
+ current_col = 0
74
+ current_row += 1
75
+ end
76
+
77
+ # Position block
78
+ x = calculate_x_position(current_col, col_widths)
79
+ y = calculate_y_position(current_row, row_heights)
80
+
81
+ positioned_blocks[block.id] = {
82
+ block: block,
83
+ x: x,
84
+ y: y,
85
+ width: dims[:width] * block_width,
86
+ height: dims[:height],
87
+ row: current_row,
88
+ col: current_col,
89
+ col_span: block_width
90
+ }
91
+
92
+ # Update column widths
93
+ (current_col...current_col + block_width).each do |col|
94
+ col_widths[col] = [col_widths[col], dims[:width]].max if col < columns
95
+ end
96
+
97
+ # Update row height
98
+ row_heights[current_row] = [row_heights[current_row] || 0, dims[:height]].max
99
+
100
+ # Handle compound blocks
101
+ if block.compound? && !block.children.empty?
102
+ child_layout = layout_compound_children(block, x, y, dims)
103
+ positioned_blocks.merge!(child_layout)
104
+ end
105
+
106
+ # Move to next position
107
+ current_col += block_width
108
+ if current_col >= columns
109
+ current_col = 0
110
+ current_row += 1
111
+ end
112
+ end
113
+
114
+ positioned_blocks
115
+ end
116
+
117
+ def layout_compound_children(parent_block, parent_x, parent_y, parent_dims)
118
+ positioned = {}
119
+ child_y = parent_y + DEFAULT_COMPOUND_PADDING
120
+
121
+ parent_block.children.each_with_index do |child, index|
122
+ child_dims = calculate_block_dimensions(child)
123
+
124
+ positioned[child.id] = {
125
+ block: child,
126
+ x: parent_x + DEFAULT_COMPOUND_PADDING,
127
+ y: child_y,
128
+ width: child_dims[:width],
129
+ height: child_dims[:height],
130
+ parent_id: parent_block.id
131
+ }
132
+
133
+ child_y += child_dims[:height] + DEFAULT_SPACING
134
+ end
135
+
136
+ positioned
137
+ end
138
+
139
+ def calculate_block_dimensions(block)
140
+ if block.arrow?
141
+ return {
142
+ width: DEFAULT_BLOCK_WIDTH / 2,
143
+ height: DEFAULT_BLOCK_HEIGHT / 2
144
+ }
145
+ end
146
+
147
+ label = block.label || block.id
148
+ label_dims = measure_text(label, font_size: 14)
149
+
150
+ # Add padding
151
+ width = [label_dims[:width] + 40, DEFAULT_BLOCK_WIDTH].max
152
+ height = [label_dims[:height] + 30, DEFAULT_BLOCK_HEIGHT].max
153
+
154
+ if block.compound?
155
+ # Compound blocks need more space
156
+ child_height = block.children.reduce(0) do |sum, child|
157
+ child_dims = calculate_block_dimensions(child)
158
+ sum + child_dims[:height] + DEFAULT_SPACING
159
+ end
160
+ height = [height, child_height + DEFAULT_COMPOUND_PADDING * 2].max
161
+ end
162
+
163
+ {
164
+ width: width,
165
+ height: height
166
+ }
167
+ end
168
+
169
+ def calculate_x_position(col, col_widths)
170
+ return DEFAULT_SPACING if col == 0
171
+
172
+ col_widths[0...col].sum + (DEFAULT_SPACING * (col + 1))
173
+ end
174
+
175
+ def calculate_y_position(row, row_heights)
176
+ return DEFAULT_SPACING if row == 0
177
+
178
+ row_heights[0...row].sum + (DEFAULT_SPACING * (row + 1))
179
+ end
180
+
181
+ def calculate_connections(diagram, blocks_layout)
182
+ diagram.connections.map do |conn|
183
+ from_block = blocks_layout[conn.from]
184
+ to_block = blocks_layout[conn.to]
185
+
186
+ next unless from_block && to_block
187
+
188
+ {
189
+ from: conn.from,
190
+ to: conn.to,
191
+ from_x: from_block[:x] + from_block[:width] / 2,
192
+ from_y: from_block[:y] + from_block[:height],
193
+ to_x: to_block[:x] + to_block[:width] / 2,
194
+ to_y: to_block[:y],
195
+ connection_type: conn.connection_type
196
+ }
197
+ end.compact
198
+ end
199
+
200
+ def calculate_total_width(blocks_layout, columns)
201
+ return DEFAULT_SPACING * 2 if blocks_layout.empty?
202
+
203
+ max_x = blocks_layout.values.map { |b| b[:x] + b[:width] }.max || 0
204
+ max_x + DEFAULT_SPACING
205
+ end
206
+
207
+ def calculate_total_height(blocks_layout)
208
+ return DEFAULT_SPACING * 2 if blocks_layout.empty?
209
+
210
+ max_y = blocks_layout.values.map { |b| b[:y] + b[:height] }.max || 0
211
+ max_y + DEFAULT_SPACING
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../diagram/c4'
5
+
6
+ module Sirena
7
+ module Transform
8
+ # C4 transformer for converting C4 models to graphs.
9
+ #
10
+ # Converts a typed C4 diagram model into a generic graph structure
11
+ # suitable for layout computation by elkrb. Handles element positioning,
12
+ # boundary grouping, and relationship routing.
13
+ #
14
+ # @example Transform a C4 diagram
15
+ # transform = C4Transform.new
16
+ # graph = transform.to_graph(c4_diagram)
17
+ class C4Transform < Base
18
+ # Default font size for text measurement
19
+ DEFAULT_FONT_SIZE = 14
20
+
21
+ # Element dimensions based on type
22
+ PERSON_WIDTH = 140
23
+ PERSON_HEIGHT = 180
24
+ SYSTEM_WIDTH = 160
25
+ SYSTEM_HEIGHT = 120
26
+ CONTAINER_WIDTH = 160
27
+ CONTAINER_HEIGHT = 120
28
+ COMPONENT_WIDTH = 160
29
+ COMPONENT_HEIGHT = 100
30
+
31
+ # Spacing
32
+ ELEMENT_SPACING = 60
33
+ BOUNDARY_PADDING = 40
34
+ LEVEL_SPACING = 80
35
+
36
+ # Converts a C4 diagram to a graph structure.
37
+ #
38
+ # @param diagram [Diagram::C4] the C4 diagram to transform
39
+ # @return [Hash] elkrb-compatible graph hash
40
+ # @raise [TransformError] if diagram is invalid
41
+ def to_graph(diagram)
42
+ raise TransformError, 'Invalid diagram' unless diagram.valid?
43
+
44
+ # Build hierarchy with boundaries as containers
45
+ root_elements = diagram.elements.select { |e| e.boundary_id.nil? }
46
+ root_boundaries = diagram.boundaries.select { |b| b.parent_id.nil? }
47
+
48
+ {
49
+ id: diagram.id || 'c4',
50
+ children: transform_root_nodes(diagram, root_elements,
51
+ root_boundaries),
52
+ edges: transform_relationships(diagram),
53
+ layoutOptions: layout_options(diagram),
54
+ metadata: {
55
+ level: diagram.level,
56
+ title: diagram.title,
57
+ element_count: diagram.elements.length,
58
+ relationship_count: diagram.relationships.length
59
+ }
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def transform_root_nodes(diagram, elements, boundaries)
66
+ nodes = []
67
+
68
+ # Add root-level boundaries (which contain their own elements)
69
+ boundaries.each do |boundary|
70
+ nodes << transform_boundary(diagram, boundary)
71
+ end
72
+
73
+ # Add root-level elements (not in any boundary)
74
+ elements.each do |element|
75
+ nodes << transform_element(element)
76
+ end
77
+
78
+ nodes
79
+ end
80
+
81
+ def transform_boundary(diagram, boundary)
82
+ # Get elements in this boundary
83
+ elements = diagram.elements_in_boundary(boundary.id)
84
+ child_boundaries = diagram.boundaries_in_boundary(boundary.id)
85
+
86
+ children = []
87
+
88
+ # Add child boundaries first
89
+ child_boundaries.each do |child_boundary|
90
+ children << transform_boundary(diagram, child_boundary)
91
+ end
92
+
93
+ # Add elements in this boundary
94
+ elements.each do |element|
95
+ children << transform_element(element)
96
+ end
97
+
98
+ # Calculate boundary dimensions based on contents
99
+ dims = calculate_boundary_dimensions(children)
100
+
101
+ {
102
+ id: boundary.id,
103
+ width: dims[:width],
104
+ height: dims[:height],
105
+ labels: [
106
+ {
107
+ text: boundary.label,
108
+ width: measure_text(boundary.label,
109
+ font_size: DEFAULT_FONT_SIZE + 2)[:width],
110
+ height: measure_text(boundary.label,
111
+ font_size: DEFAULT_FONT_SIZE + 2)[:height]
112
+ }
113
+ ],
114
+ children: children,
115
+ layoutOptions: boundary_layout_options,
116
+ metadata: {
117
+ boundary_type: boundary.boundary_type,
118
+ type_param: boundary.type_param,
119
+ link: boundary.link,
120
+ tags: boundary.tags
121
+ }
122
+ }
123
+ end
124
+
125
+ def transform_element(element)
126
+ dims = element_dimensions(element)
127
+
128
+ labels = []
129
+
130
+ # Main label
131
+ label_dims = measure_text(element.label,
132
+ font_size: DEFAULT_FONT_SIZE + 2)
133
+ labels << {
134
+ text: element.label,
135
+ width: label_dims[:width],
136
+ height: label_dims[:height]
137
+ }
138
+
139
+ # Description (if present)
140
+ if element.description && !element.description.empty?
141
+ desc_dims = measure_text(element.description,
142
+ font_size: DEFAULT_FONT_SIZE - 2)
143
+ labels << {
144
+ text: element.description,
145
+ width: desc_dims[:width],
146
+ height: desc_dims[:height]
147
+ }
148
+ end
149
+
150
+ # Technology (if present)
151
+ if element.technology && !element.technology.empty?
152
+ tech_dims = measure_text(element.technology,
153
+ font_size: DEFAULT_FONT_SIZE - 2)
154
+ labels << {
155
+ text: "[#{element.technology}]",
156
+ width: tech_dims[:width],
157
+ height: tech_dims[:height]
158
+ }
159
+ end
160
+
161
+ {
162
+ id: element.id,
163
+ width: dims[:width],
164
+ height: dims[:height],
165
+ labels: labels,
166
+ metadata: {
167
+ element_type: element.element_type,
168
+ base_type: element.base_type,
169
+ external: element.external,
170
+ sprite: element.sprite,
171
+ link: element.link,
172
+ tags: element.tags,
173
+ person: element.person?,
174
+ system: element.system?,
175
+ container: element.container?,
176
+ component: element.component?
177
+ }
178
+ }
179
+ end
180
+
181
+ def transform_relationships(diagram)
182
+ return [] if diagram.relationships.nil? || diagram.relationships.empty?
183
+
184
+ diagram.relationships.map.with_index do |rel, index|
185
+ labels = []
186
+
187
+ if rel.label && !rel.label.empty?
188
+ label_dims = measure_text(rel.label, font_size: DEFAULT_FONT_SIZE)
189
+ labels << {
190
+ text: rel.label,
191
+ width: label_dims[:width],
192
+ height: label_dims[:height]
193
+ }
194
+ end
195
+
196
+ if rel.technology && !rel.technology.empty?
197
+ tech_dims = measure_text("[#{rel.technology}]",
198
+ font_size: DEFAULT_FONT_SIZE - 2)
199
+ labels << {
200
+ text: "[#{rel.technology}]",
201
+ width: tech_dims[:width],
202
+ height: tech_dims[:height]
203
+ }
204
+ end
205
+
206
+ {
207
+ id: "rel_#{index}",
208
+ sources: [rel.from_id],
209
+ targets: [rel.to_id],
210
+ labels: labels,
211
+ metadata: {
212
+ rel_type: rel.rel_type,
213
+ bidirectional: rel.bidirectional?
214
+ }
215
+ }
216
+ end
217
+ end
218
+
219
+ def element_dimensions(element)
220
+ # Base dimensions on element type
221
+ if element.person?
222
+ { width: PERSON_WIDTH, height: PERSON_HEIGHT }
223
+ elsif element.system?
224
+ { width: SYSTEM_WIDTH, height: SYSTEM_HEIGHT }
225
+ elsif element.container?
226
+ { width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }
227
+ elsif element.component?
228
+ { width: COMPONENT_WIDTH, height: COMPONENT_HEIGHT }
229
+ else
230
+ { width: SYSTEM_WIDTH, height: SYSTEM_HEIGHT }
231
+ end
232
+ end
233
+
234
+ def calculate_boundary_dimensions(children)
235
+ return { width: 300, height: 200 } if children.empty?
236
+
237
+ # Calculate based on child count and type
238
+ # Simple heuristic: arrange in grid
239
+ count = children.length
240
+ cols = Math.sqrt(count).ceil
241
+ rows = (count.to_f / cols).ceil
242
+
243
+ max_width = children.map { |c| c[:width] || 160 }.max
244
+ max_height = children.map { |c| c[:height] || 120 }.max
245
+
246
+ width = (cols * max_width) + ((cols + 1) * ELEMENT_SPACING) +
247
+ (2 * BOUNDARY_PADDING)
248
+ height = (rows * max_height) + ((rows + 1) * ELEMENT_SPACING) +
249
+ (2 * BOUNDARY_PADDING) + 30 # Extra for title
250
+
251
+ { width: [width, 300].max, height: [height, 200].max }
252
+ end
253
+
254
+ def layout_options(diagram)
255
+ # C4 diagrams use hierarchical layout
256
+ # Top-down for Context/Container, can be left-right for Component
257
+ direction = case diagram.level
258
+ when 'Component', 'Code'
259
+ DIRECTION_RIGHT
260
+ else
261
+ DIRECTION_DOWN
262
+ end
263
+
264
+ build_elk_options(
265
+ algorithm: ALGORITHM_LAYERED,
266
+ direction: direction,
267
+ ElkOptions::NODE_NODE_SPACING => ELEMENT_SPACING,
268
+ ElkOptions::LAYER_SPACING => LEVEL_SPACING,
269
+ ElkOptions::EDGE_NODE_SPACING => 25,
270
+ ElkOptions::EDGE_EDGE_SPACING => 20,
271
+ ElkOptions::HIERARCHY_HANDLING => 'INCLUDE_CHILDREN',
272
+ ElkOptions::NODE_PLACEMENT => 'NETWORK_SIMPLEX'
273
+ )
274
+ end
275
+
276
+ def boundary_layout_options
277
+ # Boundaries use box packing for internal layout
278
+ {
279
+ 'elk.algorithm' => 'box',
280
+ 'elk.box.packingMode' => 'GROUP_MIXED',
281
+ 'elk.padding' => "[top=#{BOUNDARY_PADDING},left=#{BOUNDARY_PADDING}," \
282
+ "bottom=#{BOUNDARY_PADDING},right=#{BOUNDARY_PADDING}]",
283
+ 'elk.spacing.nodeNode' => ELEMENT_SPACING.to_s
284
+ }
285
+ end
286
+ end
287
+ end
288
+ end