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,1075 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../notation/mermaid/ir_adapters/git_graph"
5
+
6
+ module Etassia
7
+ module Layout
8
+ # Transforms a GitGraph diagram into a positioned layout structure.
9
+ #
10
+ # Unlike other transformers that use ELK layout, GitGraph uses a custom
11
+ # layout algorithm that assigns commits to lanes and sequential
12
+ # positions along the time axis based on commit order. The time axis
13
+ # runs left to right for `LR`, top to bottom for `TB` and bottom to top
14
+ # for `BT`; lanes are rows for `LR` and columns otherwise.
15
+ #
16
+ # The layout algorithm handles:
17
+ # - Branch lane assignment (main branch in the first lane)
18
+ # - Commit positioning (chronological, along the time axis)
19
+ # - Parent-child relationships for drawing connections
20
+ # - Merge point tracking for drawing merge arrows
21
+ # - Cherry-pick visualization
22
+ #
23
+ # @example Transform a git graph
24
+ # transform = Layout::GitGraph.new
25
+ # layout = transform.to_graph(diagram)
26
+ class GitGraph < Base
27
+ # Spacing between commits along the time axis
28
+ COMMIT_SPACING = 50
29
+
30
+ # Spacing between branch lanes
31
+ LANE_SPACING = 90
32
+
33
+ # Radius of commit circles
34
+ COMMIT_RADIUS = 10
35
+
36
+ PADDING = 40
37
+ LABEL_HEADROOM = 1.2
38
+ BRANCH_LABEL_INSET = 8.5
39
+ EMPTY_BRANCH_LABEL_CLEARANCE = 50
40
+ BRANCH_LABEL_BASELINE_OFFSET = 10.5
41
+ VERTICAL_BRANCH_LABEL_CLEARANCE = 40
42
+ EXPLICIT_ID_BRANCH_LABEL_CLEARANCE = 47
43
+ CHERRY_PICK_BRANCH_LABEL_CLEARANCE = 28
44
+ MERMAID_LABEL_FONT_SIZE = 10
45
+ MERMAID_LABEL_WIDTH_SCALE = 0.86
46
+ VERTICAL_LANE_LABEL_RATIO = 0.85
47
+ VERTICAL_LANE_LABEL_HEADROOM = 12.5
48
+ VERTICAL_LANE_GAP = 85
49
+
50
+ class Label < Lutaml::Model::Serializable
51
+ attribute :text, :string
52
+ attribute :x, :float
53
+ attribute :y, :float
54
+ attribute :text_anchor, :string
55
+ attribute :kind, :string
56
+ attribute :branch, :string
57
+ attribute :font_size, :float
58
+ attribute :rotation, :float
59
+ attribute :rotation_center_x, :float
60
+ attribute :rotation_center_y, :float
61
+ attribute :translation_x, :float
62
+ end
63
+
64
+ class Commit < Lutaml::Model::Serializable
65
+ attribute :id, :string
66
+ attribute :message, :string
67
+ attribute :x, :float
68
+ attribute :y, :float
69
+ attribute :branch, :string
70
+ attribute :lane, :integer
71
+ attribute :type, :string
72
+ attribute :tag, :string
73
+ attribute :tags, :string, collection: true, default: -> { [] }
74
+ attribute :parent_ids, :string, collection: true, default: -> { [] }
75
+ attribute :is_merge, :boolean
76
+ attribute :merge_branch, :string
77
+ attribute :is_cherry_pick, :boolean
78
+ attribute :cherry_pick_parent, :string
79
+ attribute :labels, Label, collection: true, default: -> { [] }
80
+ end
81
+
82
+ class Branch < Lutaml::Model::Serializable
83
+ attribute :name, :string
84
+ attribute :lane, :integer
85
+ attribute :color, :string
86
+ attribute :order, :integer
87
+ attribute :parent_branch, :string
88
+ attribute :created_at_commit, :string
89
+ attribute :label, Label
90
+ end
91
+
92
+ class Connection < Lutaml::Model::Serializable
93
+ attribute :source, :string
94
+ attribute :target, :string
95
+ attribute :from_x, :float
96
+ attribute :from_y, :float
97
+ attribute :to_x, :float
98
+ attribute :to_y, :float
99
+ attribute :from_branch, :string
100
+ attribute :to_branch, :string
101
+ attribute :type, :symbol
102
+ attribute :path, :string
103
+ end
104
+
105
+ class Scene < Layout::Scene
106
+ attribute :view_box, :string
107
+ attribute :orientation, :string
108
+ attribute :commits, Commit, collection: true, default: -> { [] }
109
+ attribute :branches, Branch, collection: true, default: -> { [] }
110
+ attribute :connections, Connection, collection: true,
111
+ default: -> { [] }
112
+ end
113
+
114
+ # Transforms the diagram into a layout structure.
115
+ #
116
+ # @param diagram [Diagram::GitGraph] the git graph diagram
117
+ # @return [Hash] layout data with commits, branches, and connections
118
+ def build_graph(diagram)
119
+ graph = ir_graph(diagram)
120
+ orientation, main_branch, branches, lane_assignments, lane_positions =
121
+ positioning_context(graph)
122
+ commits = position_graph_commits(
123
+ graph, lane_assignments, orientation, main_branch, lane_positions
124
+ )
125
+ graph_result(
126
+ graph, commits, branches, [lane_assignments, lane_positions],
127
+ [orientation, main_branch]
128
+ )
129
+ end
130
+
131
+ def graph_context(graph)
132
+ orientation = graph_orientation(graph)
133
+ main_branch = graph_main_branch(graph)
134
+ branches = source_branches(graph)
135
+ lanes = assign_lanes(
136
+ branches, build_branch_info(branches, main_branch), main_branch
137
+ )
138
+ [orientation, main_branch, branches, lanes]
139
+ end
140
+
141
+ private
142
+
143
+ def positioning_context(graph)
144
+ orientation, main_branch, branches, lanes = graph_context(graph)
145
+ [orientation, main_branch, branches, lanes,
146
+ lane_positions(lanes, orientation)]
147
+ end
148
+
149
+ def position_graph_commits(graph, lanes, orientation, main_branch,
150
+ lane_positions)
151
+ position_commits(
152
+ visible_commits(source_commits(graph)), lanes, orientation,
153
+ main_branch, lane_positions
154
+ )
155
+ end
156
+
157
+ def graph_result(graph, commits, branches, lane_layout, context)
158
+ lanes, lane_positions = lane_layout
159
+ orientation, main_branch = context
160
+ dimensions = extents(commits, lanes, lane_positions, orientation)
161
+ {
162
+ commits: commits,
163
+ branches: build_branch_metadata(branches, lanes, main_branch),
164
+ connections: build_connections(commits, graph.edges),
165
+ lane_positions: lane_positions,
166
+ orientation: orientation,
167
+ }.merge(width: dimensions.first, height: dimensions.last)
168
+ end
169
+
170
+ def ir_graph(diagram)
171
+ return diagram if diagram.is_a?(IR::Graph)
172
+
173
+ Notation::Mermaid::IRAdapters::GitGraph.call(diagram)
174
+ end
175
+
176
+ def graph_orientation(graph)
177
+ graph.nodes.find { |node| node.role == "orientation" }&.label || "LR"
178
+ end
179
+
180
+ def graph_main_branch(graph)
181
+ graph.nodes.find { |node| node.role == "main_branch" }&.label || "main"
182
+ end
183
+
184
+ def source_commits(graph)
185
+ children = graph.nodes.group_by(&:parent_id)
186
+ graph.nodes.select { |node| node.role == "commit" }.map do |node|
187
+ commit_attributes(node, children.fetch(node.id, []))
188
+ end
189
+ end
190
+
191
+ def visible_commits(commits)
192
+ latest = commits.each_with_object({}) do |commit, identities|
193
+ identities[commit[:id]] = commit[:ir_id] unless
194
+ commit[:is_cherry_pick]
195
+ end
196
+ commits.select do |commit|
197
+ commit[:is_cherry_pick] || latest[commit[:id]] == commit[:ir_id]
198
+ end
199
+ end
200
+
201
+ def commit_attributes(node, semantics)
202
+ commit_identity(node, semantics).merge(commit_action(semantics))
203
+ end
204
+
205
+ def commit_identity(node, semantics)
206
+ {
207
+ ir_id: node.id, id: node.label,
208
+ message: semantic_value(semantics, "message"),
209
+ type: semantic_value(semantics, "type") || "NORMAL",
210
+ tag: semantic_value(semantics, "tag"),
211
+ tags: semantic_values(semantics, "tag"),
212
+ branch_name: semantic_value(semantics, "branch_name"),
213
+ parent_ids: semantic_values(semantics, "parent_reference")
214
+ }
215
+ end
216
+
217
+ def commit_action(semantics)
218
+ {
219
+ is_merge: semantic?(semantics, "merge_commit"),
220
+ merge_branch: semantic_value(semantics, "merge_branch"),
221
+ is_cherry_pick: semantic?(semantics, "cherry_pick_commit"),
222
+ cherry_pick_parent: semantic_value(
223
+ semantics, "cherry_pick_parent"
224
+ ),
225
+ }
226
+ end
227
+
228
+ def source_branches(graph)
229
+ children = graph.nodes.group_by(&:parent_id)
230
+ graph.nodes.select { |node| node.role == "branch" }.map do |node|
231
+ branch_attributes(node, children.fetch(node.id, []))
232
+ end
233
+ end
234
+
235
+ def branch_attributes(node, semantics)
236
+ order = semantic_value(semantics, "order")
237
+ {
238
+ name: node.label, order: order&.to_i,
239
+ parent_branch: semantic_value(semantics, "parent_branch"),
240
+ created_at_commit: semantic_value(semantics, "created_at_commit")
241
+ }
242
+ end
243
+
244
+ def semantic_values(semantics, role)
245
+ semantics.select { |node| node.role == role }.map(&:label)
246
+ end
247
+
248
+ def semantic_value(semantics, role)
249
+ semantics.find { |node| node.role == role }&.label
250
+ end
251
+
252
+ def semantic?(semantics, role)
253
+ semantics.any? { |node| node.role == role }
254
+ end
255
+
256
+ def scene(diagram)
257
+ graph = build_graph(diagram)
258
+ labels = label_hashes(graph)
259
+ shift, width, height = scene_dimensions(graph, labels)
260
+ typed_scene(graph, labels, shift, width, height)
261
+ end
262
+
263
+ def typed_scene(graph, labels, shift, width, height)
264
+ Scene.new(
265
+ width: width, height: height,
266
+ view_box: "0 0 #{width.to_f} #{height.to_f}",
267
+ orientation: graph[:orientation],
268
+ commits: typed_commits(graph, labels, shift),
269
+ branches: typed_branches(graph, labels, shift),
270
+ connections: typed_connections(graph[:connections], shift)
271
+ )
272
+ end
273
+
274
+ def scene_dimensions(graph, labels)
275
+ base_width = graph[:width] + (PADDING * 2)
276
+ spill_left, spill_right = label_spill(labels, base_width)
277
+ shift = PADDING + spill_left
278
+ width = base_width + spill_left + spill_right
279
+ height = graph[:height] + (PADDING * 2)
280
+ [shift, width, height]
281
+ end
282
+
283
+ def label_hashes(graph)
284
+ commit_labels(graph) + branch_labels(graph)
285
+ end
286
+
287
+ def commit_labels(graph)
288
+ style = commit_label_style(graph)
289
+ graph[:commits].flat_map do |commit|
290
+ labels_for_commit(commit, graph[:orientation], style)
291
+ end
292
+ end
293
+
294
+ def commit_label_style(graph)
295
+ return :lone if graph[:orientation] == "LR" && graph[:commits].one?
296
+ return :generated_diagonal if compact_generated_semantic_graph?(graph)
297
+ return :tagged_vertical if all_tagged_vertical_graph?(graph)
298
+ return :linear if linear_label_graph?(graph)
299
+
300
+ :legacy
301
+ end
302
+
303
+ def linear_label_graph?(graph)
304
+ vertical?(graph[:orientation]) || simple_three_commit_graph?(graph)
305
+ end
306
+
307
+ def labels_for_commit(commit, orientation, style)
308
+ labels = [commit_id_label(commit, orientation, style)].compact
309
+ labels.concat(positioned_tag_labels(commit, orientation, style))
310
+ end
311
+
312
+ def commit_id_label(commit, orientation, style)
313
+ return if commit[:is_cherry_pick] || commit[:id].nil?
314
+ return if commit[:id].start_with?("commit_")
315
+
316
+ positioned_label(commit[:id], commit, orientation, "id", style)
317
+ end
318
+
319
+ def positioned_tag_labels(commit, orientation, style)
320
+ tag_labels(commit).map do |tag|
321
+ positioned_label(tag, commit, orientation, "tag", style)
322
+ end
323
+ end
324
+
325
+ def simple_three_commit_graph?(graph)
326
+ graph_sizes(graph) == [3, 1, 2] && normal_connections?(graph) &&
327
+ graph[:commits].all? { |commit| plain_linear_commit?(commit) }
328
+ end
329
+
330
+ def graph_sizes(graph)
331
+ %i[commits branches connections].map { |key| graph.fetch(key).size }
332
+ end
333
+
334
+ def normal_connections?(graph)
335
+ graph[:connections].all? { |connection| connection[:type] == :normal }
336
+ end
337
+
338
+ def plain_linear_commit?(commit)
339
+ commit[:message].nil? && commit[:tags].empty? &&
340
+ commit[:type] == "NORMAL" && !commit[:is_merge] &&
341
+ !commit[:is_cherry_pick]
342
+ end
343
+
344
+ def all_tagged_vertical_graph?(graph)
345
+ vertical?(graph[:orientation]) && graph[:branches].one? &&
346
+ graph[:commits].size > 1 && graph[:commits].all? do |commit|
347
+ Array(commit[:tags]).any? { |tag| !tag.empty? }
348
+ end
349
+ end
350
+
351
+ def tag_labels(commit)
352
+ tags = Array(commit[:tags])
353
+ return tags.reject(&:empty?) unless tags.empty?
354
+ return [] unless commit[:is_cherry_pick]
355
+
356
+ [cherry_pick_label(commit)]
357
+ end
358
+
359
+ def cherry_pick_label(commit)
360
+ label = "cherry-pick:#{commit[:id]}"
361
+ parent = commit[:cherry_pick_parent]
362
+ parent ? "#{label}|parent:#{parent}" : label
363
+ end
364
+
365
+ def positioned_label(text, commit, orientation, kind, style)
366
+ position = label_position(
367
+ text, commit, orientation, kind, style
368
+ )
369
+ label_geometry(text, commit, kind, position)
370
+ end
371
+
372
+ def label_geometry(text, commit, kind, position)
373
+ x, y, anchor, rotation, center_x, center_y, translation_x = position
374
+ { text: text, x: x + PADDING, y: y + PADDING,
375
+ text_anchor: anchor, kind: kind, branch: commit[:branch],
376
+ owner_id: commit[:ir_id], font_size: small_font_size,
377
+ rotation: rotation,
378
+ rotation_center_x: padded_coordinate(center_x),
379
+ rotation_center_y: padded_coordinate(center_y),
380
+ translation_x: translation_x }.compact
381
+ end
382
+
383
+ def padded_coordinate(coordinate)
384
+ coordinate && (coordinate + PADDING)
385
+ end
386
+
387
+ def label_position(text, commit, orientation, kind, style)
388
+ x_coordinate, y_coordinate = commit.values_at(:x, :y)
389
+ return lone_label_position(text, x_coordinate, y_coordinate, kind) if
390
+ style == :lone
391
+
392
+ specialized_label_position(
393
+ text, [commit, orientation, kind, style],
394
+ [x_coordinate, y_coordinate]
395
+ ) || legacy_label_position(
396
+ text, x_coordinate, y_coordinate, orientation, kind
397
+ )
398
+ end
399
+
400
+ def specialized_label_position(text, context, coordinates)
401
+ commit, orientation, kind, style = context
402
+ if kind == "id"
403
+ return styled_id_label_position(
404
+ text, commit, orientation, style, coordinates
405
+ )
406
+ end
407
+ if style == :tagged_vertical && kind == "tag"
408
+ tagged_vertical_label_position(*coordinates)
409
+ end
410
+ end
411
+
412
+ def styled_id_label_position(text, commit, orientation, style,
413
+ coordinates)
414
+ x_coordinate, y_coordinate = coordinates
415
+ if %i[linear tagged_vertical].include?(style)
416
+ styled_linear_position(text, orientation, style, coordinates)
417
+ elsif style == :generated_diagonal
418
+ diagonal_id_label_position(
419
+ mermaid_label_width(text), x_coordinate, y_coordinate
420
+ )
421
+ elsif style == :legacy && generated_commit_id?(commit)
422
+ [x_coordinate, y_coordinate + 20, "middle"]
423
+ end
424
+ end
425
+
426
+ def styled_linear_position(text, orientation, style, coordinates)
427
+ x_coordinate, y_coordinate = coordinates
428
+ font_size = MERMAID_LABEL_FONT_SIZE if style == :tagged_vertical
429
+ linear_label_position(
430
+ text, x_coordinate, y_coordinate, orientation, font_size
431
+ )
432
+ end
433
+
434
+ def generated_commit_id?(commit)
435
+ commit[:id].match?(/\Acommit[-_]\d+\z/)
436
+ end
437
+
438
+ def compact_generated_semantic_graph?(graph)
439
+ compact_generated_graph?(graph) && tagged_merge?(graph[:commits])
440
+ end
441
+
442
+ def compact_generated_graph?(graph)
443
+ graph[:orientation] == "LR" && graph[:commits].size <= 3 &&
444
+ graph[:commits].all? { |commit| generated_commit_id?(commit) }
445
+ end
446
+
447
+ def tagged_merge?(commits)
448
+ commits.any? do |commit|
449
+ commit[:is_merge] && tag_labels(commit).any?
450
+ end
451
+ end
452
+
453
+ def lone_label_position(text, x_coordinate, y_coordinate, kind)
454
+ width = mermaid_label_width(text)
455
+ return [x_coordinate - (width / 2.0), y_coordinate - 16, "middle"] if
456
+ kind == "tag"
457
+
458
+ diagonal_id_label_position(width, x_coordinate, y_coordinate)
459
+ end
460
+
461
+ def mermaid_label_width(text)
462
+ TextMeasurement.measure_in(
463
+ :arial, text, font_size: MERMAID_LABEL_FONT_SIZE
464
+ )[:width] * MERMAID_LABEL_WIDTH_SCALE
465
+ end
466
+
467
+ def diagonal_id_label_position(width, x_coordinate, y_coordinate)
468
+ offset_x, offset_y = diagonal_id_label_offsets(width)
469
+ [x_coordinate - (width / 2.0) + offset_x,
470
+ y_coordinate + 25 + offset_y, "start", -45.0,
471
+ x_coordinate - COMMIT_RADIUS, y_coordinate]
472
+ end
473
+
474
+ def diagonal_id_label_offsets(width)
475
+ shift_x = -7.5 - (((width + 10) / 25.0) * 9.5)
476
+ shift_y = 10 + ((width / 25.0) * 8.5)
477
+ diagonal = Math.sqrt(2)
478
+ [(shift_x - shift_y) / diagonal, (shift_x + shift_y) / diagonal]
479
+ end
480
+
481
+ def linear_label_position(text, x_coordinate, y_coordinate, orientation,
482
+ font_size = nil)
483
+ if vertical?(orientation)
484
+ return vertical_label_position(
485
+ text, x_coordinate, y_coordinate, font_size
486
+ )
487
+ end
488
+
489
+ width = measure_text(text, font_size: small_font_size)[:width]
490
+ [x_coordinate - (width / 2.0), y_coordinate + 24, "start"]
491
+ end
492
+
493
+ def legacy_label_position(text, x_coordinate, y_coordinate, orientation,
494
+ kind)
495
+ if vertical?(orientation)
496
+ return legacy_vertical_label_position(
497
+ x_coordinate, y_coordinate, kind
498
+ )
499
+ end
500
+ return horizontal_tag_position(text, x_coordinate, y_coordinate) if
501
+ kind == "tag"
502
+
503
+ width = mermaid_label_width(text)
504
+ diagonal_id_label_position(width, x_coordinate, y_coordinate)
505
+ end
506
+
507
+ def horizontal_tag_position(text, x_coordinate, y_coordinate)
508
+ width = mermaid_label_width(text)
509
+ [x_coordinate - (width / 2.0), y_coordinate - 15, "start"]
510
+ end
511
+
512
+ def legacy_vertical_label_position(x_coordinate, y_coordinate, kind)
513
+ return [x_coordinate - 14, y_coordinate + 4, "end"] if kind == "tag"
514
+
515
+ [x_coordinate + 14, y_coordinate + 4, "start"]
516
+ end
517
+
518
+ def vertical_label_position(text, x_coordinate, y_coordinate,
519
+ font_size = nil)
520
+ width = measure_text(
521
+ text, font_size: font_size || small_font_size
522
+ )[:width]
523
+ logical_x = x_coordinate + 14
524
+ visual_x = x_coordinate - width - 12
525
+ [logical_x, y_coordinate - 1, "start", -45.0,
526
+ x_coordinate, y_coordinate, visual_x - logical_x]
527
+ end
528
+
529
+ def tagged_vertical_label_position(x_coordinate, y_coordinate)
530
+ [x_coordinate + 19, y_coordinate + 7, "start", 45.0,
531
+ x_coordinate + 14, y_coordinate + 4]
532
+ end
533
+
534
+ def branch_labels(graph)
535
+ commits = representative_branch_commits(graph)
536
+ commits.map do |branch, commit|
537
+ branch_label(branch, commit, graph)
538
+ end
539
+ end
540
+
541
+ def representative_branch_commits(graph)
542
+ representatives = positioned_branch_commits(graph)
543
+ graph[:branches].each_with_object(representatives) do |branch, result|
544
+ next if result.key?(branch[:name])
545
+
546
+ anchor = empty_branch_anchor(branch, graph)
547
+ result[branch[:name]] = anchor if anchor
548
+ end
549
+ end
550
+
551
+ def positioned_branch_commits(graph)
552
+ if vertical?(graph[:orientation])
553
+ return graph[:commits].to_h do |commit|
554
+ [commit[:branch], commit]
555
+ end
556
+ end
557
+
558
+ graph[:commits].each_with_object({}) do |commit, representatives|
559
+ representatives[commit[:branch]] ||= commit
560
+ end
561
+ end
562
+
563
+ def empty_branch_anchor(branch, graph)
564
+ creation = graph[:commits].find do |commit|
565
+ commit[:id] == branch[:created_at_commit]
566
+ end
567
+ return empty_root_anchor(branch, graph) unless creation
568
+
569
+ branch_anchor(branch, graph, creation)
570
+ end
571
+
572
+ def branch_anchor(branch, graph, creation)
573
+ orientation = graph[:orientation]
574
+ time = vertical?(orientation) ? creation[:y] : creation[:x]
575
+ lane = graph[:lane_positions].fetch(branch[:lane])
576
+ x_coordinate, y_coordinate = coordinates(time, lane, orientation)
577
+ { x: x_coordinate, y: y_coordinate }
578
+ end
579
+
580
+ def empty_root_anchor(branch, graph)
581
+ return if graph[:commits].empty? || !branch[:parent_branch].nil?
582
+
583
+ x_coordinate, y_coordinate = coordinates(
584
+ 0, branch[:lane] * LANE_SPACING, graph[:orientation]
585
+ )
586
+ { x: x_coordinate, y: y_coordinate }
587
+ end
588
+
589
+ def branch_label(branch, commit, graph)
590
+ position = branch_label_position(branch, commit, graph)
591
+ geometry = branch_label_geometry(branch, *position)
592
+ # Mermaid reserves extra visual clearance without changing the
593
+ # branch label's logical boundary anchor.
594
+ translation = branch_label_translation(graph)
595
+ geometry[:translation_x] = -translation if translation
596
+ geometry
597
+ end
598
+
599
+ def branch_label_translation(graph)
600
+ return EMPTY_BRANCH_LABEL_CLEARANCE if translated_branch_label?(graph)
601
+ return CHERRY_PICK_BRANCH_LABEL_CLEARANCE if cherry_pick_graph?(graph)
602
+
603
+ if explicit_id_graph?(graph) || compact_generated_semantic_graph?(graph)
604
+ EXPLICIT_ID_BRANCH_LABEL_CLEARANCE
605
+ end
606
+ end
607
+
608
+ def cherry_pick_graph?(graph)
609
+ !vertical?(graph[:orientation]) &&
610
+ graph[:commits].any? { |commit| commit[:is_cherry_pick] }
611
+ end
612
+
613
+ def translated_branch_label?(graph)
614
+ simple_three_commit_graph?(graph) &&
615
+ !vertical?(graph[:orientation]) && !generated_commits_only?(graph)
616
+ end
617
+
618
+ def explicit_id_graph?(graph)
619
+ return false unless horizontal_explicit_id_candidate?(graph)
620
+
621
+ graph[:commits].any? { |commit| !generated_commit_id?(commit) } &&
622
+ graph[:commits].none? { |commit| commit[:is_cherry_pick] }
623
+ end
624
+
625
+ def horizontal_explicit_id_candidate?(graph)
626
+ !vertical?(graph[:orientation]) && graph[:commits].size > 1 &&
627
+ !generated_commits_only?(graph)
628
+ end
629
+
630
+ def branch_label_geometry(branch, x_coordinate, y_coordinate, anchor)
631
+ { text: branch, x: x_coordinate + PADDING,
632
+ y: y_coordinate + PADDING, text_anchor: anchor,
633
+ kind: "branch", branch: branch, font_size: branch_font_size }
634
+ end
635
+
636
+ def branch_label_position(branch, commit, graph)
637
+ width = measure_text(branch, font_size: branch_font_size)[:width]
638
+ return horizontal_branch_label_position(commit[:y], width, graph) unless
639
+ vertical?(graph[:orientation])
640
+
641
+ vertical_branch_label_position(commit[:x], width, graph)
642
+ end
643
+
644
+ def horizontal_branch_label_position(lane, width, graph)
645
+ inset = BRANCH_LABEL_INSET
646
+ inset -= EMPTY_BRANCH_LABEL_CLEARANCE if graph[:commits].one? ||
647
+ generated_commits_only?(graph)
648
+ [inset - width,
649
+ lane - BRANCH_LABEL_BASELINE_OFFSET, "start"]
650
+ end
651
+
652
+ def generated_commits_only?(graph)
653
+ graph[:commits].all? do |commit|
654
+ commit[:id].match?(/\Acommit-\d+\z/) && tag_labels(commit).empty?
655
+ end
656
+ end
657
+
658
+ def vertical_branch_label_position(lane, width, graph)
659
+ boundary = if graph[:orientation] == "TB"
660
+ COMMIT_RADIUS - VERTICAL_BRANCH_LABEL_CLEARANCE
661
+ else
662
+ graph[:height]
663
+ end
664
+ [lane - (width / 2.0), boundary, "start"]
665
+ end
666
+
667
+ def label_spill(labels, width)
668
+ return [0, 0] if labels.empty?
669
+
670
+ spans = labels.map { |label| label_span(label) }
671
+ lefts, rights = spans.transpose
672
+ [[-lefts.min, 0].max, [rights.max - width, 0].max]
673
+ end
674
+
675
+ def label_span(label)
676
+ width = measure_text(
677
+ label[:text], font_size: label.fetch(:font_size)
678
+ )[:width] * LABEL_HEADROOM
679
+ share = { "start" => 0.0, "middle" => 0.5, "end" => 1.0 }
680
+ .fetch(label[:text_anchor])
681
+ left = label[:x] + label.fetch(:translation_x, 0) - (width * share)
682
+ [left, left + width]
683
+ end
684
+
685
+ def typed_commits(graph, labels, shift)
686
+ graph[:commits].map do |commit|
687
+ typed_commit(commit, labels, shift)
688
+ end
689
+ end
690
+
691
+ def typed_commit(commit, labels, shift)
692
+ Commit.new(
693
+ **typed_commit_attributes(commit, shift),
694
+ labels: typed_labels(
695
+ commit_labels_for(labels, commit), shift - PADDING
696
+ ),
697
+ )
698
+ end
699
+
700
+ def typed_commit_attributes(commit, shift)
701
+ typed_commit_identity(commit, shift).merge(
702
+ is_merge: commit[:is_merge], merge_branch: commit[:merge_branch],
703
+ is_cherry_pick: commit[:is_cherry_pick],
704
+ cherry_pick_parent: commit[:cherry_pick_parent]
705
+ )
706
+ end
707
+
708
+ def typed_commit_identity(commit, shift)
709
+ {
710
+ id: commit[:id], message: commit[:message],
711
+ x: commit[:x] + shift, y: commit[:y] + PADDING,
712
+ branch: commit[:branch], lane: commit[:lane], type: commit[:type],
713
+ tag: commit[:tag], tags: commit[:tags],
714
+ parent_ids: commit[:parent_ids]
715
+ }
716
+ end
717
+
718
+ def commit_labels_for(labels, commit)
719
+ labels.select do |label|
720
+ label[:kind] != "branch" && label[:owner_id] == commit[:ir_id]
721
+ end
722
+ end
723
+
724
+ def typed_branches(graph, labels, shift)
725
+ graph[:branches].map do |branch|
726
+ typed_branch(branch, labels, shift)
727
+ end
728
+ end
729
+
730
+ def typed_branch(branch, labels, shift)
731
+ geometry = labels.find do |label|
732
+ label[:kind] == "branch" && label[:branch] == branch[:name]
733
+ end
734
+ Branch.new(
735
+ name: branch[:name], lane: branch[:lane], color: branch[:color],
736
+ order: branch[:order], parent_branch: branch[:parent_branch],
737
+ created_at_commit: branch[:created_at_commit],
738
+ label: typed_label(geometry, shift - PADDING)
739
+ )
740
+ end
741
+
742
+ def typed_labels(labels, extra_shift)
743
+ labels.map { |label| typed_label(label, extra_shift) }
744
+ end
745
+
746
+ def typed_label(label, extra_shift)
747
+ return unless label
748
+
749
+ Label.new(text: label[:text], x: label[:x] + extra_shift,
750
+ y: label[:y], text_anchor: label[:text_anchor],
751
+ kind: label[:kind], branch: label[:branch],
752
+ font_size: label.fetch(:font_size),
753
+ rotation: label[:rotation],
754
+ rotation_center_x: shifted_rotation_x(label, extra_shift),
755
+ rotation_center_y: label[:rotation_center_y],
756
+ translation_x: label[:translation_x])
757
+ end
758
+
759
+ def shifted_rotation_x(label, extra_shift)
760
+ center = label[:rotation_center_x]
761
+ center && (center + extra_shift)
762
+ end
763
+
764
+ def typed_connections(connections, shift)
765
+ connections.map do |connection|
766
+ typed_connection(connection_geometry(connection, shift))
767
+ end
768
+ end
769
+
770
+ def connection_geometry(connection, shift)
771
+ connection.merge(
772
+ from_x: connection[:from_x] + shift,
773
+ from_y: connection[:from_y] + PADDING,
774
+ to_x: connection[:to_x] + shift,
775
+ to_y: connection[:to_y] + PADDING,
776
+ )
777
+ end
778
+
779
+ def typed_connection(geometry)
780
+ Connection.new(
781
+ source: geometry[:from], target: geometry[:to],
782
+ from_x: geometry[:from_x], from_y: geometry[:from_y],
783
+ to_x: geometry[:to_x], to_y: geometry[:to_y],
784
+ from_branch: geometry[:from_branch],
785
+ to_branch: geometry[:to_branch], type: geometry[:type],
786
+ path: connection_path(geometry)
787
+ )
788
+ end
789
+
790
+ def connection_path(connection)
791
+ return straight_path(connection) unless connection[:type] == :merge
792
+ return straight_path(connection) if same_row?(connection)
793
+
794
+ coordinates = connection.values_at(:from_x, :from_y, :to_x, :to_y)
795
+ curved_path(*coordinates)
796
+ end
797
+
798
+ def same_row?(connection)
799
+ connection[:from_y] == connection[:to_y]
800
+ end
801
+
802
+ def curved_path(from_x, from_y, to_x, to_y)
803
+ control_x = from_x + ((to_x - from_x) * 0.5)
804
+ "M #{from_x} #{from_y} C #{control_x} #{from_y}, " \
805
+ "#{control_x} #{to_y}, #{to_x} #{to_y}"
806
+ end
807
+
808
+ def straight_path(connection)
809
+ "M #{connection[:from_x]} #{connection[:from_y]} " \
810
+ "L #{connection[:to_x]} #{connection[:to_y]}"
811
+ end
812
+
813
+ def small_font_size
814
+ theme.typography&.font_size_small ||
815
+ Theme::Registry.get(:default).typography.font_size_small
816
+ end
817
+
818
+ def branch_font_size
819
+ theme.typography&.font_size_large ||
820
+ Theme::Registry.get(:default).typography.font_size_large
821
+ end
822
+
823
+ # Builds branch information from the shared graph's branch entries.
824
+ #
825
+ # @param branches [Array<Hash>] ordered branch semantics
826
+ # @return [Hash] branch info with parent relationships
827
+ def build_branch_info(branches, main_branch)
828
+ info = { main_branch => branch_info({}, 0, main_branch) }
829
+ branches.reject { |branch| branch[:name] == main_branch }
830
+ .each do |branch|
831
+ info[branch[:name]] = branch_info(branch, info.size, main_branch)
832
+ end
833
+ info
834
+ end
835
+
836
+ def branch_info(branch, default_order, main_branch)
837
+ {
838
+ parent: branch.empty? ? nil : branch[:parent_branch] || main_branch,
839
+ order: branch[:order] || default_order,
840
+ created_at: branch[:created_at_commit],
841
+ }
842
+ end
843
+
844
+ # Assigns lanes (rows for LR, columns for TB/BT) to branches.
845
+ #
846
+ # Main branch gets lane 0, child branches get the following lanes.
847
+ #
848
+ # @param branches [Array<Hash>] ordered branch semantics
849
+ # @param branch_info [Hash] branch information
850
+ # @return [Hash<String, Integer>] branch name to lane number
851
+ def assign_lanes(branches, branch_info, main_branch)
852
+ ordered_branches(branches, branch_info).each_with_index
853
+ .with_object(main_branch => 0) do |pair, lanes|
854
+ branch, index = pair
855
+ lanes[branch[:name]] = index + 1
856
+ end
857
+ end
858
+
859
+ def ordered_branches(branches, branch_info)
860
+ main_branch = branch_info.keys.first
861
+ branches.reject { |branch| branch[:name] == main_branch }
862
+ .each_with_index.sort_by do |(branch, index)|
863
+ [branch[:order] ? 1 : 0, branch[:order] || index, branch[:name]]
864
+ end.map(&:first)
865
+ end
866
+
867
+ def lane_positions(assignments, orientation)
868
+ return horizontal_lane_positions(assignments) unless
869
+ vertical?(orientation)
870
+
871
+ ordered = assignments.sort_by { |_branch, lane| lane }
872
+ ordered.each_with_index.with_object({}) do |pair, out|
873
+ (_branch, lane), index = pair
874
+ out[lane] = next_lane_position(ordered, index, out)
875
+ end
876
+ end
877
+
878
+ def horizontal_lane_positions(assignments)
879
+ assignments.values.to_h { |lane| [lane, lane * LANE_SPACING] }
880
+ end
881
+
882
+ def next_lane_position(ordered, index, positions)
883
+ return 0.0 if index.zero?
884
+
885
+ previous_branch, previous_lane = ordered.fetch(index - 1)
886
+ positions.fetch(previous_lane) + VERTICAL_LANE_GAP +
887
+ (vertical_lane_label_width(previous_branch) / 2.0)
888
+ end
889
+
890
+ def vertical_lane_label_width(branch)
891
+ measured = measure_text(branch, font_size: branch_font_size)[:width]
892
+ substitute = (measured * VERTICAL_LANE_LABEL_RATIO) +
893
+ VERTICAL_LANE_LABEL_HEADROOM
894
+ [measured, substitute].max
895
+ end
896
+
897
+ # Positions commits with X and Y coordinates.
898
+ #
899
+ # @param commits [Array<Hash>] ordered commit semantics
900
+ # @param lane_assignments [Hash] branch to lane mapping
901
+ # @param orientation [String] "LR", "TB" or "BT"
902
+ # @return [Array<Hash>] positioned commits
903
+ def position_commits(commits, lane_assignments, orientation, main_branch,
904
+ lane_positions)
905
+ commits.each_with_index.map do |commit, index|
906
+ positioned_commit(
907
+ commit, [index, commits.size], lane_assignments,
908
+ [orientation, main_branch], lane_positions
909
+ )
910
+ end
911
+ end
912
+
913
+ def positioned_commit(commit, sequence, lane_assignments, context,
914
+ lane_positions)
915
+ index, count = sequence
916
+ orientation, main_branch = context
917
+ branch = commit[:branch_name] || main_branch
918
+ lane = lane_assignments[branch] || 0
919
+ attributes = positioned_commit_attributes(
920
+ commit, [index, count], [branch, lane], orientation, lane_positions
921
+ )
922
+ visible_commit_attributes(commit.merge(attributes))
923
+ end
924
+
925
+ def visible_commit_attributes(commit)
926
+ commit.slice(
927
+ :ir_id, :id, :x, :y, :branch, :lane, :message, :type, :tag, :tags,
928
+ :parent_ids, :is_merge, :merge_branch, :is_cherry_pick,
929
+ :cherry_pick_parent
930
+ )
931
+ end
932
+
933
+ def positioned_commit_attributes(commit, sequence, branch_lane,
934
+ orientation, lane_positions)
935
+ index, count = sequence
936
+ branch, lane = branch_lane
937
+ x, y = commit_coordinates(
938
+ index, count, lane, orientation, lane_positions
939
+ )
940
+ {
941
+ id: commit[:id] || "commit_#{index}", x: x, y: y,
942
+ branch: branch, lane: lane,
943
+ type: commit[:type] || "NORMAL"
944
+ }.merge(commit_flags(commit))
945
+ end
946
+
947
+ def commit_flags(commit)
948
+ {
949
+ is_merge: commit[:is_merge] || false,
950
+ is_cherry_pick: commit[:is_cherry_pick] || false,
951
+ }
952
+ end
953
+
954
+ def commit_coordinates(index, count, lane, orientation, lane_positions)
955
+ time = time_position(index, count, orientation)
956
+ lane_position = lane_positions.fetch(lane)
957
+ coordinates(time, lane_position, orientation)
958
+ end
959
+
960
+ # Width and height: the time axis is horizontal for `LR` only.
961
+ def extents(positioned_commits, lane_assignments, lane_positions,
962
+ orientation)
963
+ time_span = calculate_time_span(positioned_commits.size)
964
+ lane_span = calculate_lane_span(lane_assignments, lane_positions)
965
+ vertical?(orientation) ? [lane_span, time_span] : [time_span, lane_span]
966
+ end
967
+
968
+ def vertical?(orientation)
969
+ %w[TB BT].include?(orientation)
970
+ end
971
+
972
+ # Distance along the time axis from the start edge of the diagram.
973
+ # `BT` counts down from the last commit, so the first commit is
974
+ # the lowest one.
975
+ def time_position(idx, count, orientation)
976
+ step = orientation == "BT" ? count - idx - 1 : idx
977
+ (step * COMMIT_SPACING) + COMMIT_RADIUS
978
+ end
979
+
980
+ def coordinates(time, lane, orientation)
981
+ vertical?(orientation) ? [lane, time] : [time, lane]
982
+ end
983
+
984
+ # Builds connections between commits.
985
+ #
986
+ # @param positioned_commits [Array<Hash>] positioned commits
987
+ # @param edges [Array<IR::Edge>] resolved parent connections
988
+ # @return [Array<Hash>] connections with from/to commits and type
989
+ def build_connections(positioned_commits, edges)
990
+ commit_positions = positioned_commits.to_h do |commit|
991
+ [commit[:ir_id], commit]
992
+ end
993
+
994
+ edges.filter_map do |edge|
995
+ parent = commit_positions[edge.source_id]
996
+ commit = commit_positions[edge.target_id]
997
+ next unless parent && commit
998
+
999
+ connection(edge, parent, commit)
1000
+ end
1001
+ end
1002
+
1003
+ def connection(edge, parent, commit)
1004
+ {
1005
+ from: parent[:id], to: commit[:id],
1006
+ from_x: parent[:x], from_y: parent[:y],
1007
+ to_x: commit[:x], to_y: commit[:y],
1008
+ from_branch: parent[:branch], to_branch: commit[:branch],
1009
+ type: edge.role == "parent" ? :normal : edge.role.to_sym
1010
+ }
1011
+ end
1012
+
1013
+ # Builds branch metadata for rendering.
1014
+ #
1015
+ # @param branches [Array<Hash>] ordered branch semantics
1016
+ # @param lane_assignments [Hash] lane assignments
1017
+ # @return [Array<Hash>] branch metadata
1018
+ def build_branch_metadata(branches, lane_assignments, main_branch)
1019
+ main = {
1020
+ name: main_branch,
1021
+ lane: lane_assignments[main_branch] || 0,
1022
+ color: branch_color(0),
1023
+ }
1024
+ branches.reject { |branch| branch[:name] == main_branch }
1025
+ .each_with_index.with_object([main]) do |pair, metadata|
1026
+ branch, index = pair
1027
+ metadata << branch_metadata(branch, index, lane_assignments)
1028
+ end
1029
+ end
1030
+
1031
+ def branch_metadata(branch, index, lane_assignments)
1032
+ {
1033
+ name: branch[:name],
1034
+ lane: lane_assignments[branch[:name]] || (index + 1),
1035
+ color: branch_color(index + 1),
1036
+ order: branch[:order], parent_branch: branch[:parent_branch],
1037
+ created_at_commit: branch[:created_at_commit]
1038
+ }
1039
+ end
1040
+
1041
+ def branch_color(index)
1042
+ palette = theme.colors&.git_graph_palette
1043
+ if palette && !palette.empty?
1044
+ return theme.colors.palette(:git_graph, index)
1045
+ end
1046
+
1047
+ Theme::Registry.get(:default).colors.palette(:git_graph, index)
1048
+ end
1049
+
1050
+ # Calculates the extent along the time axis.
1051
+ #
1052
+ # @param commit_count [Integer] number of commits
1053
+ # @return [Numeric] extent in pixels
1054
+ def calculate_time_span(commit_count)
1055
+ return COMMIT_SPACING * 2 if commit_count.zero?
1056
+
1057
+ commit_count * COMMIT_SPACING
1058
+ end
1059
+
1060
+ # Calculates the extent across the lanes.
1061
+ #
1062
+ # @param lane_assignments [Hash] lane assignments
1063
+ # @return [Numeric] extent in pixels
1064
+ def calculate_lane_span(lane_assignments, lane_positions = nil)
1065
+ return COMMIT_RADIUS * 2 if lane_assignments.empty?
1066
+
1067
+ lane_positions ||= lane_assignments.values.to_h do |lane|
1068
+ [lane, lane * LANE_SPACING]
1069
+ end
1070
+ max_lane = lane_assignments.values.max
1071
+ lane_positions.fetch(max_lane) + (COMMIT_RADIUS * 2)
1072
+ end
1073
+ end
1074
+ end
1075
+ end