etassia 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 (727) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +140 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +436 -0
  5. data/exe/etassia +7 -0
  6. data/lib/etassia/cli.rb +187 -0
  7. data/lib/etassia/commands/batch.rb +210 -0
  8. data/lib/etassia/commands/notation_loader.rb +22 -0
  9. data/lib/etassia/commands/render.rb +98 -0
  10. data/lib/etassia/commands/types.rb +33 -0
  11. data/lib/etassia/commands/version.rb +24 -0
  12. data/lib/etassia/corpus_type_names.rb +52 -0
  13. data/lib/etassia/diagram/architecture.rb +108 -0
  14. data/lib/etassia/diagram/base.rb +61 -0
  15. data/lib/etassia/diagram/block.rb +102 -0
  16. data/lib/etassia/diagram/c4.rb +337 -0
  17. data/lib/etassia/diagram/class_diagram.rb +466 -0
  18. data/lib/etassia/diagram/class_namespace.rb +17 -0
  19. data/lib/etassia/diagram/class_note.rb +17 -0
  20. data/lib/etassia/diagram/containment.rb +107 -0
  21. data/lib/etassia/diagram/er_diagram.rb +317 -0
  22. data/lib/etassia/diagram/error.rb +38 -0
  23. data/lib/etassia/diagram/flowchart.rb +419 -0
  24. data/lib/etassia/diagram/flowchart_label_text.rb +63 -0
  25. data/lib/etassia/diagram/gantt.rb +94 -0
  26. data/lib/etassia/diagram/generic_text.rb +30 -0
  27. data/lib/etassia/diagram/git_graph.rb +57 -0
  28. data/lib/etassia/diagram/info.rb +38 -0
  29. data/lib/etassia/diagram/kanban.rb +198 -0
  30. data/lib/etassia/diagram/mindmap.rb +71 -0
  31. data/lib/etassia/diagram/packet.rb +93 -0
  32. data/lib/etassia/diagram/pie.rb +115 -0
  33. data/lib/etassia/diagram/quadrant.rb +135 -0
  34. data/lib/etassia/diagram/radar.rb +65 -0
  35. data/lib/etassia/diagram/requirement.rb +168 -0
  36. data/lib/etassia/diagram/sankey.rb +217 -0
  37. data/lib/etassia/diagram/sequence.rb +299 -0
  38. data/lib/etassia/diagram/sequence_box.rb +18 -0
  39. data/lib/etassia/diagram/sequence_frame.rb +37 -0
  40. data/lib/etassia/diagram/sequence_frame_section.rb +23 -0
  41. data/lib/etassia/diagram/sequence_text.rb +69 -0
  42. data/lib/etassia/diagram/sequence_wrap.rb +23 -0
  43. data/lib/etassia/diagram/state_diagram.rb +282 -0
  44. data/lib/etassia/diagram/timeline.rb +183 -0
  45. data/lib/etassia/diagram/treemap.rb +95 -0
  46. data/lib/etassia/diagram/user_journey.rb +153 -0
  47. data/lib/etassia/diagram/xy_chart.rb +90 -0
  48. data/lib/etassia/diagram.rb +8 -0
  49. data/lib/etassia/diagram_registry.rb +34 -0
  50. data/lib/etassia/engine.rb +334 -0
  51. data/lib/etassia/error/diagram_type_error.rb +10 -0
  52. data/lib/etassia/error/layout_error.rb +12 -0
  53. data/lib/etassia/error/notation_registration_error.rb +10 -0
  54. data/lib/etassia/error/parse_error.rb +11 -0
  55. data/lib/etassia/error/pipeline_error.rb +10 -0
  56. data/lib/etassia/error/render_error.rb +12 -0
  57. data/lib/etassia/error/score_error.rb +12 -0
  58. data/lib/etassia/error.rb +10 -0
  59. data/lib/etassia/error_report.rb +69 -0
  60. data/lib/etassia/ir/data.rb +39 -0
  61. data/lib/etassia/ir/data_value.rb +35 -0
  62. data/lib/etassia/ir/dimension.rb +10 -0
  63. data/lib/etassia/ir/edge.rb +34 -0
  64. data/lib/etassia/ir/graph.rb +28 -0
  65. data/lib/etassia/ir/integrity.rb +80 -0
  66. data/lib/etassia/ir/item.rb +46 -0
  67. data/lib/etassia/ir/model.rb +56 -0
  68. data/lib/etassia/ir/node.rb +10 -0
  69. data/lib/etassia/ir/placement.rb +47 -0
  70. data/lib/etassia/ir/prepositioned.rb +25 -0
  71. data/lib/etassia/ir/prepositioned_item.rb +26 -0
  72. data/lib/etassia/ir/property_set.rb +43 -0
  73. data/lib/etassia/ir/scalar.rb +25 -0
  74. data/lib/etassia/ir/series.rb +10 -0
  75. data/lib/etassia/ir.rb +17 -0
  76. data/lib/etassia/js_math.rb +511 -0
  77. data/lib/etassia/js_number.rb +64 -0
  78. data/lib/etassia/layout/architecture.rb +1472 -0
  79. data/lib/etassia/layout/base.rb +276 -0
  80. data/lib/etassia/layout/block.rb +738 -0
  81. data/lib/etassia/layout/c4.rb +876 -0
  82. data/lib/etassia/layout/c4_boundary_metrics.rb +35 -0
  83. data/lib/etassia/layout/c4_bounds.rb +106 -0
  84. data/lib/etassia/layout/c4_intersection.rb +77 -0
  85. data/lib/etassia/layout/c4_placement.rb +135 -0
  86. data/lib/etassia/layout/c4_shape_metrics.rb +97 -0
  87. data/lib/etassia/layout/c4_shape_outline.rb +67 -0
  88. data/lib/etassia/layout/c4_stereotype.rb +30 -0
  89. data/lib/etassia/layout/c4_text.rb +54 -0
  90. data/lib/etassia/layout/class_cell_placement.rb +148 -0
  91. data/lib/etassia/layout/class_diagram.rb +1116 -0
  92. data/lib/etassia/layout/class_diagram_namespaces.rb +26 -0
  93. data/lib/etassia/layout/class_diagram_notes.rb +26 -0
  94. data/lib/etassia/layout/class_name_advances.rb +31 -0
  95. data/lib/etassia/layout/class_namespace_box.rb +21 -0
  96. data/lib/etassia/layout/class_namespace_nodes.rb +197 -0
  97. data/lib/etassia/layout/class_namespace_placement.rb +71 -0
  98. data/lib/etassia/layout/class_note.rb +44 -0
  99. data/lib/etassia/layout/class_note_nodes.rb +84 -0
  100. data/lib/etassia/layout/elk_notices.rb +48 -0
  101. data/lib/etassia/layout/elk_placement.rb +197 -0
  102. data/lib/etassia/layout/er_diagram.rb +1002 -0
  103. data/lib/etassia/layout/er_diagram_adjacency.rb +20 -0
  104. data/lib/etassia/layout/er_diagram_alignment.rb +72 -0
  105. data/lib/etassia/layout/er_diagram_compaction.rb +85 -0
  106. data/lib/etassia/layout/er_diagram_conflicts.rb +70 -0
  107. data/lib/etassia/layout/er_diagram_coordinates.rb +104 -0
  108. data/lib/etassia/layout/er_diagram_entity_size.rb +82 -0
  109. data/lib/etassia/layout/er_diagram_layering.rb +109 -0
  110. data/lib/etassia/layout/er_diagram_ordering.rb +143 -0
  111. data/lib/etassia/layout/er_diagram_placement.rb +163 -0
  112. data/lib/etassia/layout/er_diagram_placement_node.rb +22 -0
  113. data/lib/etassia/layout/error.rb +133 -0
  114. data/lib/etassia/layout/flowchart/brandes_koepf.rb +208 -0
  115. data/lib/etassia/layout/flowchart/cluster_reach.rb +70 -0
  116. data/lib/etassia/layout/flowchart/component_packing.rb +129 -0
  117. data/lib/etassia/layout/flowchart/directed_placement.rb +428 -0
  118. data/lib/etassia/layout/flowchart/edge_chains.rb +111 -0
  119. data/lib/etassia/layout/flowchart/edge_order.rb +34 -0
  120. data/lib/etassia/layout/flowchart/edge_router.rb +533 -0
  121. data/lib/etassia/layout/flowchart/label_backdrop_geometry.rb +62 -0
  122. data/lib/etassia/layout/flowchart/layer_spacing.rb +85 -0
  123. data/lib/etassia/layout/flowchart/level_edges.rb +74 -0
  124. data/lib/etassia/layout/flowchart/level_frame.rb +47 -0
  125. data/lib/etassia/layout/flowchart/minimum_rank_spacing.rb +74 -0
  126. data/lib/etassia/layout/flowchart/row_centering.rb +229 -0
  127. data/lib/etassia/layout/flowchart.rb +1290 -0
  128. data/lib/etassia/layout/gantt.rb +814 -0
  129. data/lib/etassia/layout/gantt_duration.rb +49 -0
  130. data/lib/etassia/layout/gantt_exclusions.rb +74 -0
  131. data/lib/etassia/layout/gantt_ticks.rb +165 -0
  132. data/lib/etassia/layout/git_graph.rb +1075 -0
  133. data/lib/etassia/layout/grid.rb +165 -0
  134. data/lib/etassia/layout/info.rb +113 -0
  135. data/lib/etassia/layout/kanban.rb +452 -0
  136. data/lib/etassia/layout/kanban_card_text.rb +95 -0
  137. data/lib/etassia/layout/mindmap.rb +425 -0
  138. data/lib/etassia/layout/mindmap_cose.rb +81 -0
  139. data/lib/etassia/layout/mindmap_cose_body.rb +93 -0
  140. data/lib/etassia/layout/mindmap_cose_geometry.rb +190 -0
  141. data/lib/etassia/layout/mindmap_cose_grid.rb +96 -0
  142. data/lib/etassia/layout/mindmap_cose_link.rb +42 -0
  143. data/lib/etassia/layout/mindmap_cose_relaxation.rb +166 -0
  144. data/lib/etassia/layout/mindmap_cose_seed.rb +93 -0
  145. data/lib/etassia/layout/mindmap_cose_wedge.rb +37 -0
  146. data/lib/etassia/layout/mindmap_node_size.rb +130 -0
  147. data/lib/etassia/layout/packet.rb +285 -0
  148. data/lib/etassia/layout/pie.rb +305 -0
  149. data/lib/etassia/layout/pie_legend.rb +64 -0
  150. data/lib/etassia/layout/pie_legend_entry.rb +21 -0
  151. data/lib/etassia/layout/quadrant.rb +402 -0
  152. data/lib/etassia/layout/radar.rb +442 -0
  153. data/lib/etassia/layout/requirement.rb +765 -0
  154. data/lib/etassia/layout/sankey.rb +501 -0
  155. data/lib/etassia/layout/scene.rb +18 -0
  156. data/lib/etassia/layout/sequence/actor_gaps.rb +151 -0
  157. data/lib/etassia/layout/sequence/box_shape.rb +23 -0
  158. data/lib/etassia/layout/sequence/creations.rb +73 -0
  159. data/lib/etassia/layout/sequence/frame_divider.rb +22 -0
  160. data/lib/etassia/layout/sequence/frame_placement.rb +424 -0
  161. data/lib/etassia/layout/sequence/frame_reader.rb +75 -0
  162. data/lib/etassia/layout/sequence/frame_shape.rb +45 -0
  163. data/lib/etassia/layout/sequence/geometry.rb +24 -0
  164. data/lib/etassia/layout/sequence/message_rows.rb +165 -0
  165. data/lib/etassia/layout/sequence/note.rb +16 -0
  166. data/lib/etassia/layout/sequence/note_box.rb +114 -0
  167. data/lib/etassia/layout/sequence/note_placement.rb +223 -0
  168. data/lib/etassia/layout/sequence/text_width.rb +69 -0
  169. data/lib/etassia/layout/sequence/text_wrap.rb +93 -0
  170. data/lib/etassia/layout/sequence.rb +936 -0
  171. data/lib/etassia/layout/state_diagram.rb +1277 -0
  172. data/lib/etassia/layout/timeline.rb +165 -0
  173. data/lib/etassia/layout/timeline_card.rb +142 -0
  174. data/lib/etassia/layout/timeline_flow.rb +164 -0
  175. data/lib/etassia/layout/timeline_line.rb +33 -0
  176. data/lib/etassia/layout/timeline_rounding.rb +19 -0
  177. data/lib/etassia/layout/timeline_text.rb +105 -0
  178. data/lib/etassia/layout/treemap.rb +303 -0
  179. data/lib/etassia/layout/treemap_placement.rb +52 -0
  180. data/lib/etassia/layout/treemap_squarify.rb +108 -0
  181. data/lib/etassia/layout/user_journey.rb +300 -0
  182. data/lib/etassia/layout/user_journey_face.rb +33 -0
  183. data/lib/etassia/layout/user_journey_face_geometry.rb +80 -0
  184. data/lib/etassia/layout/user_journey_geometry.rb +336 -0
  185. data/lib/etassia/layout/user_journey_legend.rb +84 -0
  186. data/lib/etassia/layout/xy_chart.rb +620 -0
  187. data/lib/etassia/layout.rb +19 -0
  188. data/lib/etassia/lint_debt_scoreboard.rb +219 -0
  189. data/lib/etassia/markdown_text/emphasis_simulator.rb +373 -0
  190. data/lib/etassia/markdown_text.rb +393 -0
  191. data/lib/etassia/notation/builtin.rb +39 -0
  192. data/lib/etassia/notation/entry.rb +15 -0
  193. data/lib/etassia/notation/mermaid/class_diagram_config.rb +42 -0
  194. data/lib/etassia/notation/mermaid/git_graph_config.rb +46 -0
  195. data/lib/etassia/notation/mermaid/ir_adapter.rb +27 -0
  196. data/lib/etassia/notation/mermaid/ir_adapters/architecture.rb +196 -0
  197. data/lib/etassia/notation/mermaid/ir_adapters/block.rb +138 -0
  198. data/lib/etassia/notation/mermaid/ir_adapters/c4.rb +258 -0
  199. data/lib/etassia/notation/mermaid/ir_adapters/class_diagram.rb +257 -0
  200. data/lib/etassia/notation/mermaid/ir_adapters/er_diagram.rb +220 -0
  201. data/lib/etassia/notation/mermaid/ir_adapters/error.rb +32 -0
  202. data/lib/etassia/notation/mermaid/ir_adapters/flowchart.rb +208 -0
  203. data/lib/etassia/notation/mermaid/ir_adapters/gantt.rb +236 -0
  204. data/lib/etassia/notation/mermaid/ir_adapters/git_graph.rb +186 -0
  205. data/lib/etassia/notation/mermaid/ir_adapters/info.rb +34 -0
  206. data/lib/etassia/notation/mermaid/ir_adapters/kanban.rb +110 -0
  207. data/lib/etassia/notation/mermaid/ir_adapters/mindmap.rb +76 -0
  208. data/lib/etassia/notation/mermaid/ir_adapters/packet.rb +51 -0
  209. data/lib/etassia/notation/mermaid/ir_adapters/pie.rb +71 -0
  210. data/lib/etassia/notation/mermaid/ir_adapters/quadrant.rb +116 -0
  211. data/lib/etassia/notation/mermaid/ir_adapters/radar.rb +142 -0
  212. data/lib/etassia/notation/mermaid/ir_adapters/requirement.rb +237 -0
  213. data/lib/etassia/notation/mermaid/ir_adapters/sankey.rb +74 -0
  214. data/lib/etassia/notation/mermaid/ir_adapters/sequence.rb +304 -0
  215. data/lib/etassia/notation/mermaid/ir_adapters/state_diagram.rb +225 -0
  216. data/lib/etassia/notation/mermaid/ir_adapters/timeline.rb +93 -0
  217. data/lib/etassia/notation/mermaid/ir_adapters/treemap.rb +143 -0
  218. data/lib/etassia/notation/mermaid/ir_adapters/user_journey.rb +127 -0
  219. data/lib/etassia/notation/mermaid/ir_adapters/xychart.rb +131 -0
  220. data/lib/etassia/notation/mermaid.rb +399 -0
  221. data/lib/etassia/notation/parsed.rb +31 -0
  222. data/lib/etassia/notation/plantuml/activity/arrow_head.rb +45 -0
  223. data/lib/etassia/notation/plantuml/activity/arrow_style.rb +53 -0
  224. data/lib/etassia/notation/plantuml/activity/bar_columns.rb +75 -0
  225. data/lib/etassia/notation/plantuml/activity/box.rb +60 -0
  226. data/lib/etassia/notation/plantuml/activity/builder.rb +77 -0
  227. data/lib/etassia/notation/plantuml/activity/canvas.rb +117 -0
  228. data/lib/etassia/notation/plantuml/activity/column_centres.rb +151 -0
  229. data/lib/etassia/notation/plantuml/activity/columns.rb +224 -0
  230. data/lib/etassia/notation/plantuml/activity/diagram.rb +35 -0
  231. data/lib/etassia/notation/plantuml/activity/edge.rb +35 -0
  232. data/lib/etassia/notation/plantuml/activity/edge_marks.rb +68 -0
  233. data/lib/etassia/notation/plantuml/activity/exits.rb +61 -0
  234. data/lib/etassia/notation/plantuml/activity/frame_marks.rb +116 -0
  235. data/lib/etassia/notation/plantuml/activity/lane_steps.rb +82 -0
  236. data/lib/etassia/notation/plantuml/activity/lane_titles.rb +46 -0
  237. data/lib/etassia/notation/plantuml/activity/layout.rb +50 -0
  238. data/lib/etassia/notation/plantuml/activity/legacy_parser.rb +181 -0
  239. data/lib/etassia/notation/plantuml/activity/legacy_row_gaps.rb +65 -0
  240. data/lib/etassia/notation/plantuml/activity/mark.rb +23 -0
  241. data/lib/etassia/notation/plantuml/activity/marks.rb +62 -0
  242. data/lib/etassia/notation/plantuml/activity/metrics.rb +169 -0
  243. data/lib/etassia/notation/plantuml/activity/new_parser/blocks.rb +166 -0
  244. data/lib/etassia/notation/plantuml/activity/new_parser.rb +177 -0
  245. data/lib/etassia/notation/plantuml/activity/node.rb +50 -0
  246. data/lib/etassia/notation/plantuml/activity/node_marks.rb +174 -0
  247. data/lib/etassia/notation/plantuml/activity/note.rb +27 -0
  248. data/lib/etassia/notation/plantuml/activity/parser.rb +51 -0
  249. data/lib/etassia/notation/plantuml/activity/placement.rb +327 -0
  250. data/lib/etassia/notation/plantuml/activity/ranking.rb +142 -0
  251. data/lib/etassia/notation/plantuml/activity/reach.rb +58 -0
  252. data/lib/etassia/notation/plantuml/activity/renderer.rb +220 -0
  253. data/lib/etassia/notation/plantuml/activity/route.rb +22 -0
  254. data/lib/etassia/notation/plantuml/activity/router.rb +308 -0
  255. data/lib/etassia/notation/plantuml/activity/row_gaps.rb +105 -0
  256. data/lib/etassia/notation/plantuml/activity/row_spread.rb +59 -0
  257. data/lib/etassia/notation/plantuml/activity/scene.rb +20 -0
  258. data/lib/etassia/notation/plantuml/activity/text.rb +36 -0
  259. data/lib/etassia/notation/plantuml/activity.rb +50 -0
  260. data/lib/etassia/notation/plantuml/arrow.rb +135 -0
  261. data/lib/etassia/notation/plantuml/caption.rb +50 -0
  262. data/lib/etassia/notation/plantuml/caption_layout.rb +97 -0
  263. data/lib/etassia/notation/plantuml/caption_rows.rb +67 -0
  264. data/lib/etassia/notation/plantuml/class_header.rb +178 -0
  265. data/lib/etassia/notation/plantuml/class_name.rb +51 -0
  266. data/lib/etassia/notation/plantuml/component.rb +37 -0
  267. data/lib/etassia/notation/plantuml/creole.rb +278 -0
  268. data/lib/etassia/notation/plantuml/creole_icons.rb +33 -0
  269. data/lib/etassia/notation/plantuml/diagram.rb +60 -0
  270. data/lib/etassia/notation/plantuml/diagram_builder.rb +442 -0
  271. data/lib/etassia/notation/plantuml/diagram_name.rb +51 -0
  272. data/lib/etassia/notation/plantuml/directives.rb +33 -0
  273. data/lib/etassia/notation/plantuml/drawing/box.rb +28 -0
  274. data/lib/etassia/notation/plantuml/drawing/face.rb +39 -0
  275. data/lib/etassia/notation/plantuml/drawing/pen.rb +105 -0
  276. data/lib/etassia/notation/plantuml/drawing/renderer.rb +36 -0
  277. data/lib/etassia/notation/plantuml/drawing/scene.rb +28 -0
  278. data/lib/etassia/notation/plantuml/drawing.rb +19 -0
  279. data/lib/etassia/notation/plantuml/ebnf/blocks.rb +272 -0
  280. data/lib/etassia/notation/plantuml/ebnf/layout.rb +117 -0
  281. data/lib/etassia/notation/plantuml/ebnf/model.rb +22 -0
  282. data/lib/etassia/notation/plantuml/ebnf/reader.rb +131 -0
  283. data/lib/etassia/notation/plantuml/ebnf.rb +34 -0
  284. data/lib/etassia/notation/plantuml/end_texts.rb +78 -0
  285. data/lib/etassia/notation/plantuml/entity.rb +54 -0
  286. data/lib/etassia/notation/plantuml/further.rb +38 -0
  287. data/lib/etassia/notation/plantuml/gantt/layout.rb +269 -0
  288. data/lib/etassia/notation/plantuml/gantt/reader.rb +210 -0
  289. data/lib/etassia/notation/plantuml/gantt/renderer.rb +81 -0
  290. data/lib/etassia/notation/plantuml/gantt.rb +34 -0
  291. data/lib/etassia/notation/plantuml/include/blocks.rb +66 -0
  292. data/lib/etassia/notation/plantuml/include/finder.rb +61 -0
  293. data/lib/etassia/notation/plantuml/include.rb +78 -0
  294. data/lib/etassia/notation/plantuml/ir_adapter/annotations.rb +40 -0
  295. data/lib/etassia/notation/plantuml/ir_adapter/links.rb +91 -0
  296. data/lib/etassia/notation/plantuml/ir_adapter/sink.rb +49 -0
  297. data/lib/etassia/notation/plantuml/ir_adapter/structure.rb +86 -0
  298. data/lib/etassia/notation/plantuml/ir_adapter.rb +32 -0
  299. data/lib/etassia/notation/plantuml/ir_reader/annotations.rb +51 -0
  300. data/lib/etassia/notation/plantuml/ir_reader/index.rb +49 -0
  301. data/lib/etassia/notation/plantuml/ir_reader/links.rb +133 -0
  302. data/lib/etassia/notation/plantuml/ir_reader/structure.rb +83 -0
  303. data/lib/etassia/notation/plantuml/ir_reader.rb +42 -0
  304. data/lib/etassia/notation/plantuml/junction.rb +20 -0
  305. data/lib/etassia/notation/plantuml/klass.rb +41 -0
  306. data/lib/etassia/notation/plantuml/label_clearance.rb +67 -0
  307. data/lib/etassia/notation/plantuml/layout.rb +2806 -0
  308. data/lib/etassia/notation/plantuml/member.rb +37 -0
  309. data/lib/etassia/notation/plantuml/member_icon.rb +87 -0
  310. data/lib/etassia/notation/plantuml/member_icon_elements.rb +91 -0
  311. data/lib/etassia/notation/plantuml/member_pitch.rb +76 -0
  312. data/lib/etassia/notation/plantuml/member_rule.rb +48 -0
  313. data/lib/etassia/notation/plantuml/network/grid.rb +170 -0
  314. data/lib/etassia/notation/plantuml/network/layout.rb +158 -0
  315. data/lib/etassia/notation/plantuml/network/model.rb +22 -0
  316. data/lib/etassia/notation/plantuml/network/placement.rb +64 -0
  317. data/lib/etassia/notation/plantuml/network/reader.rb +126 -0
  318. data/lib/etassia/notation/plantuml/network/shapes.rb +109 -0
  319. data/lib/etassia/notation/plantuml/network.rb +37 -0
  320. data/lib/etassia/notation/plantuml/node_graph/archimate.rb +49 -0
  321. data/lib/etassia/notation/plantuml/node_graph/box_reader.rb +86 -0
  322. data/lib/etassia/notation/plantuml/node_graph/cloud.rb +73 -0
  323. data/lib/etassia/notation/plantuml/node_graph/crows_foot.rb +85 -0
  324. data/lib/etassia/notation/plantuml/node_graph/curve.rb +66 -0
  325. data/lib/etassia/notation/plantuml/node_graph/data_reader.rb +77 -0
  326. data/lib/etassia/notation/plantuml/node_graph/data_shape.rb +39 -0
  327. data/lib/etassia/notation/plantuml/node_graph/detour.rb +98 -0
  328. data/lib/etassia/notation/plantuml/node_graph/edge.rb +26 -0
  329. data/lib/etassia/notation/plantuml/node_graph/entity_metrics.rb +49 -0
  330. data/lib/etassia/notation/plantuml/node_graph/entity_reader.rb +65 -0
  331. data/lib/etassia/notation/plantuml/node_graph/entity_shape.rb +111 -0
  332. data/lib/etassia/notation/plantuml/node_graph/figures.rb +92 -0
  333. data/lib/etassia/notation/plantuml/node_graph/frame_ports.rb +73 -0
  334. data/lib/etassia/notation/plantuml/node_graph/graph.rb +40 -0
  335. data/lib/etassia/notation/plantuml/node_graph/heads.rb +89 -0
  336. data/lib/etassia/notation/plantuml/node_graph/label_fan.rb +76 -0
  337. data/lib/etassia/notation/plantuml/node_graph/label_placer.rb +310 -0
  338. data/lib/etassia/notation/plantuml/node_graph/label_waypoint.rb +47 -0
  339. data/lib/etassia/notation/plantuml/node_graph/layout.rb +293 -0
  340. data/lib/etassia/notation/plantuml/node_graph/link.rb +78 -0
  341. data/lib/etassia/notation/plantuml/node_graph/macros.rb +39 -0
  342. data/lib/etassia/notation/plantuml/node_graph/metrics.rb +113 -0
  343. data/lib/etassia/notation/plantuml/node_graph/node.rb +44 -0
  344. data/lib/etassia/notation/plantuml/node_graph/note_tail.rb +58 -0
  345. data/lib/etassia/notation/plantuml/node_graph/packer.rb +126 -0
  346. data/lib/etassia/notation/plantuml/node_graph/parser.rb +309 -0
  347. data/lib/etassia/notation/plantuml/node_graph/paths.rb +48 -0
  348. data/lib/etassia/notation/plantuml/node_graph/ranking.rb +231 -0
  349. data/lib/etassia/notation/plantuml/node_graph/reader.rb +53 -0
  350. data/lib/etassia/notation/plantuml/node_graph/references.rb +38 -0
  351. data/lib/etassia/notation/plantuml/node_graph/renderer.rb +160 -0
  352. data/lib/etassia/notation/plantuml/node_graph/scene.rb +22 -0
  353. data/lib/etassia/notation/plantuml/node_graph/shapes.rb +302 -0
  354. data/lib/etassia/notation/plantuml/node_graph/skipped_ranks.rb +61 -0
  355. data/lib/etassia/notation/plantuml/node_graph/tab.rb +68 -0
  356. data/lib/etassia/notation/plantuml/node_graph/usecase_ellipse.rb +100 -0
  357. data/lib/etassia/notation/plantuml/node_graph.rb +38 -0
  358. data/lib/etassia/notation/plantuml/note.rb +27 -0
  359. data/lib/etassia/notation/plantuml/note_layout.rb +257 -0
  360. data/lib/etassia/notation/plantuml/note_outline.rb +68 -0
  361. data/lib/etassia/notation/plantuml/objects/map_rows.rb +46 -0
  362. data/lib/etassia/notation/plantuml/objects/reader.rb +208 -0
  363. data/lib/etassia/notation/plantuml/objects.rb +39 -0
  364. data/lib/etassia/notation/plantuml/package.rb +45 -0
  365. data/lib/etassia/notation/plantuml/package_color.rb +32 -0
  366. data/lib/etassia/notation/plantuml/package_frames.rb +178 -0
  367. data/lib/etassia/notation/plantuml/parser.rb +622 -0
  368. data/lib/etassia/notation/plantuml/portion_visibility.rb +91 -0
  369. data/lib/etassia/notation/plantuml/qualified_association_layout.rb +332 -0
  370. data/lib/etassia/notation/plantuml/qualifier_box.rb +78 -0
  371. data/lib/etassia/notation/plantuml/rank_chain.rb +527 -0
  372. data/lib/etassia/notation/plantuml/rank_hierarchy.rb +347 -0
  373. data/lib/etassia/notation/plantuml/relation.rb +103 -0
  374. data/lib/etassia/notation/plantuml/relation_label_separator.rb +199 -0
  375. data/lib/etassia/notation/plantuml/relation_path_points.rb +84 -0
  376. data/lib/etassia/notation/plantuml/renderer.rb +490 -0
  377. data/lib/etassia/notation/plantuml/scene.rb +134 -0
  378. data/lib/etassia/notation/plantuml/segment_box.rb +85 -0
  379. data/lib/etassia/notation/plantuml/sequence/activation.rb +28 -0
  380. data/lib/etassia/notation/plantuml/sequence/appearance.rb +71 -0
  381. data/lib/etassia/notation/plantuml/sequence/arrow_end.rb +23 -0
  382. data/lib/etassia/notation/plantuml/sequence/arrow_marks.rb +111 -0
  383. data/lib/etassia/notation/plantuml/sequence/arrow_style.rb +42 -0
  384. data/lib/etassia/notation/plantuml/sequence/arrow_syntax.rb +122 -0
  385. data/lib/etassia/notation/plantuml/sequence/bar_tracker.rb +59 -0
  386. data/lib/etassia/notation/plantuml/sequence/box.rb +23 -0
  387. data/lib/etassia/notation/plantuml/sequence/chrome.rb +60 -0
  388. data/lib/etassia/notation/plantuml/sequence/chrome_rows.rb +184 -0
  389. data/lib/etassia/notation/plantuml/sequence/create.rb +34 -0
  390. data/lib/etassia/notation/plantuml/sequence/delay.rb +19 -0
  391. data/lib/etassia/notation/plantuml/sequence/destroy.rb +20 -0
  392. data/lib/etassia/notation/plantuml/sequence/diagram.rb +86 -0
  393. data/lib/etassia/notation/plantuml/sequence/divider.rb +19 -0
  394. data/lib/etassia/notation/plantuml/sequence/edge.rb +53 -0
  395. data/lib/etassia/notation/plantuml/sequence/embedded.rb +77 -0
  396. data/lib/etassia/notation/plantuml/sequence/fill.rb +56 -0
  397. data/lib/etassia/notation/plantuml/sequence/fragment.rb +28 -0
  398. data/lib/etassia/notation/plantuml/sequence/fragment_shape.rb +138 -0
  399. data/lib/etassia/notation/plantuml/sequence/head_style.rb +43 -0
  400. data/lib/etassia/notation/plantuml/sequence/ir_adapter/blocks.rb +73 -0
  401. data/lib/etassia/notation/plantuml/sequence/ir_adapter/fills.rb +24 -0
  402. data/lib/etassia/notation/plantuml/sequence/ir_adapter/items.rb +48 -0
  403. data/lib/etassia/notation/plantuml/sequence/ir_adapter/messages.rb +62 -0
  404. data/lib/etassia/notation/plantuml/sequence/ir_adapter/participants.rb +42 -0
  405. data/lib/etassia/notation/plantuml/sequence/ir_adapter/settings.rb +54 -0
  406. data/lib/etassia/notation/plantuml/sequence/ir_adapter.rb +38 -0
  407. data/lib/etassia/notation/plantuml/sequence/ir_reader/blocks.rb +77 -0
  408. data/lib/etassia/notation/plantuml/sequence/ir_reader/fills.rb +27 -0
  409. data/lib/etassia/notation/plantuml/sequence/ir_reader/index.rb +36 -0
  410. data/lib/etassia/notation/plantuml/sequence/ir_reader/items.rb +35 -0
  411. data/lib/etassia/notation/plantuml/sequence/ir_reader/messages.rb +70 -0
  412. data/lib/etassia/notation/plantuml/sequence/ir_reader/participants.rb +42 -0
  413. data/lib/etassia/notation/plantuml/sequence/ir_reader/settings.rb +74 -0
  414. data/lib/etassia/notation/plantuml/sequence/ir_reader.rb +38 -0
  415. data/lib/etassia/notation/plantuml/sequence/keyword_defaults.rb +28 -0
  416. data/lib/etassia/notation/plantuml/sequence/layout.rb +1567 -0
  417. data/lib/etassia/notation/plantuml/sequence/message.rb +97 -0
  418. data/lib/etassia/notation/plantuml/sequence/message_wrap.rb +77 -0
  419. data/lib/etassia/notation/plantuml/sequence/note.rb +61 -0
  420. data/lib/etassia/notation/plantuml/sequence/note_fill.rb +35 -0
  421. data/lib/etassia/notation/plantuml/sequence/note_geometry.rb +126 -0
  422. data/lib/etassia/notation/plantuml/sequence/note_shape.rb +51 -0
  423. data/lib/etassia/notation/plantuml/sequence/numbered_label.rb +57 -0
  424. data/lib/etassia/notation/plantuml/sequence/numbering.rb +49 -0
  425. data/lib/etassia/notation/plantuml/sequence/outline.rb +204 -0
  426. data/lib/etassia/notation/plantuml/sequence/page_break.rb +17 -0
  427. data/lib/etassia/notation/plantuml/sequence/paint.rb +79 -0
  428. data/lib/etassia/notation/plantuml/sequence/parallel_message.rb +19 -0
  429. data/lib/etassia/notation/plantuml/sequence/parser.rb +844 -0
  430. data/lib/etassia/notation/plantuml/sequence/participant.rb +27 -0
  431. data/lib/etassia/notation/plantuml/sequence/picture.rb +29 -0
  432. data/lib/etassia/notation/plantuml/sequence/ref.rb +24 -0
  433. data/lib/etassia/notation/plantuml/sequence/ref_shape.rb +74 -0
  434. data/lib/etassia/notation/plantuml/sequence/refusals.rb +47 -0
  435. data/lib/etassia/notation/plantuml/sequence/renderer.rb +501 -0
  436. data/lib/etassia/notation/plantuml/sequence/scene.rb +200 -0
  437. data/lib/etassia/notation/plantuml/sequence/skin.rb +76 -0
  438. data/lib/etassia/notation/plantuml/sequence/spacing.rb +28 -0
  439. data/lib/etassia/notation/plantuml/sequence/style.rb +151 -0
  440. data/lib/etassia/notation/plantuml/sequence/tab_size.rb +42 -0
  441. data/lib/etassia/notation/plantuml/sequence/walker.rb +1190 -0
  442. data/lib/etassia/notation/plantuml/sequence.rb +47 -0
  443. data/lib/etassia/notation/plantuml/source_lines.rb +33 -0
  444. data/lib/etassia/notation/plantuml/state/axis_solver.rb +272 -0
  445. data/lib/etassia/notation/plantuml/state/cluster_padding.rb +75 -0
  446. data/lib/etassia/notation/plantuml/state/curve.rb +73 -0
  447. data/lib/etassia/notation/plantuml/state/dot_axis.rb +248 -0
  448. data/lib/etassia/notation/plantuml/state/extent.rb +42 -0
  449. data/lib/etassia/notation/plantuml/state/hierarchy.rb +355 -0
  450. data/lib/etassia/notation/plantuml/state/label_clearance.rb +148 -0
  451. data/lib/etassia/notation/plantuml/state/label_reach.rb +139 -0
  452. data/lib/etassia/notation/plantuml/state/layout.rb +173 -0
  453. data/lib/etassia/notation/plantuml/state/level_edges.rb +113 -0
  454. data/lib/etassia/notation/plantuml/state/look.rb +36 -0
  455. data/lib/etassia/notation/plantuml/state/note_shape.rb +73 -0
  456. data/lib/etassia/notation/plantuml/state/plane.rb +85 -0
  457. data/lib/etassia/notation/plantuml/state/ranking.rb +181 -0
  458. data/lib/etassia/notation/plantuml/state/reader.rb +310 -0
  459. data/lib/etassia/notation/plantuml/state/renderer.rb +253 -0
  460. data/lib/etassia/notation/plantuml/state/route_detour.rb +115 -0
  461. data/lib/etassia/notation/plantuml/state/route_geometry.rb +186 -0
  462. data/lib/etassia/notation/plantuml/state/routes.rb +196 -0
  463. data/lib/etassia/notation/plantuml/state/row.rb +88 -0
  464. data/lib/etassia/notation/plantuml/state/rows.rb +50 -0
  465. data/lib/etassia/notation/plantuml/state/stretch.rb +71 -0
  466. data/lib/etassia/notation/plantuml/state.rb +62 -0
  467. data/lib/etassia/notation/plantuml/stereotype_head.rb +58 -0
  468. data/lib/etassia/notation/plantuml/style/bound.rb +34 -0
  469. data/lib/etassia/notation/plantuml/style/colour.rb +41 -0
  470. data/lib/etassia/notation/plantuml/style/colour_names.rb +89 -0
  471. data/lib/etassia/notation/plantuml/style/defaults.rb +33 -0
  472. data/lib/etassia/notation/plantuml/style/inline.rb +56 -0
  473. data/lib/etassia/notation/plantuml/style/reader.rb +242 -0
  474. data/lib/etassia/notation/plantuml/style/sheet.rb +103 -0
  475. data/lib/etassia/notation/plantuml/style.rb +32 -0
  476. data/lib/etassia/notation/plantuml/style_reader.rb +160 -0
  477. data/lib/etassia/notation/plantuml/style_sheet.rb +27 -0
  478. data/lib/etassia/notation/plantuml/styled_relation_curve.rb +71 -0
  479. data/lib/etassia/notation/plantuml/timing/arrows.rb +81 -0
  480. data/lib/etassia/notation/plantuml/timing/axis.rb +100 -0
  481. data/lib/etassia/notation/plantuml/timing/layout.rb +172 -0
  482. data/lib/etassia/notation/plantuml/timing/model.rb +34 -0
  483. data/lib/etassia/notation/plantuml/timing/reader.rb +173 -0
  484. data/lib/etassia/notation/plantuml/timing/signals.rb +328 -0
  485. data/lib/etassia/notation/plantuml/timing.rb +38 -0
  486. data/lib/etassia/notation/plantuml/tree/layout.rb +276 -0
  487. data/lib/etassia/notation/plantuml/tree/mindmap_layout.rb +157 -0
  488. data/lib/etassia/notation/plantuml/tree/reader.rb +186 -0
  489. data/lib/etassia/notation/plantuml/tree/renderer.rb +67 -0
  490. data/lib/etassia/notation/plantuml/tree/role.rb +57 -0
  491. data/lib/etassia/notation/plantuml/tree/rules.rb +156 -0
  492. data/lib/etassia/notation/plantuml/tree/wbs_layout.rb +164 -0
  493. data/lib/etassia/notation/plantuml/tree.rb +41 -0
  494. data/lib/etassia/notation/plantuml/unsupported_construct_error.rb +39 -0
  495. data/lib/etassia/notation/plantuml/unsupported_constructs.rb +103 -0
  496. data/lib/etassia/notation/plantuml/usecase.rb +36 -0
  497. data/lib/etassia/notation/plantuml.rb +132 -0
  498. data/lib/etassia/notation/plantuml_data/boxes/frames.rb +101 -0
  499. data/lib/etassia/notation/plantuml_data/boxes/graph_layout.rb +348 -0
  500. data/lib/etassia/notation/plantuml_data/boxes/heads.rb +65 -0
  501. data/lib/etassia/notation/plantuml_data/boxes/highlights.rb +70 -0
  502. data/lib/etassia/notation/plantuml_data/boxes/inline_rows.rb +99 -0
  503. data/lib/etassia/notation/plantuml_data/boxes/json_reader.rb +24 -0
  504. data/lib/etassia/notation/plantuml_data/boxes/link_text.rb +94 -0
  505. data/lib/etassia/notation/plantuml_data/boxes/marks.rb +76 -0
  506. data/lib/etassia/notation/plantuml_data/boxes/ornaments.rb +130 -0
  507. data/lib/etassia/notation/plantuml_data/boxes/renderer.rb +95 -0
  508. data/lib/etassia/notation/plantuml_data/boxes/row_alignment.rb +58 -0
  509. data/lib/etassia/notation/plantuml_data/boxes/row_bend.rb +43 -0
  510. data/lib/etassia/notation/plantuml_data/boxes/scene.rb +27 -0
  511. data/lib/etassia/notation/plantuml_data/boxes/table.rb +56 -0
  512. data/lib/etassia/notation/plantuml_data/boxes/table_box.rb +355 -0
  513. data/lib/etassia/notation/plantuml_data/boxes/text.rb +101 -0
  514. data/lib/etassia/notation/plantuml_data/boxes/tree_builder.rb +82 -0
  515. data/lib/etassia/notation/plantuml_data/boxes/tree_layout.rb +82 -0
  516. data/lib/etassia/notation/plantuml_data/boxes/yaml_reader.rb +105 -0
  517. data/lib/etassia/notation/plantuml_data/openiconic.LICENSE +6 -0
  518. data/lib/etassia/notation/plantuml_data/openiconic.json +1 -0
  519. data/lib/etassia/notation/plantuml_data.rb +84 -0
  520. data/lib/etassia/notation/plantuml_salt/block.rb +80 -0
  521. data/lib/etassia/notation/plantuml_salt/box.rb +30 -0
  522. data/lib/etassia/notation/plantuml_salt/font_metrics.rb +28 -0
  523. data/lib/etassia/notation/plantuml_salt/grid_marks.rb +66 -0
  524. data/lib/etassia/notation/plantuml_salt/group_marks.rb +77 -0
  525. data/lib/etassia/notation/plantuml_salt/layout.rb +206 -0
  526. data/lib/etassia/notation/plantuml_salt/mark.rb +36 -0
  527. data/lib/etassia/notation/plantuml_salt/markup.rb +57 -0
  528. data/lib/etassia/notation/plantuml_salt/parser.rb +239 -0
  529. data/lib/etassia/notation/plantuml_salt/renderer.rb +138 -0
  530. data/lib/etassia/notation/plantuml_salt/row.rb +19 -0
  531. data/lib/etassia/notation/plantuml_salt/run.rb +34 -0
  532. data/lib/etassia/notation/plantuml_salt/scene.rb +23 -0
  533. data/lib/etassia/notation/plantuml_salt/scroll_marks.rb +94 -0
  534. data/lib/etassia/notation/plantuml_salt/tab_marks.rb +74 -0
  535. data/lib/etassia/notation/plantuml_salt/tree_marks.rb +127 -0
  536. data/lib/etassia/notation/plantuml_salt/widget.rb +40 -0
  537. data/lib/etassia/notation/plantuml_salt/widget_marks.rb +319 -0
  538. data/lib/etassia/notation/plantuml_salt.rb +66 -0
  539. data/lib/etassia/notation/plugin_failure.rb +42 -0
  540. data/lib/etassia/notation.rb +289 -0
  541. data/lib/etassia/parser/architecture.rb +25 -0
  542. data/lib/etassia/parser/atoms/delimited_run.rb +82 -0
  543. data/lib/etassia/parser/atoms/greedy_run.rb +116 -0
  544. data/lib/etassia/parser/atoms/joined.rb +47 -0
  545. data/lib/etassia/parser/atoms/trimmed_run.rb +47 -0
  546. data/lib/etassia/parser/base.rb +258 -0
  547. data/lib/etassia/parser/block.rb +32 -0
  548. data/lib/etassia/parser/builders/architecture.rb +135 -0
  549. data/lib/etassia/parser/builders/block.rb +288 -0
  550. data/lib/etassia/parser/builders/c4.rb +275 -0
  551. data/lib/etassia/parser/builders/capture_string.rb +17 -0
  552. data/lib/etassia/parser/builders/class_diagram.rb +539 -0
  553. data/lib/etassia/parser/builders/er_diagram.rb +308 -0
  554. data/lib/etassia/parser/builders/error.rb +51 -0
  555. data/lib/etassia/parser/builders/flowchart.rb +1150 -0
  556. data/lib/etassia/parser/builders/gantt.rb +267 -0
  557. data/lib/etassia/parser/builders/git_graph.rb +301 -0
  558. data/lib/etassia/parser/builders/info.rb +57 -0
  559. data/lib/etassia/parser/builders/kanban.rb +411 -0
  560. data/lib/etassia/parser/builders/mindmap.rb +271 -0
  561. data/lib/etassia/parser/builders/packet.rb +85 -0
  562. data/lib/etassia/parser/builders/pie.rb +105 -0
  563. data/lib/etassia/parser/builders/quadrant.rb +165 -0
  564. data/lib/etassia/parser/builders/radar.rb +157 -0
  565. data/lib/etassia/parser/builders/requirement.rb +230 -0
  566. data/lib/etassia/parser/builders/sankey.rb +142 -0
  567. data/lib/etassia/parser/builders/sequence.rb +431 -0
  568. data/lib/etassia/parser/builders/sequence_blocks.rb +117 -0
  569. data/lib/etassia/parser/builders/sequence_box_title.rb +57 -0
  570. data/lib/etassia/parser/builders/state_diagram.rb +364 -0
  571. data/lib/etassia/parser/builders/timeline.rb +133 -0
  572. data/lib/etassia/parser/builders/treemap.rb +138 -0
  573. data/lib/etassia/parser/builders/xy_chart.rb +148 -0
  574. data/lib/etassia/parser/c4.rb +37 -0
  575. data/lib/etassia/parser/class_diagram.rb +58 -0
  576. data/lib/etassia/parser/class_namespaces.rb +62 -0
  577. data/lib/etassia/parser/class_notes.rb +54 -0
  578. data/lib/etassia/parser/er_diagram.rb +28 -0
  579. data/lib/etassia/parser/error.rb +30 -0
  580. data/lib/etassia/parser/flowchart.rb +87 -0
  581. data/lib/etassia/parser/gantt.rb +43 -0
  582. data/lib/etassia/parser/git_graph.rb +106 -0
  583. data/lib/etassia/parser/grammars/architecture.rb +165 -0
  584. data/lib/etassia/parser/grammars/block.rb +190 -0
  585. data/lib/etassia/parser/grammars/c4.rb +229 -0
  586. data/lib/etassia/parser/grammars/class_diagram.rb +606 -0
  587. data/lib/etassia/parser/grammars/common.rb +107 -0
  588. data/lib/etassia/parser/grammars/er_diagram.rb +232 -0
  589. data/lib/etassia/parser/grammars/error.rb +32 -0
  590. data/lib/etassia/parser/grammars/flowchart.rb +1722 -0
  591. data/lib/etassia/parser/grammars/gantt.rb +198 -0
  592. data/lib/etassia/parser/grammars/git_graph.rb +234 -0
  593. data/lib/etassia/parser/grammars/info.rb +53 -0
  594. data/lib/etassia/parser/grammars/kanban.rb +417 -0
  595. data/lib/etassia/parser/grammars/mermaid_unicode_text.rb +108 -0
  596. data/lib/etassia/parser/grammars/mindmap.rb +220 -0
  597. data/lib/etassia/parser/grammars/packet.rb +71 -0
  598. data/lib/etassia/parser/grammars/pie.rb +128 -0
  599. data/lib/etassia/parser/grammars/quadrant.rb +202 -0
  600. data/lib/etassia/parser/grammars/radar.rb +158 -0
  601. data/lib/etassia/parser/grammars/requirement.rb +258 -0
  602. data/lib/etassia/parser/grammars/sankey.rb +104 -0
  603. data/lib/etassia/parser/grammars/sequence.rb +940 -0
  604. data/lib/etassia/parser/grammars/state_diagram.rb +382 -0
  605. data/lib/etassia/parser/grammars/timeline.rb +153 -0
  606. data/lib/etassia/parser/grammars/treemap.rb +122 -0
  607. data/lib/etassia/parser/grammars/xy_chart.rb +119 -0
  608. data/lib/etassia/parser/info.rb +32 -0
  609. data/lib/etassia/parser/kanban.rb +131 -0
  610. data/lib/etassia/parser/mermaid_shapes.rb +106 -0
  611. data/lib/etassia/parser/metadata_yaml.rb +452 -0
  612. data/lib/etassia/parser/mindmap.rb +92 -0
  613. data/lib/etassia/parser/native/builders/architecture.rb +103 -0
  614. data/lib/etassia/parser/native/builders/block.rb +68 -0
  615. data/lib/etassia/parser/native/builders/c4.rb +16 -0
  616. data/lib/etassia/parser/native/builders/class_diagram.rb +63 -0
  617. data/lib/etassia/parser/native/builders/er_diagram.rb +47 -0
  618. data/lib/etassia/parser/native/builders/flowchart.rb +58 -0
  619. data/lib/etassia/parser/native/builders/gantt.rb +35 -0
  620. data/lib/etassia/parser/native/builders/gitgraph.rb +35 -0
  621. data/lib/etassia/parser/native/builders/info.rb +30 -0
  622. data/lib/etassia/parser/native/builders/kanban.rb +37 -0
  623. data/lib/etassia/parser/native/builders/mindmap.rb +26 -0
  624. data/lib/etassia/parser/native/builders/packet.rb +63 -0
  625. data/lib/etassia/parser/native/builders/pie.rb +82 -0
  626. data/lib/etassia/parser/native/builders/quadrant.rb +27 -0
  627. data/lib/etassia/parser/native/builders/radar.rb +84 -0
  628. data/lib/etassia/parser/native/builders/requirement.rb +25 -0
  629. data/lib/etassia/parser/native/builders/sankey.rb +18 -0
  630. data/lib/etassia/parser/native/builders/sequence.rb +48 -0
  631. data/lib/etassia/parser/native/builders/state_diagram.rb +61 -0
  632. data/lib/etassia/parser/native/builders/timeline.rb +46 -0
  633. data/lib/etassia/parser/native/builders/treemap.rb +60 -0
  634. data/lib/etassia/parser/native/builders/user_journey.rb +83 -0
  635. data/lib/etassia/parser/native/builders/xy_chart.rb +94 -0
  636. data/lib/etassia/parser/native/declined.rb +12 -0
  637. data/lib/etassia/parser/native/mermaid.json +1 -0
  638. data/lib/etassia/parser/native/text.rb +31 -0
  639. data/lib/etassia/parser/native.rb +99 -0
  640. data/lib/etassia/parser/packet.rb +56 -0
  641. data/lib/etassia/parser/pie.rb +32 -0
  642. data/lib/etassia/parser/quadrant.rb +77 -0
  643. data/lib/etassia/parser/radar.rb +99 -0
  644. data/lib/etassia/parser/requirement.rb +32 -0
  645. data/lib/etassia/parser/sankey.rb +47 -0
  646. data/lib/etassia/parser/sequence.rb +34 -0
  647. data/lib/etassia/parser/state_diagram.rb +29 -0
  648. data/lib/etassia/parser/timeline.rb +40 -0
  649. data/lib/etassia/parser/treemap.rb +25 -0
  650. data/lib/etassia/parser/user_journey.rb +253 -0
  651. data/lib/etassia/parser/xy_chart.rb +98 -0
  652. data/lib/etassia/parser.rb +18 -0
  653. data/lib/etassia/renderer/architecture.rb +232 -0
  654. data/lib/etassia/renderer/architecture_edge_router.rb +379 -0
  655. data/lib/etassia/renderer/base.rb +295 -0
  656. data/lib/etassia/renderer/block.rb +157 -0
  657. data/lib/etassia/renderer/c4.rb +294 -0
  658. data/lib/etassia/renderer/class_diagram.rb +671 -0
  659. data/lib/etassia/renderer/er_diagram.rb +842 -0
  660. data/lib/etassia/renderer/error.rb +131 -0
  661. data/lib/etassia/renderer/flowchart.rb +315 -0
  662. data/lib/etassia/renderer/gantt.rb +177 -0
  663. data/lib/etassia/renderer/git_graph.rb +224 -0
  664. data/lib/etassia/renderer/info.rb +59 -0
  665. data/lib/etassia/renderer/kanban.rb +157 -0
  666. data/lib/etassia/renderer/line_break_text.rb +56 -0
  667. data/lib/etassia/renderer/markdown_text.rb +106 -0
  668. data/lib/etassia/renderer/mindmap.rb +290 -0
  669. data/lib/etassia/renderer/packet.rb +87 -0
  670. data/lib/etassia/renderer/pie.rb +113 -0
  671. data/lib/etassia/renderer/quadrant.rb +189 -0
  672. data/lib/etassia/renderer/radar.rb +191 -0
  673. data/lib/etassia/renderer/requirement.rb +209 -0
  674. data/lib/etassia/renderer/sankey.rb +114 -0
  675. data/lib/etassia/renderer/sequence/frame_drawing.rb +169 -0
  676. data/lib/etassia/renderer/sequence/note_drawing.rb +69 -0
  677. data/lib/etassia/renderer/sequence.rb +490 -0
  678. data/lib/etassia/renderer/state_diagram.rb +202 -0
  679. data/lib/etassia/renderer/timeline.rb +134 -0
  680. data/lib/etassia/renderer/timeline_palette.rb +18 -0
  681. data/lib/etassia/renderer/treemap/section_total.rb +33 -0
  682. data/lib/etassia/renderer/treemap.rb +128 -0
  683. data/lib/etassia/renderer/user_journey.rb +209 -0
  684. data/lib/etassia/renderer/xy_chart.rb +180 -0
  685. data/lib/etassia/renderer.rb +22 -0
  686. data/lib/etassia/runtime.rb +125 -0
  687. data/lib/etassia/source/frontmatter.rb +501 -0
  688. data/lib/etassia/source.rb +224 -0
  689. data/lib/etassia/svg/arrowhead.rb +205 -0
  690. data/lib/etassia/svg/circle.rb +19 -0
  691. data/lib/etassia/svg/document.rb +106 -0
  692. data/lib/etassia/svg/element.rb +209 -0
  693. data/lib/etassia/svg/ellipse.rb +23 -0
  694. data/lib/etassia/svg/escaping.rb +149 -0
  695. data/lib/etassia/svg/group.rb +64 -0
  696. data/lib/etassia/svg/line.rb +21 -0
  697. data/lib/etassia/svg/numbers.rb +53 -0
  698. data/lib/etassia/svg/path.rb +67 -0
  699. data/lib/etassia/svg/path_geometry.rb +378 -0
  700. data/lib/etassia/svg/polygon.rb +25 -0
  701. data/lib/etassia/svg/polyline.rb +28 -0
  702. data/lib/etassia/svg/rect.rb +34 -0
  703. data/lib/etassia/svg/style.rb +50 -0
  704. data/lib/etassia/svg/text.rb +181 -0
  705. data/lib/etassia/svg/tspan.rb +48 -0
  706. data/lib/etassia/svg.rb +38 -0
  707. data/lib/etassia/text_measurement/arial_advances.rb +122 -0
  708. data/lib/etassia/text_measurement/dejavu_advances.rb +111 -0
  709. data/lib/etassia/text_measurement/times_advances.rb +64 -0
  710. data/lib/etassia/text_measurement/trebuchet_advances.rb +53 -0
  711. data/lib/etassia/text_measurement/trebuchet_kerning.rb +29 -0
  712. data/lib/etassia/text_measurement.rb +198 -0
  713. data/lib/etassia/theme/builtin/dark.yml +229 -0
  714. data/lib/etassia/theme/builtin/default.yml +239 -0
  715. data/lib/etassia/theme/builtin/high_contrast.yml +231 -0
  716. data/lib/etassia/theme/builtin/light.yml +229 -0
  717. data/lib/etassia/theme/color_palette.rb +97 -0
  718. data/lib/etassia/theme/completion.rb +50 -0
  719. data/lib/etassia/theme/effect_styles.rb +34 -0
  720. data/lib/etassia/theme/registry.rb +45 -0
  721. data/lib/etassia/theme/shape_styles.rb +34 -0
  722. data/lib/etassia/theme/spacing_config.rb +30 -0
  723. data/lib/etassia/theme/typography.rb +40 -0
  724. data/lib/etassia/theme.rb +73 -0
  725. data/lib/etassia/version.rb +5 -0
  726. data/lib/etassia.rb +40 -0
  727. metadata +865 -0
