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,1277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "elk_placement"
5
+ require_relative "grid"
6
+ require_relative "../diagram/state_diagram"
7
+ require_relative "../notation/mermaid/ir_adapters/state_diagram"
8
+
9
+ module Etassia
10
+ module Layout
11
+ # Computes final canvas geometry for state diagrams.
12
+ class StateDiagram < Base
13
+ DEFAULT_FONT_SIZE = 14.0
14
+ DEFAULT_SMALL_FONT_SIZE = 12.0
15
+ TERMINAL_RADIUS = 7.0
16
+ TERMINAL_TYPES = %w[start end].freeze
17
+ CHAIN_STATE_TYPES = %w[start end normal].freeze
18
+ MMDC_STATE_FONT_SIZE = 16.0
19
+ MMDC_STATE_HEIGHT = 40.0
20
+ MMDC_STATE_PADDING = 16.0
21
+ MMDC_COMPOSITE_SIDE_PADDING = 48.3515625
22
+ MMDC_COMPOSITE_START_Y = 29.5
23
+ MMDC_COMPOSITE_FIRST_STATE_Y = 126.5
24
+ MMDC_COMPOSITE_SECOND_STATE_Y = 265.5
25
+ MMDC_COMPOSITE_BOTTOM_PADDING = 37.5
26
+ MMDC_NOTE_WIDTH = 230.0
27
+ MMDC_NOTE_HEIGHT = 78.0
28
+ MMDC_MULTILINE_NOTE_FONT_SIZE = 16.0
29
+ MMDC_MULTILINE_NOTE_PADDING = 30.0
30
+ MMDC_MULTILINE_NOTE_LINE_HEIGHT = 24.0
31
+ MMDC_DISCONNECTED_PAIR_INSET = 8.0
32
+ MMDC_DISCONNECTED_PAIR_GAP = 50.0
33
+ MMDC_CONCURRENT_REGION_LAYOUT = {
34
+ outer_padding: 35.0,
35
+ top: 37.5,
36
+ width_padding: 70.0,
37
+ height: 140.0,
38
+ child_top: 50.0,
39
+ gap: 50.0,
40
+ }.freeze
41
+
42
+ State = Data.define(
43
+ :id, :label, :state_type, :description, :descriptions, :children,
44
+ :target_id, :position
45
+ )
46
+ private_constant :State
47
+
48
+ STATE_TYPES = {
49
+ "state" => "normal",
50
+ "initial_state" => "start",
51
+ "final_state" => "end",
52
+ "choice_state" => "choice",
53
+ "fork_state" => "fork",
54
+ "join_state" => "join",
55
+ }.freeze
56
+ DIRECTIONS = {
57
+ "TD" => DIRECTION_DOWN, "TB" => DIRECTION_DOWN,
58
+ "LR" => DIRECTION_RIGHT, "RL" => DIRECTION_LEFT,
59
+ "BT" => DIRECTION_UP
60
+ }.freeze
61
+ LAYOUT_SETTINGS = {
62
+ ElkOptions::NODE_NODE_SPACING => 60,
63
+ ElkOptions::LAYER_SPACING => 60,
64
+ ElkOptions::EDGE_NODE_SPACING => 40,
65
+ ElkOptions::EDGE_EDGE_SPACING => 30,
66
+ ElkOptions::NODE_PLACEMENT => "SIMPLE",
67
+ }.freeze
68
+ private_constant :STATE_TYPES, :DIRECTIONS, :LAYOUT_SETTINGS,
69
+ :TERMINAL_RADIUS, :TERMINAL_TYPES,
70
+ :CHAIN_STATE_TYPES,
71
+ :MMDC_STATE_FONT_SIZE, :MMDC_STATE_HEIGHT,
72
+ :MMDC_STATE_PADDING, :MMDC_COMPOSITE_SIDE_PADDING,
73
+ :MMDC_COMPOSITE_START_Y,
74
+ :MMDC_COMPOSITE_FIRST_STATE_Y,
75
+ :MMDC_COMPOSITE_SECOND_STATE_Y,
76
+ :MMDC_COMPOSITE_BOTTOM_PADDING, :MMDC_NOTE_WIDTH,
77
+ :MMDC_NOTE_HEIGHT, :MMDC_MULTILINE_NOTE_FONT_SIZE,
78
+ :MMDC_MULTILINE_NOTE_PADDING,
79
+ :MMDC_MULTILINE_NOTE_LINE_HEIGHT,
80
+ :MMDC_DISCONNECTED_PAIR_INSET,
81
+ :MMDC_DISCONNECTED_PAIR_GAP,
82
+ :MMDC_CONCURRENT_REGION_LAYOUT
83
+
84
+ class Point < Lutaml::Model::Serializable
85
+ attribute :x, :float
86
+ attribute :y, :float
87
+ end
88
+
89
+ class Label < Lutaml::Model::Serializable
90
+ attribute :text, :string
91
+ attribute :width, :float
92
+ attribute :height, :float
93
+ attribute :x, :float
94
+ attribute :y, :float
95
+ attribute :font_size, :float
96
+ end
97
+
98
+ class Section < Lutaml::Model::Serializable
99
+ attribute :start_point, Point
100
+ attribute :end_point, Point
101
+ attribute :bend_points, Point, collection: true, default: -> { [] }
102
+ end
103
+
104
+ class Edge < Lutaml::Model::Serializable
105
+ attribute :id, :string
106
+ attribute :source, :string
107
+ attribute :target, :string
108
+ attribute :sections, Section, collection: true, default: -> { [] }
109
+ attribute :labels, Label, collection: true, default: -> { [] }
110
+ attribute :path, :string
111
+ attribute :trigger, :string
112
+ attribute :guard_condition, :string
113
+ end
114
+
115
+ class Node < Lutaml::Model::Serializable
116
+ attribute :id, :string
117
+ attribute :x, :float
118
+ attribute :y, :float
119
+ attribute :width, :float
120
+ attribute :height, :float
121
+ attribute :labels, Label, collection: true, default: -> { [] }
122
+ attribute :state_type, :string
123
+ attribute :shape_type, :string
124
+ attribute :center_x, :float
125
+ attribute :center_y, :float
126
+ attribute :radius, :float
127
+ attribute :inner_radius, :float
128
+ attribute :shape_points, :string
129
+ attribute :shape_y, :float
130
+ attribute :shape_height, :float
131
+ attribute :children, Node, collection: true, default: -> { [] }
132
+ end
133
+
134
+ class Scene < Layout::Scene
135
+ attribute :id, :string
136
+ attribute :view_box, :string
137
+ attribute :children, Node, collection: true, default: -> { [] }
138
+ attribute :edges, Edge, collection: true, default: -> { [] }
139
+ end
140
+
141
+ # :grid (the default) or :elk. Grid remains the default until the
142
+ # parity ratchet permits changing it.
143
+ attr_accessor :placement
144
+
145
+ class << self
146
+ def from_graph(graph, theme: nil)
147
+ layout = new
148
+ layout.theme = theme if theme
149
+ layout.send(:scene_from_graph, graph)
150
+ end
151
+
152
+ def center(box)
153
+ {
154
+ x: (box[:x] || 0) + ((box[:width] || 100) / 2),
155
+ y: (box[:y] || 0) + ((box[:height] || 50) / 2),
156
+ }
157
+ end
158
+
159
+ def shape_geometry(state, state_type)
160
+ dimensions = box_geometry(state)
161
+ midpoint = center(dimensions)
162
+ radius = state.dig(:metadata, :shape_radius)
163
+ dimensions.merge(
164
+ shape_details(dimensions, midpoint, state_type, radius),
165
+ )
166
+ end
167
+
168
+ def label_geometry(state, label, index, font_sizes)
169
+ x_position, y_position = label_position(state, index)
170
+ {
171
+ text: label[:text], width: label[:width], height: label[:height],
172
+ x: x_position, y: y_position,
173
+ font_size: index.zero? ? font_sizes[:normal] : font_sizes[:small]
174
+ }
175
+ end
176
+
177
+ def transition_label_geometry(source, target, label, font_size)
178
+ source_center = center(source)
179
+ target_center = center(target)
180
+ {
181
+ text: label[:text], width: label[:width], height: label[:height],
182
+ x: (source_center[:x] + target_center[:x]) / 2,
183
+ y: ((source_center[:y] + target_center[:y]) / 2) - 8,
184
+ font_size: font_size
185
+ }
186
+ end
187
+
188
+ def path_data(start_point, end_point, bend_points)
189
+ points = [start_point, *bend_points, end_point]
190
+ ["M #{coordinate(points.first.x)} #{coordinate(points.first.y)}",
191
+ *points.drop(1).map do |point|
192
+ "L #{coordinate(point.x)} #{coordinate(point.y)}"
193
+ end].join(" ")
194
+ end
195
+
196
+ def renderer_extent(graph, axis)
197
+ return coordinate(graph.public_send(axis) - 40) if graph.is_a?(Scene)
198
+
199
+ boxes = graph[:children]
200
+ return default_extent(axis) unless boxes
201
+
202
+ maximum_extent(boxes, axis) + 60
203
+ end
204
+
205
+ private
206
+
207
+ def box_geometry(state)
208
+ {
209
+ x: state[:x] || 0, y: state[:y] || 0,
210
+ width: state[:width] || 100, height: state[:height] || 50
211
+ }
212
+ end
213
+
214
+ def shape_details(dimensions, midpoint, state_type, radius)
215
+ radius ||= [dimensions[:width], dimensions[:height]].min / 2
216
+ {
217
+ center_x: midpoint[:x], center_y: midpoint[:y], radius: radius,
218
+ inner_radius: radius - 5,
219
+ shape_points: choice_points(dimensions, midpoint, state_type),
220
+ shape_y: dimensions[:y] + (dimensions[:height] / 2) - 5,
221
+ shape_height: 10
222
+ }
223
+ end
224
+
225
+ def choice_points(dimensions, midpoint, state_type)
226
+ return unless state_type == "choice"
227
+
228
+ x_position = dimensions[:x]
229
+ y_position = dimensions[:y]
230
+ width = dimensions[:width]
231
+ height = dimensions[:height]
232
+ [
233
+ "#{midpoint[:x]},#{y_position}",
234
+ "#{x_position + width},#{midpoint[:y]}",
235
+ "#{midpoint[:x]},#{y_position + height}",
236
+ "#{x_position},#{midpoint[:y]}",
237
+ ].join(" ")
238
+ end
239
+
240
+ def label_position(state, index)
241
+ midpoint = center(state)
242
+ stack_offset = ((state[:labels] || []).length - 1) * 10
243
+ [midpoint[:x], midpoint[:y] - stack_offset + (index * 20)]
244
+ end
245
+
246
+ def default_extent(axis)
247
+ axis == :width ? 800 : 600
248
+ end
249
+
250
+ def maximum_extent(boxes, axis)
251
+ position, fallback = axis == :width ? [:x, 100] : [:y, 50]
252
+ boxes.map do |state|
253
+ (state[position] || 0) + (state[axis] || fallback)
254
+ end.max || default_extent(axis)
255
+ end
256
+
257
+ def coordinate(number)
258
+ number.to_i == number ? number.to_i : number
259
+ end
260
+ end
261
+
262
+ def scene(diagram)
263
+ graph = build_graph(diagram)
264
+ placer.apply(graph)
265
+ place_disconnected_normal_pair(graph)
266
+ orient_grid_terminals(graph)
267
+ place_connected_state_chain(graph)
268
+ place_concurrent_regions(graph)
269
+ place_nested_plain_composite(graph)
270
+ place_nested_note_composite(graph)
271
+ place_attached_notes(graph)
272
+ scene_from_graph(graph)
273
+ end
274
+
275
+ private
276
+
277
+ def placer
278
+ placement == :elk ? ElkPlacement : Grid
279
+ end
280
+
281
+ def place_disconnected_normal_pair(graph)
282
+ states = Array(graph[:children])
283
+ return unless disconnected_normal_pair?(graph, states)
284
+
285
+ pack_disconnected_normal_pair(states)
286
+ end
287
+
288
+ def disconnected_normal_pair?(graph, states)
289
+ placement != :elk && states.length == 2 &&
290
+ Array(graph[:edges]).empty? &&
291
+ states.all? { |state| disconnected_normal?(state) }
292
+ end
293
+
294
+ def pack_disconnected_normal_pair(states)
295
+ x_position = MMDC_DISCONNECTED_PAIR_INSET
296
+ states.each do |state|
297
+ state[:x] = x_position
298
+ state[:y] = MMDC_DISCONNECTED_PAIR_INSET
299
+ x_position += state[:width] + MMDC_DISCONNECTED_PAIR_GAP
300
+ end
301
+ end
302
+
303
+ def disconnected_normal?(state)
304
+ state.dig(:metadata, :shape_type) == "normal" &&
305
+ Array(state[:children]).empty?
306
+ end
307
+
308
+ def orient_grid_terminals(graph)
309
+ return if placement == :elk
310
+ return unless graph.dig(:layoutOptions, ElkOptions::DIRECTION) ==
311
+ DIRECTION_DOWN
312
+
313
+ nested_states(graph[:children]).each do |state|
314
+ shape_type = state.dig(:metadata, :shape_type)
315
+ next unless TERMINAL_TYPES.include?(shape_type)
316
+
317
+ state[:x], state[:y] = state[:y], state[:x]
318
+ end
319
+ end
320
+
321
+ def place_connected_state_chain(graph)
322
+ topology = connected_state_chain(graph)
323
+ return unless topology
324
+
325
+ start, states, finish = topology
326
+ if graph.dig(:layoutOptions, ElkOptions::DIRECTION) == DIRECTION_RIGHT
327
+ place_horizontal_state_chain(start, states, finish)
328
+ else
329
+ place_vertical_state_chain(start, states, finish)
330
+ end
331
+ end
332
+
333
+ def connected_state_chain(graph)
334
+ nodes = connected_chain_nodes(graph)
335
+ return if nodes.empty?
336
+
337
+ start, finish, states = chain_nodes_by_type(nodes)
338
+ return unless start.one? && finish.length <= 1 && states.any?
339
+
340
+ ordered = ordered_chain_states(graph, start.first, states)
341
+ return unless ordered.length == states.length
342
+
343
+ [start.first, ordered, finish.first]
344
+ end
345
+
346
+ def connected_chain_nodes(graph)
347
+ direction = graph.dig(:layoutOptions, ElkOptions::DIRECTION)
348
+ return [] if placement == :elk
349
+ return [] unless [DIRECTION_DOWN, DIRECTION_RIGHT].include?(direction)
350
+
351
+ nodes = Array(graph[:children])
352
+ nodes.any? && nodes.all? { |node| chain_state?(node) } ? nodes : []
353
+ end
354
+
355
+ def chain_state?(node)
356
+ Array(node[:children]).empty? &&
357
+ CHAIN_STATE_TYPES.include?(node.dig(:metadata, :shape_type))
358
+ end
359
+
360
+ def chain_nodes_by_type(nodes)
361
+ grouped = nodes.group_by { |node| node.dig(:metadata, :shape_type) }
362
+ %w[start end normal].map { |type| grouped.fetch(type, []) }
363
+ end
364
+
365
+ def ordered_chain_states(graph, start, states)
366
+ ordered = []
367
+ current = start
368
+ loop do
369
+ candidates = outgoing_nodes(graph, current, states - ordered)
370
+ return [] if candidates.length > 1
371
+ break if candidates.empty?
372
+
373
+ current = candidates.first
374
+ ordered << current
375
+ end
376
+ ordered
377
+ end
378
+
379
+ def outgoing_nodes(graph, source, candidates)
380
+ target_ids = Array(graph[:edges]).filter_map do |edge|
381
+ edge[:targets]&.first if edge[:sources] == [source[:id]]
382
+ end
383
+ candidates.select { |candidate| target_ids.include?(candidate[:id]) }
384
+ end
385
+
386
+ def place_vertical_state_chain(start, states, finish)
387
+ center = states.map { |state| state[:width] }.max / 2.0
388
+ position_chain_start(start, center - (start[:width] / 2.0), 0.0)
389
+ position = position_vertical_states(states, center)
390
+ return unless finish
391
+
392
+ finish.merge!(x: center - (finish[:width] / 2.0),
393
+ y: position - 8.0)
394
+ end
395
+
396
+ def position_vertical_states(states, center)
397
+ position = 72.0
398
+ states.each do |state|
399
+ state[:x] = center - (state[:width] / 2.0)
400
+ state[:y] = position
401
+ position += state[:height] + 50.0
402
+ end
403
+ position
404
+ end
405
+
406
+ def place_horizontal_state_chain(start, states, finish)
407
+ center = states.map { |state| state[:height] }.max / 2.0
408
+ position_chain_start(start, 0.0, center - (start[:height] / 2.0))
409
+ position = position_horizontal_states(states, center)
410
+ return unless finish
411
+
412
+ finish.merge!(x: position - 8.0,
413
+ y: center - (finish[:height] / 2.0))
414
+ end
415
+
416
+ def position_horizontal_states(states, center)
417
+ position = 72.0
418
+ states.each do |state|
419
+ state[:x] = position
420
+ state[:y] = center - (state[:height] / 2.0)
421
+ position += state[:width] + 50.0
422
+ end
423
+ position
424
+ end
425
+
426
+ def position_chain_start(start, x_position, y_position)
427
+ start[:x] = x_position
428
+ start[:y] = y_position
429
+ end
430
+
431
+ # Converts a state diagram to a graph structure.
432
+ #
433
+ # @param diagram [Diagram::StateDiagram] the state diagram to transform
434
+ # @return [Hash] elkrb-compatible graph hash
435
+ def build_graph(diagram)
436
+ graph = ir_graph(diagram)
437
+ semantics = graph.nodes.group_by(&:parent_id)
438
+ states = layout_states(graph, semantics)
439
+ {
440
+ id: diagram_identifier(graph, semantics),
441
+ children: transform_states(states + layout_notes(graph, semantics)),
442
+ edges: transform_transitions(graph, semantics),
443
+ layoutOptions: layout_options(layout_direction(graph, semantics)),
444
+ }
445
+ end
446
+
447
+ def ir_graph(diagram)
448
+ return diagram if diagram.is_a?(IR::Graph)
449
+
450
+ Notation::Mermaid::IRAdapters::StateDiagram.call(diagram)
451
+ end
452
+
453
+ def layout_states(graph, semantics)
454
+ graph.nodes.filter_map do |node|
455
+ next unless STATE_TYPES.key?(node.role) && node.parent_id.nil?
456
+
457
+ state_from(node, semantics)
458
+ end
459
+ end
460
+
461
+ def layout_notes(graph, semantics)
462
+ graph.nodes.filter_map do |node|
463
+ note_from(node, semantics) if node.role == "annotation"
464
+ end
465
+ end
466
+
467
+ def state_from(node, semantics)
468
+ State.new(
469
+ id: state_identifier(node, semantics),
470
+ label: node.label,
471
+ state_type: declared_state_type(node, semantics),
472
+ description: semantic_value(semantics, node.id,
473
+ "latest_description"),
474
+ descriptions: semantic_values(semantics, node.id, "display_text"),
475
+ children: state_children_from(node, semantics),
476
+ target_id: nil, position: nil
477
+ )
478
+ end
479
+
480
+ def state_children_from(node, semantics)
481
+ nested_state_nodes(node, semantics).map do |child|
482
+ state_from(child, semantics)
483
+ end
484
+ end
485
+
486
+ def note_from(node, semantics)
487
+ State.new(
488
+ id: state_identifier(node, semantics), label: nil,
489
+ state_type: "note", description: nil,
490
+ descriptions: note_lines(node.label), children: [],
491
+ target_id: state_identifier_from_id(node.parent_id, semantics),
492
+ position: semantic_value(semantics, node.id, "placement_side")
493
+ )
494
+ end
495
+
496
+ def state_identifier_from_id(node_id, semantics)
497
+ semantic_value(semantics, node_id, "original_identifier") || node_id
498
+ end
499
+
500
+ def note_lines(text)
501
+ text.to_s.split(/\s*<br\s*\/?\s*>\s*/i).map(&:strip).reject(&:empty?)
502
+ end
503
+
504
+ def nested_state_nodes(node, semantics)
505
+ Array(semantics[node.id]).select do |child|
506
+ STATE_TYPES.key?(child.role)
507
+ end
508
+ end
509
+
510
+ def nested_states(states)
511
+ Array(states).flat_map do |state|
512
+ [state, *nested_states(state[:children])]
513
+ end
514
+ end
515
+
516
+ def declared_state_type(node, semantics)
517
+ semantic_value(semantics, node.id, "declared_state_type") ||
518
+ STATE_TYPES.fetch(node.role)
519
+ end
520
+
521
+ def settings_node(graph)
522
+ graph.nodes.find { |node| node.role == "diagram_settings" }
523
+ end
524
+
525
+ def diagram_identifier(graph, semantics)
526
+ settings = settings_node(graph)
527
+ return "state_diagram" unless settings
528
+
529
+ semantic_value(semantics, settings.id, "diagram_identifier") ||
530
+ "state_diagram"
531
+ end
532
+
533
+ def layout_direction(graph, semantics)
534
+ settings = settings_node(graph)
535
+ semantic_value(semantics, settings&.id, "layout_direction")
536
+ end
537
+
538
+ def semantic_value(semantics, parent_id, role)
539
+ semantic_values(semantics, parent_id, role).first
540
+ end
541
+
542
+ def semantic_values(semantics, parent_id, role)
543
+ Array(semantics[parent_id]).filter_map do |node|
544
+ node.label if node.role == role
545
+ end
546
+ end
547
+
548
+ def scene_from_graph(graph)
549
+ children = typed_children(graph[:children] || [])
550
+ raw_children = positioned_children(graph[:children] || [])
551
+ Scene.new(
552
+ id: graph[:id] || "state_diagram",
553
+ **scene_geometry(graph),
554
+ children: children,
555
+ edges: typed_edges(
556
+ graph[:edges] || [], children, raw_children
557
+ ),
558
+ )
559
+ end
560
+
561
+ def scene_geometry(graph)
562
+ padding = graph.key?(:children) ? 100 : 40
563
+ width = canvas_width(graph) + padding
564
+ height = canvas_height(graph) + padding
565
+ { width: width, height: height, view_box: "0 0 #{width} #{height}" }
566
+ end
567
+
568
+ def typed_children(children, parent_x = 0, parent_y = 0)
569
+ children.map { |state| typed_node(state, parent_x, parent_y) }
570
+ end
571
+
572
+ def typed_node(state, parent_x, parent_y)
573
+ positioned = position_state(state, parent_x, parent_y)
574
+ state_type = positioned.dig(:metadata, :state_type) || "normal"
575
+ shape_type = positioned.dig(:metadata, :shape_type) || state_type
576
+ Node.new(**typed_node_values(positioned, state_type, shape_type))
577
+ end
578
+
579
+ def position_state(state, parent_x, parent_y)
580
+ state.merge(x: state[:x].to_f + parent_x,
581
+ y: state[:y].to_f + parent_y)
582
+ end
583
+
584
+ def typed_node_values(positioned, state_type, shape_type)
585
+ {
586
+ id: positioned[:id], labels: typed_labels(positioned),
587
+ state_type: state_type, shape_type: shape_type,
588
+ children: typed_child_nodes(positioned),
589
+ **self.class.shape_geometry(positioned, shape_type)
590
+ }
591
+ end
592
+
593
+ def typed_child_nodes(state)
594
+ typed_children(state[:children] || [], state[:x], state[:y])
595
+ end
596
+
597
+ def typed_labels(state)
598
+ (state[:labels] || []).each_with_index.map do |label, index|
599
+ geometry = self.class.label_geometry(
600
+ state, label, index, label_font_sizes
601
+ )
602
+ Label.new(**geometry)
603
+ end
604
+ end
605
+
606
+ def typed_edges(edges, children, raw_children)
607
+ nodes = flatten_typed_nodes(children).to_h { |node| [node.id, node] }
608
+ raw_nodes = raw_children.to_h { |node| [node[:id], node] }
609
+ edges.filter_map { |edge| typed_edge_for(edge, nodes, raw_nodes) }
610
+ end
611
+
612
+ def flatten_typed_nodes(children)
613
+ children.flat_map do |node|
614
+ [node, *flatten_typed_nodes(node.children)]
615
+ end
616
+ end
617
+
618
+ def positioned_children(children, parent_x = 0, parent_y = 0)
619
+ children.flat_map do |state|
620
+ positioned = state.merge(
621
+ x: state[:x].to_f + parent_x,
622
+ y: state[:y].to_f + parent_y,
623
+ )
624
+ [positioned, *positioned_children(
625
+ positioned[:children] || [], positioned[:x], positioned[:y]
626
+ )]
627
+ end
628
+ end
629
+
630
+ def typed_edge_for(edge, nodes, raw_nodes)
631
+ source = nodes[edge[:sources]&.first]
632
+ target = nodes[edge[:targets]&.first]
633
+ return unless source && target
634
+
635
+ typed_edge(
636
+ edge, source, target, raw_nodes[source.id], raw_nodes[target.id]
637
+ )
638
+ end
639
+
640
+ def typed_edge(edge, source, target, raw_source, raw_target)
641
+ sections = typed_sections(edge, raw_source, raw_target)
642
+ Edge.new(
643
+ id: edge[:id], source: source.id, target: target.id,
644
+ sections: sections,
645
+ labels: typed_edge_labels(edge, raw_source, raw_target),
646
+ path: sections_path(sections),
647
+ trigger: edge.dig(:metadata, :trigger),
648
+ guard_condition: edge.dig(:metadata, :guard_condition)
649
+ )
650
+ end
651
+
652
+ def typed_edge_labels(edge, source, target)
653
+ label = (edge[:labels] || []).first
654
+ return [] unless label
655
+
656
+ geometry = self.class.transition_label_geometry(
657
+ source, target, label, small_font_size
658
+ )
659
+ [Label.new(**geometry)]
660
+ end
661
+
662
+ def typed_sections(edge, source, target)
663
+ source_point = point(self.class.center(source))
664
+ target_point = point(self.class.center(target))
665
+ sections = edge[:sections]
666
+ unless sections&.any?
667
+ return [straight_section(source_point, target_point)]
668
+ end
669
+
670
+ sections.map do |section|
671
+ typed_section(section, source_point, target_point)
672
+ end
673
+ end
674
+
675
+ def straight_section(source_point, target_point)
676
+ Section.new(start_point: source_point, end_point: target_point)
677
+ end
678
+
679
+ def typed_section(section, source_point, target_point)
680
+ bends = (section[:bendPoints] || []).map { |item| point(item) }
681
+ Section.new(
682
+ start_point: point(section[:startPoint]) || source_point,
683
+ end_point: point(section[:endPoint]) || target_point,
684
+ bend_points: bends,
685
+ )
686
+ end
687
+
688
+ def section_path(section)
689
+ self.class.path_data(
690
+ section.start_point, section.end_point, section.bend_points
691
+ )
692
+ end
693
+
694
+ def sections_path(sections)
695
+ sections.map { |section| section_path(section) }.join(" ")
696
+ end
697
+
698
+ def point(value)
699
+ return unless value
700
+
701
+ Point.new(x: value[:x], y: value[:y])
702
+ end
703
+
704
+ def canvas_width(graph)
705
+ return 800 unless graph[:children]
706
+
707
+ graph[:children].map do |state|
708
+ (state[:x] || 0) + (state[:width] || 100)
709
+ end.max || 800
710
+ end
711
+
712
+ def canvas_height(graph)
713
+ return 600 unless graph[:children]
714
+
715
+ graph[:children].map do |state|
716
+ (state[:y] || 0) + (state[:height] || 50)
717
+ end.max || 600
718
+ end
719
+
720
+ def transform_states(states)
721
+ states.map { |state| transformed_state(state) }
722
+ end
723
+
724
+ def transformed_state(state)
725
+ dimensions = calculate_state_dimensions(state)
726
+ {
727
+ id: state.id,
728
+ width: dimensions[:width],
729
+ height: dimensions[:height],
730
+ labels: state_labels(state),
731
+ metadata: state_metadata(state),
732
+ children: transform_states(state.children),
733
+ }
734
+ end
735
+
736
+ def state_metadata(state)
737
+ metadata = base_state_metadata(state)
738
+ add_terminal_radius(metadata)
739
+ end
740
+
741
+ def base_state_metadata(state)
742
+ {
743
+ state_type: state.state_type,
744
+ shape_type: state_shape_type(state),
745
+ description: state.description,
746
+ cluster: state.children.any?,
747
+ note_target: state.target_id,
748
+ note_position: state.position,
749
+ }
750
+ end
751
+
752
+ def add_terminal_radius(metadata)
753
+ if TERMINAL_TYPES.include?(metadata[:shape_type])
754
+ metadata[:shape_radius] = TERMINAL_RADIUS
755
+ end
756
+ metadata
757
+ end
758
+
759
+ def transform_transitions(graph, semantics)
760
+ states = graph.nodes.to_h { |node| [node.id, node] }
761
+ graph.edges.filter_map do |transition|
762
+ next unless transition.role == "state_transition"
763
+
764
+ transition_hash(transition, states, semantics)
765
+ end
766
+ end
767
+
768
+ def transition_hash(transition, states, semantics)
769
+ {
770
+ id: transition_identifier(transition, semantics),
771
+ sources: [state_identifier(states[transition.source_id], semantics)],
772
+ targets: [state_identifier(states[transition.target_id], semantics)],
773
+ labels: transition_labels(transition.label),
774
+ metadata: transition_metadata(transition, semantics),
775
+ }
776
+ end
777
+
778
+ def transition_identifier(transition, semantics)
779
+ semantic_value(
780
+ semantics, transition.parent_id, "transition_identifier"
781
+ ) || transition.id
782
+ end
783
+
784
+ def transition_metadata(transition, semantics)
785
+ parent_id = transition.parent_id
786
+ {
787
+ trigger: semantic_value(semantics, parent_id, "trigger"),
788
+ guard_condition: semantic_value(
789
+ semantics, parent_id, "guard_condition"
790
+ ),
791
+ }
792
+ end
793
+
794
+ def state_identifier(state, semantics)
795
+ return unless state
796
+
797
+ semantic_value(semantics, state.id, "original_identifier") || state.id
798
+ end
799
+
800
+ def state_labels(state)
801
+ state_texts(state).each_with_index.map do |text, index|
802
+ state_label(text, index)
803
+ end
804
+ end
805
+
806
+ def state_label(text, index)
807
+ font_size = index.zero? ? normal_font_size : small_font_size
808
+ dimensions = measure_text(text, font_size: font_size)
809
+ { text: text, width: dimensions[:width], height: dimensions[:height] }
810
+ end
811
+
812
+ def transition_labels(label)
813
+ return [] if label.nil? || label.empty?
814
+
815
+ label_dims = measure_text(label, font_size: small_font_size)
816
+
817
+ [
818
+ {
819
+ text: label,
820
+ width: label_dims[:width],
821
+ height: label_dims[:height],
822
+ },
823
+ ]
824
+ end
825
+
826
+ def calculate_state_dimensions(state)
827
+ texts = state_texts(state)
828
+ label_dims = measure_text(
829
+ texts.first || state.id, font_size: normal_font_size
830
+ )
831
+ state_dims = dimensions_for_shape(state, label_dims, texts.drop(1))
832
+ dimensions_with_label(state_dims, label_dims)
833
+ end
834
+
835
+ def dimensions_for_shape(state, label_dims, descriptions)
836
+ case state_shape_type(state)
837
+ when "start", "end" then calculate_terminal_dimensions
838
+ when "choice" then calculate_choice_dimensions(label_dims)
839
+ when "fork", "join" then calculate_fork_join_dimensions
840
+ when "note"
841
+ calculate_note_dimensions(state, label_dims, descriptions)
842
+ else calculate_normal_state_dimensions(state, label_dims, descriptions)
843
+ end
844
+ end
845
+
846
+ def dimensions_with_label(state_dims, label_dims)
847
+ {
848
+ width: state_dims[:width],
849
+ height: state_dims[:height],
850
+ label_width: label_dims[:width],
851
+ label_height: label_dims[:height],
852
+ }
853
+ end
854
+
855
+ # Parsed aliases and descriptions are Mermaid's ordered display text.
856
+ # StateNode's scalar fields remain the fallback for callers that build
857
+ # the model directly.
858
+ def state_texts(state)
859
+ descriptions = Array(state.descriptions).reject(&:empty?)
860
+ return descriptions unless descriptions.empty?
861
+
862
+ [state.label, state.description].compact.reject(&:empty?)
863
+ end
864
+
865
+ # Mermaid keeps the declared marker type, but displays a marker carrying
866
+ # ANY display text as an ordinary rectangular state. An alias lands in
867
+ # `descriptions`, never in the scalar `description`, so asking the scalar
868
+ # alone left `state C <<choice>>` + `state "Label" as C` drawing a
869
+ # polygon where mmdc 11.12.0 draws two rects, in both declaration orders.
870
+ #
871
+ # "Display text" here means `descriptions` or the scalar `description`,
872
+ # and deliberately NOT the label. A label cannot be used: terminals carry
873
+ # `label: "[*]"` with no descriptions, so keying the shape on the label
874
+ # turns `[*]` into an ordinary state and its 30px circle into a 100px box.
875
+ # Measured — adding a label check reddened "handles start state
876
+ # dimensions", which is the guard that caught it.
877
+ #
878
+ # Nothing is lost by that. No parsed source produces a marker carrying a
879
+ # label but no descriptions: `add_special_state` clears the label, and the
880
+ # one input that sets one — `state C <<choice>>` with `state "L" as C` —
881
+ # fills `descriptions` too, which the first check already catches. Only a
882
+ # directly built model can hold that combination, and it keeps its
883
+ # declared marker type.
884
+ def state_shape_type(state)
885
+ return "note" if state.state_type == "note"
886
+ return "normal" unless Array(state.descriptions).reject(&:empty?).empty?
887
+ return "normal" if state.description && !state.description.empty?
888
+
889
+ state.state_type
890
+ end
891
+
892
+ def calculate_terminal_dimensions
893
+ # Start and end states are small circles
894
+ {
895
+ width: 30,
896
+ height: 30,
897
+ }
898
+ end
899
+
900
+ def calculate_choice_dimensions(label_dims)
901
+ # Choice states are diamonds
902
+ size = [label_dims[:width], label_dims[:height]].max + 40
903
+ {
904
+ width: size,
905
+ height: size,
906
+ }
907
+ end
908
+
909
+ def calculate_fork_join_dimensions
910
+ # Fork and join are represented as thick bars
911
+ {
912
+ width: 100,
913
+ height: 10,
914
+ }
915
+ end
916
+
917
+ def calculate_normal_state_dimensions(state, label_dims, descriptions)
918
+ if descriptions.empty? && state.children.empty?
919
+ return simple_state_dimensions(state)
920
+ end
921
+
922
+ dimensions = descriptions.map do |description|
923
+ measure_text(description, font_size: small_font_size)
924
+ end
925
+ width = normal_state_width(label_dims, dimensions)
926
+ height = normal_state_height(label_dims, dimensions)
927
+ { width: [width, 100].max, height: [height, 50].max }
928
+ end
929
+
930
+ def simple_state_dimensions(state)
931
+ {
932
+ width: mmdc_state_text_width(state_texts(state).first || state.id),
933
+ height: MMDC_STATE_HEIGHT,
934
+ }
935
+ end
936
+
937
+ def calculate_note_dimensions(state, label_dims, descriptions)
938
+ if mmdc_multiline_note?(state, descriptions)
939
+ return mmdc_multiline_note_dimensions(state_texts(state))
940
+ end
941
+
942
+ dimensions = descriptions.map do |description|
943
+ measure_text(description, font_size: small_font_size)
944
+ end
945
+ {
946
+ width: normal_state_width(label_dims, dimensions),
947
+ height: normal_state_height(label_dims, dimensions),
948
+ }
949
+ end
950
+
951
+ def mmdc_multiline_note?(state, descriptions)
952
+ state.target_id && descriptions.any? &&
953
+ normal_font_size == DEFAULT_FONT_SIZE &&
954
+ small_font_size == DEFAULT_SMALL_FONT_SIZE
955
+ end
956
+
957
+ def mmdc_multiline_note_dimensions(lines)
958
+ width = lines.map { |line| mmdc_multiline_note_text_width(line) }.max
959
+ {
960
+ width: width + MMDC_MULTILINE_NOTE_PADDING,
961
+ height: (lines.length * MMDC_MULTILINE_NOTE_LINE_HEIGHT) +
962
+ MMDC_MULTILINE_NOTE_PADDING,
963
+ }
964
+ end
965
+
966
+ def mmdc_multiline_note_text_width(text)
967
+ measured = TextMeasurement.measure_in(
968
+ :trebuchet, text, font_size: MMDC_MULTILINE_NOTE_FONT_SIZE
969
+ )[:width]
970
+ ((measured * 32).round + 1) / 32.0
971
+ end
972
+
973
+ def normal_state_width(label_dims, descriptions)
974
+ descriptions.map { |item| item[:width] }
975
+ .push(label_dims[:width]).max + 40
976
+ end
977
+
978
+ def normal_state_height(label_dims, descriptions)
979
+ label_dims[:height] + 30 +
980
+ descriptions.sum { |item| item[:height] + 10 }
981
+ end
982
+
983
+ def label_font_sizes
984
+ { normal: normal_font_size, small: small_font_size }
985
+ end
986
+
987
+ def normal_font_size
988
+ valid_font_size(theme.typography&.font_size_normal) || DEFAULT_FONT_SIZE
989
+ end
990
+
991
+ def small_font_size
992
+ valid_font_size(theme.typography&.font_size_small) ||
993
+ DEFAULT_SMALL_FONT_SIZE
994
+ end
995
+
996
+ def valid_font_size(value)
997
+ value if value.is_a?(Numeric) && value.positive?
998
+ end
999
+
1000
+ def layout_options(direction)
1001
+ build_elk_options(
1002
+ algorithm: ALGORITHM_LAYERED,
1003
+ direction: direction_to_layout(direction),
1004
+ **LAYOUT_SETTINGS,
1005
+ )
1006
+ end
1007
+
1008
+ def direction_to_layout(direction)
1009
+ DIRECTIONS.fetch(direction, DIRECTION_DOWN)
1010
+ end
1011
+
1012
+ def place_attached_notes(graph)
1013
+ nodes = nested_states(graph[:children]).to_h do |node|
1014
+ [node[:id], node]
1015
+ end
1016
+ nodes.each_value { |note| place_attached_note(note, nodes) }
1017
+ end
1018
+
1019
+ def place_nested_note_composite(graph)
1020
+ topology = nested_note_topology(graph)
1021
+ return unless topology
1022
+
1023
+ root, composite, note, start, first, second = topology
1024
+ size_nested_note_nodes(note, first, second)
1025
+ place_nested_note_nodes(root, composite, start, first, second)
1026
+ end
1027
+
1028
+ def place_nested_plain_composite(graph)
1029
+ composite = plain_nested_composite(graph)
1030
+ return unless composite
1031
+
1032
+ start, first, second = nested_note_nodes(graph, composite)
1033
+ size_nested_states(first, second)
1034
+ width = nested_composite_width(first, second)
1035
+ height = nested_composite_height(second)
1036
+ composite.merge!(x: Grid::MARGIN, y: Grid::MARGIN,
1037
+ width: width, height: height)
1038
+ place_nested_children(width, start, first, second)
1039
+ end
1040
+
1041
+ def place_concurrent_regions(graph)
1042
+ composite, regions = concurrent_region_topology(graph)
1043
+ return unless composite
1044
+
1045
+ position = MMDC_CONCURRENT_REGION_LAYOUT.fetch(:outer_padding)
1046
+ regions.each do |region|
1047
+ position = place_concurrent_region(region, position)
1048
+ end
1049
+ composite.merge!(concurrent_composite_box(position))
1050
+ end
1051
+
1052
+ def concurrent_composite_box(position)
1053
+ layout = MMDC_CONCURRENT_REGION_LAYOUT
1054
+ padding = layout.fetch(:outer_padding)
1055
+ bottom = layout.fetch(:top) + layout.fetch(:height) +
1056
+ layout.fetch(:top)
1057
+ { x: Grid::MARGIN, y: Grid::MARGIN,
1058
+ width: position - layout.fetch(:gap) + padding, height: bottom }
1059
+ end
1060
+
1061
+ def concurrent_region_topology(graph)
1062
+ composite = concurrent_composite(graph)
1063
+ return [] unless composite
1064
+
1065
+ regions = Array(composite[:children])
1066
+ return [] unless regions.length > 1 &&
1067
+ regions.all? { |region| concurrent_region?(region) }
1068
+
1069
+ [composite, regions]
1070
+ end
1071
+
1072
+ def concurrent_composite(graph)
1073
+ return unless plain_nested_graph?(graph)
1074
+ return unless Array(graph[:edges]).empty?
1075
+
1076
+ children = Array(graph[:children])
1077
+ children.first if children.one? &&
1078
+ children.first.dig(:metadata, :cluster)
1079
+ end
1080
+
1081
+ def concurrent_region?(region)
1082
+ child = Array(region[:children])
1083
+ region.dig(:metadata, :cluster) &&
1084
+ region[:id].match?(/\Aconcurrent_region_\d+\z/) &&
1085
+ child.one? && !child.first.dig(:metadata, :cluster)
1086
+ end
1087
+
1088
+ def place_concurrent_region(region, position)
1089
+ layout = MMDC_CONCURRENT_REGION_LAYOUT
1090
+ child = region[:children].first
1091
+ width = child[:width] + layout.fetch(:width_padding)
1092
+ region.merge!(x: position, y: layout.fetch(:top), width: width,
1093
+ height: layout.fetch(:height))
1094
+ place_concurrent_child(child, layout)
1095
+ position + width + layout.fetch(:gap)
1096
+ end
1097
+
1098
+ def place_concurrent_child(child, layout)
1099
+ child[:x] = layout.fetch(:outer_padding)
1100
+ child[:y] = layout.fetch(:child_top)
1101
+ end
1102
+
1103
+ def plain_nested_composite(graph)
1104
+ return unless plain_nested_graph?(graph)
1105
+
1106
+ children = Array(graph[:children])
1107
+ return unless children.one?
1108
+
1109
+ composite = children.first
1110
+ composite if composite.dig(:metadata, :cluster) &&
1111
+ nested_note_nodes(graph, composite).any?
1112
+ end
1113
+
1114
+ def plain_nested_graph?(graph)
1115
+ placement != :elk && normal_font_size == DEFAULT_FONT_SIZE &&
1116
+ small_font_size == DEFAULT_SMALL_FONT_SIZE &&
1117
+ graph.dig(:layoutOptions, ElkOptions::DIRECTION) == DIRECTION_DOWN
1118
+ end
1119
+
1120
+ def nested_note_topology(graph)
1121
+ return unless plain_nested_graph?(graph)
1122
+
1123
+ root, composite, note = connected_top_level_note_nodes(graph)
1124
+ return unless root
1125
+
1126
+ start, first, second = nested_note_nodes(graph, composite)
1127
+ return unless start && first && second
1128
+
1129
+ [root, composite, note, start, first, second]
1130
+ end
1131
+
1132
+ def connected_top_level_note_nodes(graph)
1133
+ root, composite, note = top_level_note_nodes(graph[:children])
1134
+ return [] unless root && composite && note &&
1135
+ connected?(graph, root, composite)
1136
+
1137
+ [root, composite, note]
1138
+ end
1139
+
1140
+ def top_level_note_nodes(children)
1141
+ return [] unless Array(children).length == 3
1142
+
1143
+ composite = children.find { |node| node.dig(:metadata, :cluster) }
1144
+ root = children.find do |node|
1145
+ node.dig(:metadata, :shape_type) == "start"
1146
+ end
1147
+ note = children.find do |node|
1148
+ right_composite_note?(node, composite)
1149
+ end
1150
+ [root, composite, note]
1151
+ end
1152
+
1153
+ def right_composite_note?(node, composite)
1154
+ node.dig(:metadata, :note_target) == composite&.fetch(:id, nil) &&
1155
+ node.dig(:metadata, :note_position) == "right" &&
1156
+ two_line_note?(node)
1157
+ end
1158
+
1159
+ def two_line_note?(note)
1160
+ text = note.dig(:labels, 0, :text).to_s
1161
+ width = TextMeasurement.measure_in(
1162
+ :trebuchet, text, font_size: MMDC_STATE_FONT_SIZE
1163
+ )[:width]
1164
+ width > 200 && width <= 400
1165
+ end
1166
+
1167
+ def nested_note_nodes(graph, composite)
1168
+ children = composite[:children]
1169
+ return [] unless Array(children).length == 3
1170
+
1171
+ start = children.find do |node|
1172
+ node.dig(:metadata, :shape_type) == "start"
1173
+ end
1174
+ states = children.reject { |node| node.equal?(start) }
1175
+ return [] unless start && two_normal_states?(states)
1176
+
1177
+ nested_note_chain(graph, start, states)
1178
+ end
1179
+
1180
+ def two_normal_states?(states)
1181
+ states.length == 2 && states.all? do |state|
1182
+ state.dig(:metadata, :shape_type) == "normal"
1183
+ end
1184
+ end
1185
+
1186
+ def nested_note_chain(graph, start, states)
1187
+ first = states.find { |state| connected?(graph, start, state) }
1188
+ second = (states - [first]).first
1189
+ return [] unless first && connected?(graph, first, second) &&
1190
+ connected?(graph, second, first)
1191
+
1192
+ [start, first, second]
1193
+ end
1194
+
1195
+ def connected?(graph, source, target)
1196
+ graph[:edges].any? do |edge|
1197
+ edge[:sources] == [source[:id]] && edge[:targets] == [target[:id]]
1198
+ end
1199
+ end
1200
+
1201
+ def size_nested_note_nodes(note, first, second)
1202
+ size_nested_states(first, second)
1203
+ note[:width] = MMDC_NOTE_WIDTH
1204
+ note[:height] = MMDC_NOTE_HEIGHT
1205
+ end
1206
+
1207
+ def size_nested_states(first, second)
1208
+ [first, second].each do |state|
1209
+ state[:width] = mmdc_state_width(state)
1210
+ state[:height] = MMDC_STATE_HEIGHT
1211
+ end
1212
+ end
1213
+
1214
+ def mmdc_state_width(state)
1215
+ text = state.dig(:labels, 0, :text).to_s
1216
+ mmdc_state_text_width(text)
1217
+ end
1218
+
1219
+ def mmdc_state_text_width(text)
1220
+ measured = TextMeasurement.measure_in(
1221
+ :trebuchet, text, font_size: MMDC_STATE_FONT_SIZE
1222
+ )[:width]
1223
+ browser_width = ((measured * 64).round + 1) / 64.0
1224
+ browser_width + MMDC_STATE_PADDING
1225
+ end
1226
+
1227
+ def place_nested_note_nodes(root, composite, start, first, second)
1228
+ width = nested_composite_width(first, second)
1229
+ height = nested_composite_height(second)
1230
+ composite.merge!(x: Grid::MARGIN, y: Grid::MARGIN + 72,
1231
+ width: width, height: height)
1232
+ root[:x] = composite[:x] + ((width - root[:width]) / 2)
1233
+ root[:y] = Grid::MARGIN
1234
+ place_nested_children(width, start, first, second)
1235
+ end
1236
+
1237
+ def nested_composite_width(first, second)
1238
+ [first[:width], second[:width]].max +
1239
+ (MMDC_COMPOSITE_SIDE_PADDING * 2)
1240
+ end
1241
+
1242
+ def nested_composite_height(second)
1243
+ MMDC_COMPOSITE_SECOND_STATE_Y + second[:height] +
1244
+ MMDC_COMPOSITE_BOTTOM_PADDING
1245
+ end
1246
+
1247
+ def place_nested_children(width, start, first, second)
1248
+ start[:x] = (width - start[:width]) / 2
1249
+ start[:y] = MMDC_COMPOSITE_START_Y
1250
+ first[:x] = (width - first[:width]) / 2
1251
+ first[:y] = MMDC_COMPOSITE_FIRST_STATE_Y
1252
+ second.merge!(x: (width - second[:width]) / 2,
1253
+ y: MMDC_COMPOSITE_SECOND_STATE_Y)
1254
+ end
1255
+
1256
+ def place_attached_note(note, nodes)
1257
+ target = nodes[note.dig(:metadata, :note_target)]
1258
+ return unless target
1259
+
1260
+ note[:x], note[:y] = attached_note_coordinates(note, target)
1261
+ end
1262
+
1263
+ def attached_note_coordinates(note, target)
1264
+ x_position = target[:x] + ((target[:width] - note[:width]) / 2.0)
1265
+ y_position = target[:y] + subtree_height(target) + 75
1266
+ [x_position, y_position]
1267
+ end
1268
+
1269
+ def subtree_height(state)
1270
+ child_extent = Array(state[:children]).map do |child|
1271
+ child[:y].to_f + subtree_height(child)
1272
+ end.max || 0
1273
+ [state[:height].to_f, child_extent].max
1274
+ end
1275
+ end
1276
+ end
1277
+ end