@@ -0,0 +1,1472 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "elk_placement"
5
+ require_relative "../diagram/architecture"
6
+ require_relative "../notation/mermaid/ir_adapters/architecture"
7
+ require_relative "../renderer/architecture_edge_router"
8
+
9
+ module Etassia
10
+ module Layout
11
+ # Transforms architecture diagram models into positioned layouts.
12
+ class Architecture < Base
13
+ # Default dimensions
14
+ # Service icon boxes are 80x80 in mmdc; the label sits below the box.
15
+ # Without an icon, Mermaid's fallback outline extends 10px right and
16
+ # 5px above that placement box.
17
+ DEFAULT_SERVICE_WIDTH = 80
18
+ DEFAULT_SERVICE_HEIGHT = 80
19
+ FALLBACK_SERVICE_WIDTH = 90
20
+ FALLBACK_SERVICE_HEIGHT = 85
21
+ SERVICE_LABEL_OFFSET = 16
22
+ # Group padding measured from mmdc: 42.5 on three sides, 17 more below
23
+ # for the service labels.
24
+ DEFAULT_GROUP_PADDING = 42.5
25
+ GROUP_LABEL_ALLOWANCE = 17
26
+ EMPTY_GROUP_SIZE = 112
27
+ EMPTY_GROUP_GAP = 98
28
+ # Centre-to-centre spacing of the grid that edge sides imply.
29
+ GRID_PITCH_X = 201
30
+ GRID_PITCH_Y = 201
31
+ GRID_ORIGIN = 60
32
+ COMPONENT_GAP = 100
33
+ BLOCK_GAP = 100
34
+ DEFAULT_SPACING = 40
35
+ DEFAULT_ICON_SIZE = 24
36
+ DEFAULT_JUNCTION_SIZE = 12
37
+ # Mermaid's force layout distributes four disconnected services around
38
+ # the canvas instead of flattening them into a row. These rounded
39
+ # offsets preserve that stable visual order while keeping the layout
40
+ # deterministic.
41
+ FOUR_SERVICE_OFFSETS = [
42
+ [143, 296].freeze,
43
+ [0, 155].freeze,
44
+ [95, 0].freeze,
45
+ [250, 131].freeze,
46
+ ].freeze
47
+
48
+ VALID_SIDES = %w[L R T B].freeze
49
+ LABEL_FONT_ROLES = {
50
+ group: [:font_size_title, 14],
51
+ service: [:font_size_normal, 12],
52
+ edge: [:font_size_small, 10],
53
+ }.freeze
54
+
55
+ SemanticGroup = Struct.new(
56
+ :id, :label, :icon, :parent_id, keyword_init: true
57
+ )
58
+ SemanticService = Struct.new(
59
+ :id, :label, :icon, :group_id, keyword_init: true
60
+ )
61
+ SemanticJunction = Struct.new(:id, :group_id, keyword_init: true)
62
+ SemanticEdge = Struct.new(
63
+ :from_id, :to_id, :from_position, :to_position, :label,
64
+ keyword_init: true
65
+ )
66
+ SemanticDiagram = Struct.new(
67
+ :id, :title, :direction, :theme, :acc_title, :acc_descr,
68
+ :groups, :services, :junctions, :edges, keyword_init: true
69
+ )
70
+ private_constant :SemanticGroup, :SemanticService, :SemanticJunction,
71
+ :SemanticEdge, :SemanticDiagram
72
+
73
+ class Point < Lutaml::Model::Serializable
74
+ attribute :x, :float
75
+ attribute :y, :float
76
+ end
77
+
78
+ class Label < Lutaml::Model::Serializable
79
+ attribute :text, :string
80
+ attribute :x, :float
81
+ attribute :y, :float
82
+ attribute :font_size, :float
83
+ attribute :font_weight, :string
84
+ attribute :text_anchor, :string
85
+ end
86
+
87
+ class Node < Lutaml::Model::Serializable
88
+ attribute :id, :string
89
+ attribute :x, :float
90
+ attribute :y, :float
91
+ attribute :width, :float
92
+ attribute :height, :float
93
+ attribute :labels, Label, collection: true, default: -> { [] }
94
+ attribute :kind, :string
95
+ attribute :icon, :string
96
+ attribute :icon_x, :float
97
+ attribute :icon_y, :float
98
+ attribute :center_x, :float
99
+ attribute :center_y, :float
100
+ attribute :radius, :float
101
+ attribute :group_id, :string
102
+ attribute :parent_id, :string
103
+ end
104
+
105
+ class Section < Lutaml::Model::Serializable
106
+ attribute :start_point, Point
107
+ attribute :end_point, Point
108
+ attribute :bend_points, Point, collection: true, default: -> { [] }
109
+ end
110
+
111
+ class Edge < Lutaml::Model::Serializable
112
+ attribute :id, :string
113
+ attribute :source, :string
114
+ attribute :target, :string
115
+ attribute :sections, Section, collection: true, default: -> { [] }
116
+ attribute :labels, Label, collection: true, default: -> { [] }
117
+ end
118
+
119
+ class Scene < Layout::Scene
120
+ attribute :view_box, :string
121
+ attribute :children, Node, collection: true, default: -> { [] }
122
+ attribute :edges, Edge, collection: true, default: -> { [] }
123
+ end
124
+
125
+ ElkPositions = Data.define(:services, :junctions, :groups)
126
+ private_constant :ElkPositions
127
+
128
+ # :grid (the byte-compatible default) or :elk. The explicit switch
129
+ # lets the parity ratchet measure real elkrb output before it becomes
130
+ # the default.
131
+ attr_accessor :placement
132
+
133
+ # Converts an architecture diagram to a positioned layout structure
134
+ #
135
+ # @param diagram [Diagram::Architecture] the diagram to transform
136
+ # @return [Hash] positioned layout hash
137
+ def build_graph(diagram)
138
+ diagram = semantic_diagram(ir_graph(diagram))
139
+ hierarchy = build_hierarchy(diagram)
140
+ services, junctions, groups = position_nodes(diagram, hierarchy)
141
+ positioned_graph(diagram, services, junctions, groups)
142
+ end
143
+
144
+ private
145
+
146
+ def positioned_graph(diagram, services, junctions, groups)
147
+ edges = position_edges(diagram, services.merge(junctions))
148
+ { services: services, junctions: junctions, groups: groups,
149
+ edges: edges,
150
+ width: calculate_total_width(services, junctions, groups),
151
+ height: calculate_total_height(services, junctions, groups) }
152
+ end
153
+
154
+ ENTITY_ROLES = %w[group service junction].freeze
155
+ private_constant :ENTITY_ROLES
156
+
157
+ def ir_graph(diagram)
158
+ return diagram if diagram.is_a?(IR::Graph)
159
+
160
+ Notation::Mermaid::IRAdapters::Architecture.call(diagram)
161
+ end
162
+
163
+ def semantic_diagram(graph)
164
+ children, nodes_by_id = semantic_context(graph)
165
+ SemanticDiagram.new(
166
+ **semantic_attributes(graph, children, nodes_by_id),
167
+ )
168
+ end
169
+
170
+ def semantic_context(graph)
171
+ children = graph.nodes.group_by(&:parent_id)
172
+ nodes_by_id = graph.nodes.to_h { |node| [node.id, node] }
173
+ [children, nodes_by_id]
174
+ end
175
+
176
+ def semantic_attributes(graph, children, nodes_by_id)
177
+ semantic_metadata(graph, children).merge(
178
+ semantic_collections(graph, children, nodes_by_id),
179
+ )
180
+ end
181
+
182
+ def semantic_metadata(graph, children)
183
+ settings = graph.nodes.find { |node| node.role == "diagram_settings" }
184
+ fields = semantic_fields(children[settings&.id])
185
+ {
186
+ id: fields["diagram_identifier"], title: graph.label,
187
+ direction: fields["layout_direction"], theme: fields["theme"],
188
+ acc_title: graph.accessibility_title,
189
+ acc_descr: graph.accessibility_description
190
+ }
191
+ end
192
+
193
+ def semantic_collections(graph, children, nodes_by_id)
194
+ entities = graph.nodes.select do |node|
195
+ ENTITY_ROLES.include?(node.role)
196
+ end
197
+ {
198
+ groups: semantic_groups(entities, children, nodes_by_id),
199
+ services: semantic_services(entities, children, nodes_by_id),
200
+ junctions: semantic_junctions(entities, children, nodes_by_id),
201
+ edges: semantic_edges(graph.edges, children, nodes_by_id),
202
+ }
203
+ end
204
+
205
+ def semantic_groups(entities, children, nodes_by_id)
206
+ entities.select { |node| node.role == "group" }.map do |node|
207
+ fields = semantic_fields(children[node.id])
208
+ SemanticGroup.new(
209
+ id: source_identifier(node, children), label: node.label,
210
+ icon: fields["icon"],
211
+ parent_id: source_container(fields, node, nodes_by_id, children,
212
+ "parent_identifier")
213
+ )
214
+ end
215
+ end
216
+
217
+ def semantic_services(entities, children, nodes_by_id)
218
+ entities.select { |node| node.role == "service" }.map do |node|
219
+ fields = semantic_fields(children[node.id])
220
+ SemanticService.new(
221
+ id: source_identifier(node, children), label: node.label,
222
+ icon: fields["icon"],
223
+ group_id: source_container(fields, node, nodes_by_id, children,
224
+ "group_identifier")
225
+ )
226
+ end
227
+ end
228
+
229
+ def semantic_junctions(entities, children, nodes_by_id)
230
+ entities.select { |node| node.role == "junction" }.map do |node|
231
+ fields = semantic_fields(children[node.id])
232
+ SemanticJunction.new(
233
+ id: source_identifier(node, children),
234
+ group_id: source_container(fields, node, nodes_by_id, children,
235
+ "group_identifier"),
236
+ )
237
+ end
238
+ end
239
+
240
+ def semantic_edges(edges, children, nodes_by_id)
241
+ edges.map do |edge|
242
+ fields = semantic_fields(children[edge.parent_id])
243
+ SemanticEdge.new(
244
+ from_id: source_identifier(nodes_by_id[edge.source_id], children),
245
+ to_id: source_identifier(nodes_by_id[edge.target_id], children),
246
+ from_position: fields["source_position"],
247
+ to_position: fields["target_position"], label: edge.label
248
+ )
249
+ end
250
+ end
251
+
252
+ def semantic_fields(nodes)
253
+ Array(nodes).to_h { |node| [node.role, node.label] }
254
+ end
255
+
256
+ def source_identifier(node, children)
257
+ return unless node
258
+
259
+ semantic_fields(children[node.id]).fetch("original_identifier", node.id)
260
+ end
261
+
262
+ def source_container(fields, node, nodes_by_id, children, field)
263
+ fields.fetch(field) do
264
+ source_identifier(nodes_by_id[node.parent_id], children)
265
+ end
266
+ end
267
+
268
+ def scene(diagram)
269
+ return elk_scene(diagram) if placement == :elk
270
+
271
+ graph = build_graph(diagram)
272
+ routed = route_edges(graph)
273
+ positioned_scene(graph, routed)
274
+ end
275
+
276
+ def positioned_scene(graph, routed)
277
+ width, height = scene_dimensions(graph, routed)
278
+ Scene.new(
279
+ width: width, height: height, view_box: "0 0 #{width} #{height}",
280
+ children: typed_nodes(graph), edges: typed_edges(routed)
281
+ )
282
+ end
283
+
284
+ def elk_scene(source)
285
+ diagram = semantic_diagram(ir_graph(source))
286
+ elk_graph = build_elk_graph(diagram)
287
+ ElkPlacement.apply(elk_graph)
288
+ graph = positioned_elk_graph(diagram, elk_graph)
289
+ routed = elk_routed_edges(diagram, elk_graph, graph)
290
+ positioned_scene(graph, routed)
291
+ end
292
+
293
+ def elk_routed_edges(diagram, elk_graph, graph)
294
+ return routed_elk_edges(elk_graph) if diagram.groups.empty?
295
+
296
+ route_edges(graph)
297
+ end
298
+
299
+ def build_elk_graph(diagram)
300
+ {
301
+ id: diagram.id || "architecture",
302
+ children: elk_children(diagram),
303
+ edges: elk_root_edges(diagram),
304
+ layoutOptions: elk_layout_options,
305
+ }
306
+ end
307
+
308
+ def elk_root_edges(diagram)
309
+ return [] unless diagram.groups.empty?
310
+
311
+ ids = diagram.services.map(&:id) + diagram.junctions.map(&:id)
312
+ elk_edges(diagram.edges, ids.to_h { |id| [id, true] })
313
+ end
314
+
315
+ def elk_layout_options
316
+ {
317
+ ElkOptions::ALGORITHM => ALGORITHM_LAYERED,
318
+ ElkOptions::DIRECTION => DIRECTION_DOWN,
319
+ ElkOptions::NODE_NODE_SPACING => DEFAULT_SPACING,
320
+ ElkOptions::LAYER_SPACING => DEFAULT_SPACING,
321
+ }
322
+ end
323
+
324
+ def elk_children(diagram, parent_id = nil)
325
+ groups = elk_children_for(diagram.groups, :parent_id, parent_id)
326
+ services = elk_children_for(diagram.services, :group_id, parent_id)
327
+ junctions = elk_children_for(diagram.junctions, :group_id, parent_id)
328
+ groups.map { |group| elk_group_node(group, diagram) } +
329
+ elk_service_nodes(services) + elk_junction_nodes(junctions)
330
+ end
331
+
332
+ def elk_children_for(records, association, parent_id)
333
+ records.select do |record|
334
+ record.public_send(association) == parent_id
335
+ end
336
+ end
337
+
338
+ def elk_group_node(group, diagram)
339
+ {
340
+ id: group.id, width: EMPTY_GROUP_SIZE, height: EMPTY_GROUP_SIZE,
341
+ children: elk_children(diagram, group.id), edges: [],
342
+ metadata: { kind: :group, model: group }
343
+ }
344
+ end
345
+
346
+ def elk_service_nodes(services)
347
+ services.map do |service|
348
+ dimensions = calculate_service_dimensions(service)
349
+ { id: service.id, **dimensions,
350
+ metadata: { kind: :service, model: service } }
351
+ end
352
+ end
353
+
354
+ def elk_junction_nodes(junctions)
355
+ junctions.map do |junction|
356
+ { id: junction.id, width: DEFAULT_JUNCTION_SIZE,
357
+ height: DEFAULT_JUNCTION_SIZE,
358
+ metadata: { kind: :junction, model: junction } }
359
+ end
360
+ end
361
+
362
+ def elk_edges(edges, node_ids)
363
+ edges.filter_map.with_index do |edge, index|
364
+ next unless node_ids[edge.from_id] && node_ids[edge.to_id]
365
+
366
+ { id: "edge_#{index}", sources: [edge.from_id],
367
+ targets: [edge.to_id], metadata: { edge: edge } }
368
+ end
369
+ end
370
+
371
+ def positioned_elk_graph(diagram, elk_graph)
372
+ positions = ElkPositions.new(
373
+ services: {}, junctions: {}, groups: {},
374
+ )
375
+ collect_elk_positions(elk_graph.fetch(:children), positions)
376
+ normalize_elk_group_bounds!(diagram, positions)
377
+ positioned_elk_result(diagram, positions)
378
+ end
379
+
380
+ def positioned_elk_result(diagram, positions)
381
+ {
382
+ services: positions.services, junctions: positions.junctions,
383
+ groups: positions.groups,
384
+ edges: position_edges(diagram, positioned_elk_nodes(positions)),
385
+ width: calculate_total_width(*positions.to_h.values),
386
+ height: calculate_total_height(*positions.to_h.values)
387
+ }
388
+ end
389
+
390
+ def positioned_elk_nodes(positions)
391
+ positions.services.merge(positions.junctions)
392
+ end
393
+
394
+ def collect_elk_positions(nodes, positions, parent_x = 0, parent_y = 0)
395
+ nodes.each do |node|
396
+ x_coord = parent_x + node.fetch(:x)
397
+ y_coord = parent_y + node.fetch(:y)
398
+ collect_elk_node(node, x_coord, y_coord, positions)
399
+ collect_elk_positions(
400
+ Array(node[:children]), positions, x_coord, y_coord
401
+ )
402
+ end
403
+ end
404
+
405
+ def collect_elk_node(node, x_coord, y_coord, positions)
406
+ metadata = node.fetch(:metadata)
407
+ model = metadata.fetch(:model)
408
+ target = {
409
+ service: positions.services,
410
+ junction: positions.junctions,
411
+ group: positions.groups,
412
+ }
413
+ .fetch(metadata.fetch(:kind))
414
+ target[model.id] = positioned_elk_node(node, x_coord, y_coord, model)
415
+ end
416
+
417
+ def positioned_elk_node(node, x_coord, y_coord, model)
418
+ kind = node.dig(:metadata, :kind)
419
+ height = node.fetch(:height)
420
+ if kind == :group && Array(node[:children]).any?
421
+ height += GROUP_LABEL_ALLOWANCE
422
+ end
423
+ {
424
+ kind => model, x: x_coord, y: y_coord,
425
+ width: node.fetch(:width), height: node.fetch(:height),
426
+ group_id: model.respond_to?(:group_id) ? model.group_id || :root : nil
427
+ }.merge(height: height)
428
+ end
429
+
430
+ def normalize_elk_group_bounds!(diagram, positions)
431
+ deepest_groups_first(diagram.groups).each do |group|
432
+ normalize_elk_group!(diagram, group, positions)
433
+ end
434
+ pack_elk_siblings!(diagram, nil, positions)
435
+ end
436
+
437
+ def normalize_elk_group!(diagram, group, positions)
438
+ pack_elk_siblings!(diagram, group.id, positions)
439
+ bounds = positions.groups.fetch(group.id)
440
+ members = elk_direct_members(diagram, group.id, positions)
441
+ return if members.empty?
442
+
443
+ resize_elk_group!(bounds, members)
444
+ end
445
+
446
+ def resize_elk_group!(bounds, members)
447
+ width = elk_group_span(bounds, members, :x, :width)
448
+ height = elk_group_height(bounds, members)
449
+ bounds[:width] = [bounds[:width], width].max
450
+ bounds[:height] = [bounds[:height], height].max
451
+ end
452
+
453
+ def elk_direct_members(diagram, parent_id, positions)
454
+ group_id = parent_id || :root
455
+ elk_member_nodes(positions, group_id) +
456
+ elk_child_groups(diagram, parent_id, positions.groups)
457
+ end
458
+
459
+ def elk_member_nodes(positions, group_id)
460
+ [positions.services, positions.junctions].flat_map do |nodes|
461
+ nodes.values.select { |node| node[:group_id] == group_id }
462
+ end
463
+ end
464
+
465
+ def elk_child_groups(diagram, parent_id, groups)
466
+ diagram.groups.filter_map do |child|
467
+ groups[child.id] if child.parent_id == parent_id
468
+ end
469
+ end
470
+
471
+ def pack_elk_siblings!(diagram, parent_id, positions)
472
+ siblings = elk_direct_members(diagram, parent_id, positions)
473
+ .sort_by { |node| node[:x] }
474
+ cursor = nil
475
+ siblings.each do |node|
476
+ shift = cursor ? [cursor - node[:x], 0].max : 0
477
+ shift_elk_component!(diagram, node, shift, positions)
478
+ cursor = node[:x] + node[:width] + DEFAULT_SPACING
479
+ end
480
+ end
481
+
482
+ def shift_elk_component!(diagram, node, shift, positions)
483
+ return if shift.zero?
484
+
485
+ if node[:group]
486
+ shift_elk_group!(diagram, node, shift, positions)
487
+ else
488
+ node[:x] += shift
489
+ end
490
+ end
491
+
492
+ def shift_elk_group!(diagram, node, shift, positions)
493
+ ids = descendant_group_ids(diagram, node[:group].id)
494
+ positions.groups.slice(*ids).each_value do |group|
495
+ group[:x] += shift
496
+ end
497
+ positioned_elk_nodes(positions).each_value do |member|
498
+ shift_elk_member!(member, ids, shift)
499
+ end
500
+ end
501
+
502
+ def shift_elk_member!(member, group_ids, shift)
503
+ member[:x] += shift if group_ids.include?(member[:group_id])
504
+ end
505
+
506
+ def elk_group_span(group, members, start, size)
507
+ members.map { |member| member[start] + member[size] }.max - group[start]
508
+ end
509
+
510
+ def elk_group_height(group, members)
511
+ bottoms = members.map do |member|
512
+ allowance = member[:service] ? SERVICE_LABEL_OFFSET : 0
513
+ member[:y] + member[:height] + allowance
514
+ end
515
+ bottoms.max - group[:y]
516
+ end
517
+
518
+ def routed_elk_edges(elk_graph)
519
+ elk_graph.fetch(:edges).filter_map do |edge|
520
+ section = Array(edge[:sections]).first
521
+ next unless section
522
+
523
+ { edge: edge.dig(:metadata, :edge),
524
+ points: elk_section_points(section) }
525
+ end
526
+ end
527
+
528
+ def elk_section_points(section)
529
+ [section.fetch(:startPoint), *Array(section[:bendPoints]),
530
+ section.fetch(:endPoint)]
531
+ end
532
+
533
+ def typed_nodes(graph)
534
+ typed_groups(graph[:groups]) + typed_services(graph[:services]) +
535
+ typed_junctions(graph[:junctions])
536
+ end
537
+
538
+ def typed_groups(groups)
539
+ groups.values.map { |info| typed_group(info) }
540
+ end
541
+
542
+ def typed_group(info)
543
+ group = info[:group]
544
+ attributes = node_geometry(info).merge(
545
+ id: group.id, kind: "group", icon: group.icon,
546
+ icon_x: info[:x] + info[:width] - 30,
547
+ icon_y: info[:y] + 25, parent_id: group.parent_id,
548
+ labels: group_labels(group, info)
549
+ )
550
+ Node.new(**attributes)
551
+ end
552
+
553
+ def group_labels(group, info)
554
+ positioned_label(
555
+ group.label, info[:x] + 10, info[:y] + 20,
556
+ **label_typography(:group)
557
+ )
558
+ end
559
+
560
+ def typed_services(services)
561
+ services.values.map { |info| typed_service(info) }
562
+ end
563
+
564
+ def typed_service(info)
565
+ service = info[:service]
566
+ attributes = service_geometry(service, info).merge(
567
+ id: service.id, kind: "service",
568
+ icon: service.icon, icon_x: horizontal_center(info),
569
+ icon_y: info[:y] + (info[:height] / 2),
570
+ group_id: normalized_group_id(info[:group_id]),
571
+ labels: service_labels(service, info)
572
+ )
573
+ Node.new(**attributes)
574
+ end
575
+
576
+ def service_geometry(service, info)
577
+ return node_geometry(info) if service_icon?(service)
578
+
579
+ {
580
+ x: info[:x], y: info[:y] - 5,
581
+ width: FALLBACK_SERVICE_WIDTH, height: FALLBACK_SERVICE_HEIGHT
582
+ }
583
+ end
584
+
585
+ def service_labels(service, info)
586
+ positioned_label(
587
+ service.label, horizontal_center(info),
588
+ info[:y] + info[:height] + SERVICE_LABEL_OFFSET,
589
+ **label_typography(:service)
590
+ )
591
+ end
592
+
593
+ def horizontal_center(info)
594
+ info[:x] + (info[:width] / 2)
595
+ end
596
+
597
+ def node_geometry(info)
598
+ { x: info[:x], y: info[:y], width: info[:width], height: info[:height] }
599
+ end
600
+
601
+ def typed_junctions(junctions)
602
+ junctions.values.map { |info| typed_junction(info) }
603
+ end
604
+
605
+ def typed_junction(info)
606
+ attributes = node_geometry(info).merge(
607
+ id: info[:junction].id, kind: "junction",
608
+ group_id: normalized_group_id(info[:group_id])
609
+ )
610
+ Node.new(**attributes, **circle_geometry(info))
611
+ end
612
+
613
+ def circle_geometry(info)
614
+ { center_x: horizontal_center(info),
615
+ center_y: info[:y] + (info[:height] / 2),
616
+ radius: info[:width] / 2 }
617
+ end
618
+
619
+ def normalized_group_id(group_id)
620
+ group_id == :root ? nil : group_id
621
+ end
622
+
623
+ def positioned_label(text, x_coord, y_coord, **typography)
624
+ return [] unless text
625
+
626
+ [Label.new(text: text, x: x_coord, y: y_coord, **typography)]
627
+ end
628
+
629
+ def typed_edges(routed_edges)
630
+ routed_edges.map.with_index do |routed, index|
631
+ typed_edge(routed, index)
632
+ end
633
+ end
634
+
635
+ def typed_edge(routed, index)
636
+ edge = routed[:edge]
637
+ points = routed[:points].map { |point| typed_point(point) }
638
+ Edge.new(
639
+ id: "edge_#{index}", source: edge.from_id, target: edge.to_id,
640
+ sections: [typed_section(points)],
641
+ labels: edge_label(edge.label, points)
642
+ )
643
+ end
644
+
645
+ def typed_point(point)
646
+ Point.new(x: point[:x], y: point[:y])
647
+ end
648
+
649
+ def typed_section(points)
650
+ Section.new(start_point: points.first, end_point: points.last,
651
+ bend_points: points[1...-1])
652
+ end
653
+
654
+ def edge_label(text, points)
655
+ return [] unless text && !text.empty?
656
+
657
+ first, second = points
658
+ [Label.new(text: text, x: midpoint(first.x, second.x),
659
+ y: midpoint(first.y, second.y) - 5,
660
+ **label_typography(:edge))]
661
+ end
662
+
663
+ def label_typography(kind)
664
+ role, target = LABEL_FONT_ROLES.fetch(kind)
665
+ { font_size: scaled_font_size(role, target) }.merge(label_style(kind))
666
+ end
667
+
668
+ def label_style(kind)
669
+ {
670
+ group: { font_weight: typography_value(:font_weight_bold) || "bold" },
671
+ service: { text_anchor: "middle" },
672
+ edge: { text_anchor: "middle" },
673
+ }.fetch(kind)
674
+ end
675
+
676
+ def scaled_font_size(role, target)
677
+ default = Theme::Registry.get(:default).typography.public_send(role)
678
+ configured = typography_value(role)
679
+ configured = default unless configured&.positive?
680
+ size = configured * target / default
681
+ size.to_i == size ? size.to_i : size
682
+ rescue NoMethodError
683
+ target
684
+ end
685
+
686
+ def typography_value(role)
687
+ theme.typography&.public_send(role)
688
+ rescue NoMethodError
689
+ nil
690
+ end
691
+
692
+ def midpoint(first, second)
693
+ (first + second) / 2
694
+ end
695
+
696
+ def route_edges(graph)
697
+ graph[:edges].map do |edge_info|
698
+ edge = edge_info[:edge]
699
+ from = route_endpoint(edge_info, find_node(graph, edge.from_id),
700
+ :from)
701
+ to = route_endpoint(edge_info, find_node(graph, edge.to_id), :to)
702
+ { edge: edge, points: routed_points(edge, from, to, graph) }
703
+ end
704
+ end
705
+
706
+ def route_endpoint(edge_info, box, endpoint)
707
+ side = edge_info[:"#{endpoint}_side"]
708
+ point = { x: edge_info[:"#{endpoint}_x"],
709
+ y: edge_info[:"#{endpoint}_y"] }
710
+ { point: fallback_service_endpoint(point, box, side),
711
+ box: box, side: side }
712
+ end
713
+
714
+ def fallback_service_endpoint(point, box, side)
715
+ service = box[:service]
716
+ return point unless service && !service_icon?(service)
717
+
718
+ return point.merge(y: point[:y] - 5) if side == "T"
719
+ return point.merge(x: point[:x] + 10) if side == "R"
720
+
721
+ point
722
+ end
723
+
724
+ def routed_points(edge, from, to, graph)
725
+ architecture_edge_router.route(
726
+ from: from, to: to, obstacles: obstacles_for(edge, graph),
727
+ )
728
+ rescue StandardError
729
+ [from[:point], to[:point]]
730
+ end
731
+
732
+ def architecture_edge_router
733
+ @architecture_edge_router ||=
734
+ Renderer::ArchitectureEdgeRouter.new
735
+ end
736
+
737
+ def obstacles_for(edge, graph)
738
+ excluded_ids = [edge.from_id, edge.to_id]
739
+ node_obstacles(graph, excluded_ids) + group_obstacles(edge, graph)
740
+ end
741
+
742
+ def node_obstacles(graph, excluded_ids)
743
+ nodes = graph[:services].values + graph[:junctions].values
744
+ nodes.reject { |info| excluded_ids.include?(positioned_node_id(info)) }
745
+ end
746
+
747
+ def group_obstacles(edge, graph)
748
+ excluded = endpoint_ancestor_groups(edge.from_id, graph) |
749
+ endpoint_ancestor_groups(edge.to_id, graph)
750
+ graph[:groups].except(*excluded).values
751
+ end
752
+
753
+ def endpoint_ancestor_groups(node_id, graph)
754
+ ancestor_group_ids(find_node(graph, node_id)[:group_id], graph)
755
+ end
756
+
757
+ def ancestor_group_ids(group_id, graph)
758
+ ids = []
759
+ current_id = group_id
760
+ while current_id && current_id != :root && !ids.include?(current_id)
761
+ ids << current_id
762
+ current_id = graph[:groups][current_id]&.dig(:group)&.parent_id
763
+ end
764
+ ids
765
+ end
766
+
767
+ def find_node(graph, id)
768
+ graph[:services][id] || graph[:junctions][id]
769
+ end
770
+
771
+ def positioned_node_id(info)
772
+ info[:service]&.id || info[:junction]&.id
773
+ end
774
+
775
+ def scene_dimensions(graph, routed_edges)
776
+ points = routed_edges.flat_map { |routed| routed[:points] }
777
+ width = [graph[:width], *points.map { |point| point[:x] }].max
778
+ height = [graph[:height], *points.map { |point| point[:y] }].max
779
+ [width, height]
780
+ end
781
+
782
+ def build_hierarchy(diagram)
783
+ groups = hierarchy_groups(diagram.groups)
784
+ attach_hierarchy_groups(diagram.groups, groups)
785
+ { groups: groups,
786
+ services_by_group: grouped_nodes(diagram.services),
787
+ junctions_by_group: grouped_nodes(diagram.junctions) }
788
+ end
789
+
790
+ def hierarchy_groups(groups)
791
+ groups.to_h do |group|
792
+ [group.id, { group: group, parent_id: group.parent_id || :root,
793
+ children: [] }]
794
+ end
795
+ end
796
+
797
+ def attach_hierarchy_groups(groups, hierarchy)
798
+ groups.each do |group|
799
+ if group.parent_id && hierarchy[group.parent_id]
800
+ hierarchy[group.parent_id][:children] << group.id
801
+ end
802
+ end
803
+ end
804
+
805
+ def grouped_nodes(nodes)
806
+ nodes.group_by { |node| node.group_id || :root }
807
+ end
808
+
809
+ # Edge sides imply a grid, as in mmdc: "a:R -- L:b" puts b to the right
810
+ # of a. A diagram without edges, or whose grid would put a group box
811
+ # over a node or group that is not inside it, keeps the row layout.
812
+ # @return [Array(Hash, Hash, Hash)] services, junctions, group bounds
813
+ def position_nodes(diagram, hierarchy)
814
+ if four_disconnected_services?(diagram)
815
+ return bounded(diagram, position_four_services(diagram), {})
816
+ end
817
+
818
+ unless diagram.edges.empty?
819
+ placed = bounded(diagram, *position_on_grid(diagram))
820
+ return placed if groups_separate?(diagram, *placed)
821
+
822
+ placed = bounded(diagram, *position_in_blocks(diagram))
823
+ return placed if groups_separate?(diagram, *placed)
824
+ end
825
+ bounded(diagram, *position_by_rows(diagram, hierarchy))
826
+ end
827
+
828
+ def four_disconnected_services?(diagram)
829
+ diagram.services.length == FOUR_SERVICE_OFFSETS.length &&
830
+ diagram.edges.empty? && diagram.groups.empty? &&
831
+ diagram.junctions.empty?
832
+ end
833
+
834
+ def position_four_services(diagram)
835
+ diagram.services.zip(FOUR_SERVICE_OFFSETS).to_h do |service, offset|
836
+ dimensions = calculate_service_dimensions(service)
837
+ [service.id, {
838
+ service: service,
839
+ x: DEFAULT_SPACING + offset[0],
840
+ y: DEFAULT_SPACING + offset[1],
841
+ width: dimensions[:width], height: dimensions[:height],
842
+ group_id: :root
843
+ }]
844
+ end
845
+ end
846
+
847
+ def bounded(diagram, services, junctions)
848
+ [services, junctions,
849
+ calculate_group_bounds(diagram, services, junctions)]
850
+ end
851
+
852
+ def groups_separate?(diagram, services, junctions, bounds)
853
+ nodes = services.values + junctions.values
854
+ bounds.all? do |id, box|
855
+ inside = descendant_group_ids(diagram, id)
856
+ nodes.all? do |node|
857
+ inside.include?(node[:group_id]) || !boxes_overlap?(box, node)
858
+ end && sibling_groups_clear?(diagram, bounds, id, inside)
859
+ end
860
+ end
861
+
862
+ def sibling_groups_clear?(diagram, bounds, id, inside)
863
+ related = inside | ancestor_ids(diagram, id)
864
+ bounds.none? do |other_id, other|
865
+ !related.include?(other_id) && boxes_overlap?(bounds[id], other)
866
+ end
867
+ end
868
+
869
+ def descendant_group_ids(diagram, id)
870
+ ids = [id]
871
+ ids.each do |current|
872
+ diagram.groups.each do |group|
873
+ next if group.parent_id != current || ids.include?(group.id)
874
+
875
+ ids << group.id
876
+ end
877
+ end
878
+ ids
879
+ end
880
+
881
+ def ancestor_ids(diagram, id)
882
+ by_id = diagram.groups.to_h { |group| [group.id, group] }
883
+ ids = []
884
+ current = by_id[id]&.parent_id
885
+ while current && !ids.include?(current)
886
+ ids << current
887
+ current = by_id[current]&.parent_id
888
+ end
889
+ ids
890
+ end
891
+
892
+ def boxes_overlap?(first, second)
893
+ spans_overlap?(first, second, :x, :width) &&
894
+ spans_overlap?(first, second, :y, :height)
895
+ end
896
+
897
+ def spans_overlap?(first, second, start, size)
898
+ first[start] < second[start] + second[size] &&
899
+ second[start] < first[start] + first[size]
900
+ end
901
+
902
+ def position_by_rows(diagram, hierarchy)
903
+ services = position_services(diagram, hierarchy)
904
+ [services, position_junctions(diagram, hierarchy, services)]
905
+ end
906
+
907
+ def position_on_grid(diagram)
908
+ nodes = grid_nodes(diagram)
909
+ cells = grid_cells(nodes, diagram.edges)
910
+ entries = cells.to_h do |id, (x_coord, y_coord)|
911
+ [id, grid_entry(nodes.fetch(id), x_coord, y_coord)]
912
+ end
913
+ services_and_junctions(nodes, entries)
914
+ end
915
+
916
+ def services_and_junctions(nodes, entries)
917
+ entries.partition { |id, _| nodes.fetch(id)[:service] }.map(&:to_h)
918
+ end
919
+
920
+ # Each top-level group is laid out as its own grid block, and the blocks
921
+ # are placed along the sides of the edges that join them, as mmdc
922
+ # offsets whole groups. A node outside every group is a block of its own.
923
+ def position_in_blocks(diagram)
924
+ nodes = grid_nodes(diagram)
925
+ owner = block_owners(diagram, nodes)
926
+ inner, outer = diagram.edges.partition do |edge|
927
+ owner[edge.from_id] == owner[edge.to_id]
928
+ end
929
+ shifts = block_shifts(nodes, owner, inner, outer)
930
+ entries = block_entries(nodes, owner, inner, shifts)
931
+ services_and_junctions(nodes, entries)
932
+ end
933
+
934
+ def block_entries(nodes, owner, inner, shifts)
935
+ shifts.flat_map do |block, (shift_x, shift_y)|
936
+ members = block_members(nodes, owner, block)
937
+ grid_cells(members, inner).map do |id, (x_coord, y_coord)|
938
+ [id, grid_entry(nodes.fetch(id), x_coord + shift_x,
939
+ y_coord + shift_y)]
940
+ end
941
+ end.to_h
942
+ end
943
+
944
+ def block_members(nodes, owner, block)
945
+ nodes.select { |id, _| owner[id] == block }
946
+ end
947
+
948
+ def block_owners(diagram, nodes)
949
+ nodes.to_h do |id, node|
950
+ group = node[:group_id]
951
+ top = ([group] + ancestor_ids(diagram, group)).last
952
+ [id, top == :root ? id : top]
953
+ end
954
+ end
955
+
956
+ # block => [x shift, y shift]. Blocks take the cell of the side the
957
+ # joining edge names; each grid column and row is as wide as its
958
+ # largest block.
959
+ def block_shifts(nodes, owner, inner, outer)
960
+ blocks = owner.values.uniq
961
+ extents = block_extents(blocks, nodes, owner, inner)
962
+ cells = place_blocks(blocks, owner, outer)
963
+ x_starts = block_starts(cells, extents, 0, :right)
964
+ y_starts = block_starts(cells, extents, 1, :bottom)
965
+ blocks.to_h do |block|
966
+ col, row = cells.fetch(block)
967
+ [block, [x_starts.fetch(col), y_starts.fetch(row)]]
968
+ end
969
+ end
970
+
971
+ def block_extents(blocks, nodes, owner, inner)
972
+ blocks.to_h do |block|
973
+ members = block_members(nodes, owner, block)
974
+ [block, block_extent(members, grid_cells(members, inner))]
975
+ end
976
+ end
977
+
978
+ def block_extent(members, cells)
979
+ {
980
+ right: cells.map { |id, c| c[0] + (members[id][:width] / 2.0) }.max,
981
+ bottom: cells.map { |id, c| c[1] + (members[id][:height] / 2.0) }.max,
982
+ }
983
+ end
984
+
985
+ def place_blocks(blocks, owner, outer)
986
+ links = edge_links(outer) do |edge|
987
+ [owner[edge.from_id], owner[edge.to_id]]
988
+ end
989
+ cells = {}
990
+ offset = 0
991
+ each_placed_component(blocks, links) do |placed|
992
+ shift_component_cells(cells, placed, offset)
993
+ offset += placed.values.map(&:first).max + 1
994
+ end
995
+ cells
996
+ end
997
+
998
+ def shift_component_cells(cells, placed, offset)
999
+ placed.each { |block, (col, row)| cells[block] = [offset + col, row] }
1000
+ end
1001
+
1002
+ def block_starts(cells, extents, axis, edge)
1003
+ tracks = cells.group_by { |_, cell| cell[axis] }
1004
+ start = 0
1005
+ tracks.sort.to_h do |index, members|
1006
+ at = start
1007
+ start += track_size(members, extents, edge) + BLOCK_GAP
1008
+ [index, at]
1009
+ end
1010
+ end
1011
+
1012
+ def track_size(members, extents, edge)
1013
+ members.map { |block, _| extents[block][edge] }.max
1014
+ end
1015
+
1016
+ def grid_nodes(diagram)
1017
+ services = diagram.services.to_h do |service|
1018
+ dims = calculate_service_dimensions(service)
1019
+ [service.id, dims.merge(service: service,
1020
+ group_id: service.group_id || :root)]
1021
+ end
1022
+ junctions = diagram.junctions.to_h do |junction|
1023
+ [junction.id, { junction: junction, width: DEFAULT_JUNCTION_SIZE,
1024
+ height: DEFAULT_JUNCTION_SIZE,
1025
+ group_id: junction.group_id || :root }]
1026
+ end
1027
+ services.merge(junctions)
1028
+ end
1029
+
1030
+ def grid_entry(node, x_coord, y_coord)
1031
+ node.merge(x: x_coord - (node[:width] / 2.0),
1032
+ y: y_coord - (node[:height] / 2.0))
1033
+ end
1034
+
1035
+ # Pixel cell centres per node id; components sit side by side.
1036
+ def grid_cells(nodes, edges)
1037
+ links = grid_links(nodes, edges)
1038
+ cells = {}
1039
+ offset = GRID_ORIGIN
1040
+ each_placed_component(nodes.keys, links) do |placed|
1041
+ cells.merge!(pixel_cells(placed, offset))
1042
+ offset += pixel_width(placed) + COMPONENT_GAP
1043
+ end
1044
+ cells
1045
+ end
1046
+
1047
+ def pixel_cells(placed, offset)
1048
+ placed.transform_values do |(col, row)|
1049
+ [offset + (col * GRID_PITCH_X), GRID_ORIGIN + (row * GRID_PITCH_Y)]
1050
+ end
1051
+ end
1052
+
1053
+ def pixel_width(placed)
1054
+ (placed.values.map(&:first).max * GRID_PITCH_X) + DEFAULT_SERVICE_WIDTH
1055
+ end
1056
+
1057
+ # Yields each connected component's cells, moved so that its leftmost
1058
+ # column and topmost row are 0.
1059
+ def each_placed_component(ids, links)
1060
+ components(ids, links).each do |component|
1061
+ yield from_origin(place_component(component, links))
1062
+ end
1063
+ end
1064
+
1065
+ def from_origin(placed)
1066
+ min_col = placed.values.map(&:first).min
1067
+ min_row = placed.values.map(&:last).min
1068
+ placed.transform_values { |(col, row)| [col - min_col, row - min_row] }
1069
+ end
1070
+
1071
+ SIDE_STEPS = {
1072
+ "R" => [1, 0], "L" => [-1, 0], "T" => [0, -1], "B" => [0, 1]
1073
+ }.freeze
1074
+ private_constant :SIDE_STEPS
1075
+
1076
+ # id => [[neighbour id, [dx, dy] from id to neighbour], ...]
1077
+ def grid_links(nodes, edges)
1078
+ known = edges.select do |edge|
1079
+ nodes.key?(edge.from_id) && nodes.key?(edge.to_id)
1080
+ end
1081
+ edge_links(known) { |edge| [edge.from_id, edge.to_id] }
1082
+ end
1083
+
1084
+ # The block returns the [from, to] keys that an edge joins.
1085
+ def edge_links(edges)
1086
+ links = Hash.new { |hash, key| hash[key] = [] }
1087
+ edges.each do |edge|
1088
+ from, to = yield(edge)
1089
+ step = SIDE_STEPS.fetch(valid_side(edge.from_position, "R"))
1090
+ links[from] << [to, step]
1091
+ links[to] << [from, step.map(&:-@)]
1092
+ end
1093
+ links
1094
+ end
1095
+
1096
+ def components(ids, links)
1097
+ seen = {}
1098
+ ids.filter_map { |id| reachable_from(id, links, seen) unless seen[id] }
1099
+ end
1100
+
1101
+ def reachable_from(start, links, seen)
1102
+ queue = [start]
1103
+ seen[start] = true
1104
+ queue.each do |current|
1105
+ links[current].map(&:first).each do |neighbour|
1106
+ next if seen[neighbour]
1107
+
1108
+ seen[neighbour] = true
1109
+ queue << neighbour
1110
+ end
1111
+ end
1112
+ queue
1113
+ end
1114
+
1115
+ def place_component(component, links)
1116
+ placed = { component.first => [0, 0] }
1117
+ taken = { [0, 0] => true }
1118
+ component.each do |id|
1119
+ links[id].each do |neighbour, step|
1120
+ next if placed.key?(neighbour)
1121
+
1122
+ placed[neighbour] = claim_free_cell(placed.fetch(id), step, taken)
1123
+ end
1124
+ end
1125
+ placed
1126
+ end
1127
+
1128
+ def claim_free_cell(from, step, taken)
1129
+ cell = [from[0] + step[0], from[1] + step[1]]
1130
+ cell = [cell[0] + step[0], cell[1] + step[1]] while taken[cell]
1131
+ taken[cell] = true
1132
+ cell
1133
+ end
1134
+
1135
+ def position_services(diagram, hierarchy)
1136
+ positions = {}
1137
+ current_y = DEFAULT_SPACING
1138
+ row_groups(diagram).each do |group_id|
1139
+ services = hierarchy[:services_by_group][group_id] || []
1140
+ next if services.empty?
1141
+
1142
+ position_service_row(services, group_id, current_y, positions)
1143
+ current_y += DEFAULT_SERVICE_HEIGHT + (DEFAULT_SPACING * 2)
1144
+ end
1145
+ positions
1146
+ end
1147
+
1148
+ def row_groups(diagram)
1149
+ [:root] + diagram.groups.map(&:id)
1150
+ end
1151
+
1152
+ def position_service_row(services, group_id, origin_y, positions)
1153
+ current_x = DEFAULT_SPACING
1154
+ services.each do |service|
1155
+ dims = calculate_service_dimensions(service)
1156
+ positions[service.id] = positioned_service(
1157
+ service, dims, group_id, current_x, origin_y
1158
+ )
1159
+ current_x += dims[:width] + DEFAULT_SPACING
1160
+ end
1161
+ end
1162
+
1163
+ def positioned_service(service, dims, group_id, origin_x, origin_y)
1164
+ { service: service, x: origin_x, y: origin_y,
1165
+ width: dims[:width], height: dims[:height], group_id: group_id }
1166
+ end
1167
+
1168
+ # Junctions lay out on their own row-per-group grid, same shape as
1169
+ # position_services but with a fixed small size since a junction has
1170
+ # no label or icon to size against. A group that already has services
1171
+ # anchors its junction row past them (vertically centered on that
1172
+ # row) instead of at the group's default origin, so a junction never
1173
+ # lands on the same coordinates as a service in its own group. A
1174
+ # group with no services falls back to a shared cursor that starts
1175
+ # below every already-placed service in ANY group (not just its
1176
+ # own), so a junction-only group can never land on a service that
1177
+ # was laid out under a different group entirely.
1178
+ def position_junctions(diagram, hierarchy, service_positions)
1179
+ positions = {}
1180
+ current_y = junction_fallback_floor(service_positions)
1181
+ row_groups(diagram).each do |group_id|
1182
+ junctions = hierarchy[:junctions_by_group][group_id] || []
1183
+ next if junctions.empty?
1184
+
1185
+ current_y = position_junction_group(
1186
+ junctions, group_id, service_positions, current_y, positions
1187
+ )
1188
+ end
1189
+ positions
1190
+ end
1191
+
1192
+ def position_junction_group(
1193
+ junctions, group_id, services, current_y, positions
1194
+ )
1195
+ group_services = services_in_group(services, group_id)
1196
+ row_x, row_y = junction_row_origin(
1197
+ group_services, DEFAULT_SPACING, current_y
1198
+ )
1199
+ position_junction_row(junctions, group_id, [row_x, row_y], positions)
1200
+ if group_services.any?
1201
+ next_junction_y(current_y, row_y)
1202
+ else
1203
+ current_y + (DEFAULT_JUNCTION_SIZE + DEFAULT_SPACING)
1204
+ end
1205
+ end
1206
+
1207
+ def position_junction_row(junctions, group_id, origin, positions)
1208
+ row_x, row_y = origin
1209
+ junctions.each do |junction|
1210
+ positions[junction.id] = positioned_junction(
1211
+ junction, group_id, row_x, row_y
1212
+ )
1213
+ row_x += DEFAULT_JUNCTION_SIZE + DEFAULT_SPACING
1214
+ end
1215
+ end
1216
+
1217
+ def positioned_junction(junction, group_id, origin_x, origin_y)
1218
+ { junction: junction, x: origin_x, y: origin_y,
1219
+ width: DEFAULT_JUNCTION_SIZE, height: DEFAULT_JUNCTION_SIZE,
1220
+ group_id: group_id }
1221
+ end
1222
+
1223
+ def services_in_group(service_positions, group_id)
1224
+ service_positions.values.select { |pos| pos[:group_id] == group_id }
1225
+ end
1226
+
1227
+ def next_junction_y(current_y, row_y)
1228
+ row_bottom = row_y + DEFAULT_JUNCTION_SIZE + DEFAULT_SPACING
1229
+ [current_y, row_bottom].max
1230
+ end
1231
+
1232
+ def junction_row_origin(group_services, fallback_x, fallback_y)
1233
+ return [fallback_x, fallback_y] if group_services.empty?
1234
+
1235
+ row_x = group_services.map do |service|
1236
+ service[:x] + service[:width]
1237
+ end.max + DEFAULT_SPACING
1238
+ row_top = group_services.map { |s| s[:y] }.min
1239
+ row_y =
1240
+ row_top + ((DEFAULT_SERVICE_HEIGHT - DEFAULT_JUNCTION_SIZE) / 2.0)
1241
+
1242
+ [row_x, row_y]
1243
+ end
1244
+
1245
+ # A junction-only group (no services of its own) has no row to
1246
+ # anchor against, so it falls back to a shared cursor. That cursor
1247
+ # must start below every service in the WHOLE diagram, not just
1248
+ # DEFAULT_SPACING. Otherwise, it reuses the origin that position_services
1249
+ # gave to a service in an unrelated group.
1250
+ def junction_fallback_floor(service_positions)
1251
+ return DEFAULT_SPACING if service_positions.empty?
1252
+
1253
+ service_positions.values.map do |pos|
1254
+ pos[:y] + pos[:height]
1255
+ end.max + DEFAULT_SPACING
1256
+ end
1257
+
1258
+ def calculate_service_dimensions(_service)
1259
+ { width: DEFAULT_SERVICE_WIDTH, height: DEFAULT_SERVICE_HEIGHT }
1260
+ end
1261
+
1262
+ def service_icon?(service)
1263
+ !service.icon.to_s.empty?
1264
+ end
1265
+
1266
+ def calculate_group_bounds(diagram, service_positions, junction_positions)
1267
+ bounds = {}
1268
+ members = service_positions.values + junction_positions.values
1269
+
1270
+ deepest_groups_first(diagram.groups).each do |group|
1271
+ boxes = group_boxes(diagram, group, members, bounds)
1272
+ bounds[group.id] = padded_bounds(group, boxes) if boxes.any?
1273
+ end
1274
+ place_empty_groups(diagram, bounds, members)
1275
+ end
1276
+
1277
+ def group_boxes(diagram, group, members, bounds)
1278
+ members.select { |pos| pos[:group_id] == group.id } +
1279
+ child_group_bounds(diagram, group, bounds)
1280
+ end
1281
+
1282
+ def child_group_bounds(diagram, group, bounds)
1283
+ diagram.groups.select { |child| child.parent_id == group.id }
1284
+ .filter_map { |child| bounds[child.id] }
1285
+ end
1286
+
1287
+ def padded_bounds(group, boxes)
1288
+ min_x, width = span(boxes, :x, :width)
1289
+ min_y, height = span(boxes, :y, :height)
1290
+ pad = DEFAULT_GROUP_PADDING
1291
+ {
1292
+ group: group,
1293
+ x: min_x - pad,
1294
+ y: min_y - pad,
1295
+ width: width + (pad * 2),
1296
+ height: height + (pad * 2) + GROUP_LABEL_ALLOWANCE,
1297
+ }
1298
+ end
1299
+
1300
+ # [lowest start, length] covered by the boxes along one axis.
1301
+ def span(boxes, start, size)
1302
+ low = boxes.map { |box| box[start] }.min
1303
+ high = boxes.map { |box| box[start] + box[size] }.max
1304
+ [low, high - low]
1305
+ end
1306
+
1307
+ # mmdc draws a group with nothing in it as a small box beside the
1308
+ # diagram. A chain of empty nested groups stays undrawn: only an
1309
+ # isolated empty group (no parent, no child groups) is placed.
1310
+ def place_empty_groups(diagram, bounds, members)
1311
+ right = right_edge(members + bounds.values)
1312
+ centre_y = members_centre_y(members)
1313
+ diagram.groups.each do |group|
1314
+ next unless isolated_empty_group?(diagram, group, bounds)
1315
+
1316
+ x_coord = right ? right + EMPTY_GROUP_GAP : GRID_ORIGIN
1317
+ bounds[group.id] = empty_group_bounds(group, x_coord, centre_y)
1318
+ right = x_coord + EMPTY_GROUP_SIZE
1319
+ end
1320
+ bounds
1321
+ end
1322
+
1323
+ def right_edge(boxes)
1324
+ boxes.map { |box| box[:x] + box[:width] }.max
1325
+ end
1326
+
1327
+ def members_centre_y(members)
1328
+ return GRID_ORIGIN + (EMPTY_GROUP_SIZE / 2) if members.empty?
1329
+
1330
+ top = members.map { |box| box[:y] }.min
1331
+ bottom = members.map { |box| box[:y] + box[:height] }.max
1332
+ (top + bottom) / 2.0
1333
+ end
1334
+
1335
+ def isolated_empty_group?(diagram, group, bounds)
1336
+ return false if bounds.key?(group.id) || group.parent_id
1337
+
1338
+ diagram.groups.none? { |other| other.parent_id == group.id }
1339
+ end
1340
+
1341
+ def empty_group_bounds(group, x_coord, centre_y)
1342
+ { group: group, x: x_coord, y: centre_y - (EMPTY_GROUP_SIZE / 2.0),
1343
+ width: EMPTY_GROUP_SIZE, height: EMPTY_GROUP_SIZE }
1344
+ end
1345
+
1346
+ # A parent group with no direct service/junction of its own must read
1347
+ # its child group's bounds, so the child has to be processed first.
1348
+ # diagram.groups is in source-declaration order, which puts a parent
1349
+ # before a childless-of-its-own child when the mermaid source declares
1350
+ # the parent group first (the natural way to write nesting) - sort by
1351
+ # depth, deepest first, so every child is bounded before its parent
1352
+ # looks it up. A parent_id cycle (malformed diagram) can't loop
1353
+ # forever: `seen` stops recursion the second time an id reappears.
1354
+ def deepest_groups_first(groups)
1355
+ by_id = groups.to_h { |g| [g.id, g] }
1356
+ depths = groups.to_h { |g| [g.id, group_depth(g, by_id, [])] }
1357
+
1358
+ groups.each_with_index
1359
+ .sort_by { |group, index| [-depths[group.id], index] }
1360
+ .map(&:first)
1361
+ end
1362
+
1363
+ def group_depth(group, by_id, seen)
1364
+ return 0 if seen.include?(group.id)
1365
+
1366
+ parent = group.parent_id && by_id[group.parent_id]
1367
+ return 0 unless parent
1368
+
1369
+ 1 + group_depth(parent, by_id, seen + [group.id])
1370
+ end
1371
+
1372
+ # The declared (or defaulted) side is used literally, never mirrored -
1373
+ # a junction or service positioned on the "wrong" side of its hint is
1374
+ # no longer worked around here by lying about which face the line
1375
+ # attaches to. Architecture's ArchitectureEdgeRouter draws
1376
+ # around whatever is in the way instead, so the transform only needs
1377
+ # to hand it the real anchor point and the real declared side.
1378
+ def position_edges(diagram, service_positions)
1379
+ diagram.edges.filter_map do |edge|
1380
+ position_edge(edge, service_positions)
1381
+ end
1382
+ end
1383
+
1384
+ def position_edge(edge, service_positions)
1385
+ from = service_positions[edge.from_id]
1386
+ to = service_positions[edge.to_id]
1387
+ return unless from && to
1388
+
1389
+ positioned_edge(edge, from, to)
1390
+ end
1391
+
1392
+ def positioned_edge(edge, from, to)
1393
+ from_side = valid_side(edge.from_position, "R")
1394
+ to_side = valid_side(edge.to_position, "L")
1395
+ from_point = calculate_connection_point(from, from_side)
1396
+ to_point = calculate_connection_point(to, to_side)
1397
+ { edge: edge, from_x: from_point[:x], from_y: from_point[:y],
1398
+ to_x: to_point[:x], to_y: to_point[:y],
1399
+ from_side: from_side, to_side: to_side }
1400
+ end
1401
+
1402
+ # The grammar's position token is match("[LRTB]").repeat(1) - one OR
1403
+ # MORE, no upper bound - so a grammar-valid edge like `a:RT -- L:b`
1404
+ # parses to from_position == "RT". ArchitectureEdgeRouter::
1405
+ # FACE_NORMAL only has single-character keys, so an un-clamped value
1406
+ # reaches the router and raises the moment a detour is actually
1407
+ # needed. Falling back to +default+ here keeps that contract at the
1408
+ # one place every declared side passes through.
1409
+ def valid_side(position, default)
1410
+ VALID_SIDES.include?(position) ? position : default
1411
+ end
1412
+
1413
+ def calculate_connection_point(service_pos, position)
1414
+ case position
1415
+ when "L" then left_connection_point(service_pos)
1416
+ when "T" then top_connection_point(service_pos)
1417
+ when "B" then bottom_connection_point(service_pos)
1418
+ else right_connection_point(service_pos)
1419
+ end
1420
+ end
1421
+
1422
+ def left_connection_point(service_pos)
1423
+ { x: service_pos[:x], y: service_pos[:y] + (service_pos[:height] / 2) }
1424
+ end
1425
+
1426
+ def top_connection_point(service_pos)
1427
+ { x: service_pos[:x] + (service_pos[:width] / 2), y: service_pos[:y] }
1428
+ end
1429
+
1430
+ def bottom_connection_point(service_pos)
1431
+ { x: service_pos[:x] + (service_pos[:width] / 2),
1432
+ y: service_pos[:y] + service_pos[:height] }
1433
+ end
1434
+
1435
+ def right_connection_point(service_pos)
1436
+ { x: service_pos[:x] + service_pos[:width],
1437
+ y: service_pos[:y] + (service_pos[:height] / 2) }
1438
+ end
1439
+
1440
+ def calculate_total_width(
1441
+ service_positions, junction_positions, group_bounds
1442
+ )
1443
+ total_extent(
1444
+ [service_positions, junction_positions, group_bounds], :x, :width
1445
+ )
1446
+ end
1447
+
1448
+ def calculate_total_height(
1449
+ service_positions, junction_positions, group_bounds
1450
+ )
1451
+ total_extent(
1452
+ [service_positions, junction_positions, group_bounds], :y, :height
1453
+ )
1454
+ end
1455
+
1456
+ def total_extent(collections, origin, size)
1457
+ collections.map do |positions|
1458
+ position_extent(positions, origin, size)
1459
+ end.max + DEFAULT_SPACING
1460
+ end
1461
+
1462
+ def position_extent(positions, origin, size)
1463
+ positions.values.map { |node| node[origin] + node[size] }.max || 0
1464
+ end
1465
+
1466
+ def normal_font_size
1467
+ theme.typography&.font_size_normal ||
1468
+ Theme::Registry.get(:default).typography.font_size_normal
1469
+ end
1470
+ end
1471
+ end
1472
+ end