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,1150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+ require_relative "../base"
5
+ require_relative "../../diagram/flowchart"
6
+ require_relative "../metadata_yaml"
7
+ require_relative "../mermaid_shapes"
8
+ require_relative "../../diagram/containment"
9
+
10
+ module Etassia
11
+ module Parser
12
+ module Builders
13
+ # Transform for converting flowchart parse trees to diagram models.
14
+ #
15
+ # Handles transformation of nodes, edges, subgraphs, and styling
16
+ # directives from Parslet parse trees into Flowchart diagram objects.
17
+ class Flowchart < Parslet::Transform
18
+ # One parse's bookkeeping.
19
+ #
20
+ # It used to sit in `Thread.current` behind a public `.state`, so
21
+ # any code in the host app could read or rewrite the ids the next
22
+ # parse was about to hand out, and a parse that raised had to
23
+ # remember to clear the slot or it kept the whole parse tree
24
+ # alive. Made inside `apply` and passed down instead, it is
25
+ # reachable from nowhere else and goes away with the parse.
26
+ #
27
+ # A gem runs inside somebody else's app, and these used to be
28
+ # shared on the class: 11 of 36 concurrent parses of a 60-box
29
+ # diagram handed out ids that disagreed with the sequential
30
+ # answer. Every parse now counts on its own object, so two
31
+ # threads cannot see each other at all.
32
+ class Context
33
+ # Generated ids, keyed by statement IDENTITY: two subgraphs
34
+ # written the same way are still two boxes, and each needs its
35
+ # own id.
36
+ attr_reader :ids
37
+
38
+ # Who claimed whom, as won rather than as written. Read by the
39
+ # cycle check once the whole diagram has been walked.
40
+ attr_reader :ownership
41
+
42
+ # The box holding each id, so a member lookup is indexed rather
43
+ # than a scan over every box.
44
+ attr_reader :boxes
45
+
46
+ # Ids already spoken for. The first box to name an id keeps it.
47
+ attr_reader :claimed
48
+
49
+ # Ids given to edges with `A e1@--> B`. A later `e1@{ ... }`
50
+ # addresses the edge and is not a node.
51
+ attr_reader :edge_ids
52
+
53
+ def initialize
54
+ @ids = {}.compare_by_identity
55
+ @ownership = {}
56
+ @boxes = {}
57
+ @claimed = {}
58
+ @edge_ids = Set.new
59
+ @counter = -1
60
+ end
61
+
62
+ # mermaid names a box carrying a free title `subGraph<n>`, and
63
+ # counts from zero within one diagram.
64
+ #
65
+ # @return [String] the next generated subgraph id
66
+ def next_generated_id
67
+ @counter += 1
68
+ "subGraph#{@counter}"
69
+ end
70
+ end
71
+
72
+ # Two ways a name can fail, and they are not the same thing.
73
+ # Mermaid does not know `nope`, and says so in words a corpus case
74
+ # quotes. Mermaid does know `bang`, and draws it as a curved
75
+ # outline etassia has no shape for — naming it a rectangle drew a
76
+ # picture the source did not ask for, which is worse than saying no.
77
+ #
78
+ # @raise [Parser::ParseError] on a name etassia will not draw
79
+ def metadata_shape(name)
80
+ return nil if name.nil?
81
+
82
+ MERMAID_SHAPES.fetch(name) do
83
+ raise Parser::ParseError, unsupported_shape(name)
84
+ end
85
+ end
86
+
87
+ def unsupported_shape(name)
88
+ return "No such shape: #{name}." unless
89
+ UNDRAWABLE_SHAPES.include?(name)
90
+
91
+ "Shape not supported yet: #{name}."
92
+ end
93
+ private :unsupported_shape
94
+
95
+ # mermaid parses the body with js-yaml: a single-line body as a flow
96
+ # mapping, a multiline one as block YAML. Psych does both, so the
97
+ # rules fall out instead of being reimplemented in the grammar.
98
+ #
99
+ # @raise [Parser::ParseError] on YAML mermaid would also refuse
100
+ def metadata_entries(metadata)
101
+ document = metadata_document(metadata)
102
+ # mermaid reads `doc.shape` off whatever `yaml.load` returns, so a
103
+ # node's document that came back null throws there.
104
+ raise Parser::ParseError, "Empty metadata." if document.nil?
105
+
106
+ entries(document)
107
+ end
108
+
109
+ # The parsed YAML of a metadata block, before any node rule reads
110
+ # it. An edge's block stops here: `label` and `shape` mean nothing
111
+ # to an edge, so mermaid never checks them there.
112
+ #
113
+ # Mermaid wraps a single-line body as a flow mapping and uses a
114
+ # multiline body as written. It does not normalize a missing space
115
+ # after a colon: `shape:rect` is an unknown key, so inserting a space
116
+ # would honor something mermaid ignores.
117
+ def metadata_document(metadata)
118
+ body = line_feeds(metadata_body(metadata))
119
+ body = strip_metadata_comments(body)
120
+ body = break_quoted_newlines(body)
121
+ return {} if body.empty?
122
+
123
+ document = body.include?("\n") ? "#{body}\n" : "{\n#{body}\n}"
124
+ MetadataYaml.value(document)
125
+ end
126
+ private :metadata_document
127
+
128
+ # Mermaid turns every carriage return into a line feed before it
129
+ # lexes anything, so a Windows line ending inside the block is an
130
+ # ordinary newline by the time YAML sees it. Reading it as written
131
+ # folded `"one` CR `two"` onto one line, left a `%YAML 1.3` CR
132
+ # unlevelled, and took a body mermaid refuses.
133
+ def line_feeds(body)
134
+ body.gsub(/\r\n?/, "\n")
135
+ end
136
+
137
+ # Mermaid strips comments before lexing, so metadata never sees them.
138
+ # `%%{` is a directive rather than a comment and stays, and a `%%`
139
+ # with nothing after it is not a comment either.
140
+ #
141
+ # The body is a fragment cut from the middle of a line, so its own
142
+ # first character is not the start of a line — only the newlines
143
+ # inside it start one. Anchoring on `^` instead let the leading
144
+ # whitespace run reach back past `@{` and eat the newline that
145
+ # opened the block, which turned `A@{` nl `%% c` nl `}` into an
146
+ # empty body etassia drew as a rectangle. mmdc refuses it, because
147
+ # what is left after the comment goes is `A@{` nl `}`.
148
+ def strip_metadata_comments(body)
149
+ body.gsub(/(?<=\n)#{JS_SPACE}*%%(?!\{)[^\n]+\n?/o, "")
150
+ end
151
+
152
+ # A newline inside a double-quoted value is a line break to
153
+ # mermaid, which rewrites it before js-yaml ever sees it. Letting
154
+ # YAML fold it turned `"one` newline `two"` into `one two`, where
155
+ # mmdc renders `one<br/>two`.
156
+ def break_quoted_newlines(body)
157
+ body.gsub(/"[^"]*"/) { |run| run.gsub(QUOTED_BREAK, "<br/>") }
158
+ end
159
+
160
+ # mermaid's lexer runs `/\n\s*/g` over the text between the quotes,
161
+ # and that `\s` is JavaScript's. Ruby's is the five ASCII ones, so
162
+ # a no-break space after the newline stayed in the label and mmdc
163
+ # dropped it. The set below is JavaScript's exactly: it takes the
164
+ # line and paragraph separators and the byte-order mark, and it
165
+ # leaves the next-line character and the zero-width space alone.
166
+ JS_SPACE = '[\t\n\v\f\r \u00a0\u1680\u2000-\u200a' \
167
+ '\u2028\u2029\u202f\u205f\u3000\ufeff]'
168
+
169
+ QUOTED_BREAK = /\n#{JS_SPACE}*/
170
+
171
+ private_constant :JS_SPACE, :QUOTED_BREAK
172
+
173
+ # A key etassia does not read is left alone rather than validated:
174
+ # `A@{ extra: true }` is a node mmdc draws, and a merge key is one
175
+ # of those. A root that is not a mapping has neither key, and mmdc
176
+ # draws the node untouched.
177
+ #
178
+ # Mermaid reads eight more keys off the document. Six of them
179
+ # (`labelType`, `form`, `pos`, `w`, `h`, `constraint`) leave the
180
+ # picture alone on their own, so ignoring them matches mmdc. Two do
181
+ # not: `A@{ icon: "fa:bell" }` and `A@{ img: "x.png" }` each draw a
182
+ # node with an empty label there, and a plain rectangle named `A`
183
+ # here. Both are node kinds etassia does not have yet.
184
+ def entries(document)
185
+ return {} unless document.is_a?(Hash)
186
+
187
+ READ_KEYS.filter_map do |key|
188
+ usable = usable_value(key, document[key])
189
+ [key, usable] if usable
190
+ end.to_h
191
+ end
192
+
193
+ READ_KEYS = %w[shape label].freeze
194
+
195
+ # An empty repeat captures as [], not as an empty slice.
196
+ def metadata_body(metadata)
197
+ return "" unless metadata.is_a?(Hash)
198
+
199
+ value = metadata[:body]
200
+ value.is_a?(Array) ? "" : value.to_s
201
+ end
202
+
203
+ # Mermaid tests the value for truth before using it, so `null`,
204
+ # `false`, `0` and `""` are all no-ops rather than errors — the
205
+ # node keeps what it had. Reading the raw AST made `shape: null`
206
+ # a lookup for a shape called "null".
207
+ #
208
+ # A sequence renders its first element and nothing else: mmdc draws
209
+ # `label: [one, two]` as `one`, and refuses a shape or a nested
210
+ # sequence, where the value it reaches for is not a scalar.
211
+ def usable_value(key, value)
212
+ return nil unless truthy?(value)
213
+
214
+ value = value.first if value.is_a?(Array) && key == "label"
215
+ return value if value.is_a?(String)
216
+
217
+ raise Parser::ParseError, "Unusable #{key} in metadata."
218
+ end
219
+
220
+ # `null`, `false`, `0` and `""` are all falsy to mermaid, which
221
+ # skips the key rather than failing on it. An empty collection is
222
+ # NOT falsy in JavaScript, and mmdc does refuse `label: []`.
223
+ FALSY_VALUES = [nil, false, 0, ""].freeze
224
+ private_constant :FALSY_VALUES
225
+
226
+ def truthy?(value)
227
+ !FALSY_VALUES.include?(value) &&
228
+ !(value.is_a?(Float) && value.nan?)
229
+ end
230
+
231
+ # Shape delimiter to type mapping
232
+ SHAPE_MAP = {
233
+ "[]" => "rect",
234
+ "()" => "rounded",
235
+ "([])" => "stadium",
236
+ "[[]]" => "subroutine",
237
+ "[()]" => "cylindrical",
238
+ "(())" => "circle",
239
+ "((()))" => "double_circle",
240
+ ">]" => "asymmetric",
241
+ "{}" => "rhombus",
242
+ "{{}}" => "hexagon",
243
+ "[//]" => "parallelogram",
244
+ '[\\\\]' => "parallelogram_alt",
245
+ '[/\\]' => "trapezoid",
246
+ '[\\/]' => "trapezoid_alt",
247
+ }.freeze
248
+
249
+ # Which marker each character draws. One table serves both ends
250
+ # because the grammar only ever puts `<` at the start and `>` at
251
+ # the end — see the `link_start` and `link_end` rules in
252
+ # `Grammars::Flowchart`, which are the sole producers of this
253
+ # token.
254
+ LINK_MARKERS = {
255
+ ">" => "arrow", "<" => "arrow",
256
+ "x" => "cross", "o" => "circle"
257
+ }.freeze
258
+ private_constant :LINK_MARKERS
259
+
260
+ # A comment line inside a label, whitespace as mermaid counts it.
261
+ COMMENT_LINE = Regexp.new(
262
+ "\n[\t\v\f \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F" \
263
+ "\u205F\u3000\uFEFF]*%%[^\n]*",
264
+ )
265
+ private_constant :COMMENT_LINE
266
+
267
+ LABEL_EDGE_SPACE = /\A#{JS_SPACE}+|#{JS_SPACE}+\z/
268
+ private_constant :LABEL_EDGE_SPACE
269
+
270
+ # mermaid resolves the alias lexemes to a direction word before
271
+ # anything reads one, so `graph <` lays out exactly like `graph RL`.
272
+ # Measured against mmdc 11.12.0: `<` RL, `>` LR, `^` BT, `v` and
273
+ # `BR` TB. Keeping the raw lexeme sent the three glyphs down the
274
+ # default branch in the graph transform and drew them top-to-bottom.
275
+ DIRECTION_ALIASES = {
276
+ "<" => "RL",
277
+ ">" => "LR",
278
+ "^" => "BT",
279
+ "v" => "TB",
280
+ "BR" => "TB",
281
+ }.freeze
282
+
283
+ # Mermaid honours a leading marker only when the trailing one
284
+ # matches it. `o--x` draws the cross and nothing at the source, and
285
+ # a mismatched pair drops back to normal thickness but keeps its
286
+ # dots. `o==x` is no thicker than `o--x`, while `<-.-x` stays
287
+ # dotted. Both were read as two-ended links here.
288
+ def link_type(token)
289
+ return "invisible" if token.start_with?("~")
290
+
291
+ head = LINK_MARKERS[token[-1]]
292
+ tail = LINK_MARKERS[token[0]]
293
+ matched = !head.nil? && head == tail
294
+ ends = matched ? "#{head}_both" : head || "line"
295
+
296
+ "#{link_weight(token, tail, matched)}#{ends}"
297
+ end
298
+ private :link_type
299
+
300
+ # A thick body keeps its weight only when it carries no leading
301
+ # marker, or a leading marker mermaid actually honours.
302
+ def link_weight(token, tail, matched)
303
+ return "dotted_" if token.include?(".")
304
+ return "thick_" if token.include?("=") && (matched || tail.nil?)
305
+
306
+ ""
307
+ end
308
+ private :link_weight
309
+
310
+ # Direction value
311
+ rule(dir_value: simple(:v)) { v.to_s }
312
+
313
+ # Node ID
314
+ rule(node_id: simple(:id)) { id.to_s }
315
+
316
+ # Shape with label
317
+ rule(
318
+ shape: {
319
+ open: simple(:o),
320
+ label: simple(:l),
321
+ close: simple(:c),
322
+ },
323
+ ) do
324
+ delims = "#{o}#{c}"
325
+ {
326
+ shape_type: SHAPE_MAP[delims] || "rect",
327
+ label: l.to_s.strip,
328
+ }
329
+ end
330
+
331
+ # Handle empty labels
332
+ rule(
333
+ shape: {
334
+ open: simple(:o),
335
+ label: sequence(:_),
336
+ close: simple(:c),
337
+ },
338
+ ) do
339
+ delims = "#{o}#{c}"
340
+ {
341
+ shape_type: SHAPE_MAP[delims] || "rect",
342
+ label: "",
343
+ }
344
+ end
345
+
346
+ # Edge label
347
+ rule(label: simple(:l)) { l.to_s.strip }
348
+ rule(label: sequence(:l)) { l.join.strip }
349
+
350
+ # Helper method to create nodes
351
+ def create_node(node_data)
352
+ Diagram::FlowchartNode.new.tap do |n|
353
+ n.id = node_data[:node_id]
354
+ n.label = node_data[:label] || node_data[:node_id]
355
+ n.shape = node_data[:shape_type] || "rect"
356
+ n.classes = node_data[:classes] if node_data[:classes]
357
+ end
358
+ end
359
+
360
+ # `link_token` is the raw lexeme the grammar matched — `-->`,
361
+ # `o--x`, `~~~`. `link_type` is what turns it into the edge's
362
+ # arrow type; the two are not the same string.
363
+ def create_edge(source_id, target_data, link_token, label = nil,
364
+ minimum_rank_span = nil)
365
+ Diagram::FlowchartEdge.new.tap do |e|
366
+ e.source_id = source_id
367
+ e.target_id = target_data[:node_id]
368
+ e.arrow_type = link_type(link_token)
369
+ e.minimum_rank_span = minimum_rank_span
370
+ e.label = label
371
+ end
372
+ end
373
+ private :create_edge
374
+
375
+ # Mermaid derives a labelled link's length from its closing half,
376
+ # not from the display token made by joining both halves. It removes
377
+ # the last character, removes a matching start marker on a double-ended
378
+ # link, then counts the remaining body beyond the ordinary first rank.
379
+ def minimum_rank_span(edge_data)
380
+ token = end_link_token(edge_data).strip
381
+ line = token[0...-1]
382
+ line = line[1..] if matching_end_markers?(token)
383
+ dots = line.count(".")
384
+ span = dots.positive? ? dots : line.length - 1
385
+ span if span > 1
386
+ end
387
+ private :minimum_rank_span
388
+
389
+ def end_link_token(edge_data)
390
+ return edge_data[:arrow][:token].to_s if edge_data[:arrow]
391
+
392
+ edge_data[:close].to_s
393
+ end
394
+ private :end_link_token
395
+
396
+ def matching_end_markers?(token)
397
+ LINK_MARKERS[token[0]] == LINK_MARKERS[token[-1]] &&
398
+ !LINK_MARKERS[token[0]].nil?
399
+ end
400
+ private :matching_end_markers?
401
+
402
+ # Process a parse tree of [header, *statements]. The Context is local
403
+ # to this call, so its ids and retained parse nodes cannot cross parses.
404
+ def apply(tree, diagram = nil)
405
+ diagram ||= Diagram::Flowchart.new
406
+ tree = [tree] unless tree.is_a?(Array)
407
+ apply_header(diagram, tree.first)
408
+ statements = tree.drop(1)
409
+ context = Context.new
410
+ assign_ids(statements, context)
411
+ process_statements(diagram, statements, context)
412
+ reject_ownership_cycles(context)
413
+ promote_referenced_subgraphs(diagram, context)
414
+
415
+ diagram
416
+ end
417
+
418
+ def apply_header(diagram, header)
419
+ return unless header.is_a?(Hash) && header[:direction]
420
+
421
+ direction = header[:direction]
422
+ value = direction[:dir_value] || direction
423
+ diagram.direction = canonical_direction(value.to_s)
424
+ end
425
+ private :apply_header
426
+
427
+ def canonical_direction(value)
428
+ DIRECTION_ALIASES.fetch(value, value)
429
+ end
430
+ private :canonical_direction
431
+
432
+ def process_statements(diagram, statements, context, parents = [])
433
+ statements.each do |stmt|
434
+ next unless stmt.is_a?(Hash)
435
+
436
+ process_statement(diagram, stmt, context, parents)
437
+ end
438
+ end
439
+
440
+ STATEMENT_HANDLERS = {
441
+ node: :process_node_statement,
442
+ node_id: :process_standalone_statement,
443
+ subgraph_keyword: :process_subgraph_statement,
444
+ direction_keyword: :process_direction_statement,
445
+ style_keyword: :process_style_statement,
446
+ link_style_keyword: :process_link_style_statement,
447
+ }.freeze
448
+ private_constant :STATEMENT_HANDLERS
449
+
450
+ def process_statement(diagram, stmt, context, parents)
451
+ pair = STATEMENT_HANDLERS.find { |key, _handler| stmt[key] }
452
+ send(pair.last, diagram, stmt, context, parents) if pair
453
+ end
454
+ private :process_statement
455
+
456
+ def process_node_statement(diagram, stmt, context, parents)
457
+ process_node_edge_statement(diagram, stmt, parents.last, context)
458
+ end
459
+ private :process_node_statement
460
+
461
+ def process_standalone_statement(diagram, stmt, context, parents)
462
+ process_standalone_node(diagram, stmt, parents.last, context)
463
+ end
464
+ private :process_standalone_statement
465
+
466
+ def process_subgraph_statement(diagram, stmt, context, parents)
467
+ process_subgraph(diagram, stmt, context, parents)
468
+ end
469
+ private :process_subgraph_statement
470
+
471
+ def process_direction_statement(_diagram, stmt, context, parents)
472
+ set_direction(parents.last, stmt[:dir_value], context)
473
+ end
474
+ private :process_direction_statement
475
+
476
+ def process_style_statement(diagram, stmt, context, _parents)
477
+ declare_styled_node(diagram, stmt[:style_target], context)
478
+ node = diagram.find_node(stmt[:style_target].to_s)
479
+ apply_node_paint(node, stmt[:style_props]) if node
480
+ end
481
+ private :process_style_statement
482
+
483
+ # Commas within paint functions belong to the value. Only a comma
484
+ # introducing another property ends a declaration.
485
+ def apply_node_paint(node, properties)
486
+ declarations = properties.to_s.scan(
487
+ /(?:\A|,)\s*(fill|stroke)\s*:\s*(.*?)(?=,\s*[\w-]+\s*:|\z)/m,
488
+ )
489
+ declarations.each do |property, value|
490
+ paint = Diagram::FlowchartNode.paint_value(value)
491
+ node.public_send("#{property}=", paint) if paint
492
+ end
493
+ end
494
+ private :apply_node_paint
495
+
496
+ def process_link_style_statement(diagram, stmt, _context, _parents)
497
+ process_link_style(diagram, stmt)
498
+ end
499
+ private :process_link_style_statement
500
+
501
+ def process_standalone_node(diagram, stmt, parent, context)
502
+ node_data = {
503
+ node_id: stmt[:node_id], shape_type: "rect", label: stmt[:node_id]
504
+ }
505
+ add_or_update_node(diagram, node_data)
506
+ claim_member(parent, stmt[:node_id].to_s, context)
507
+ end
508
+ private :process_standalone_node
509
+
510
+ def process_link_style(diagram, stmt)
511
+ check_link_indices(diagram, stmt[:link_targets].to_s)
512
+ check_link_words(stmt)
513
+ end
514
+ private :process_link_style
515
+
516
+ # mmdc refuses `linkStyle 1` when only edge 0 has been drawn so
517
+ # far, so the count is the edges written above the statement.
518
+ def check_link_indices(diagram, targets)
519
+ return if targets == "default"
520
+
521
+ last = diagram.edges.size - 1
522
+ targets.split(",").each do |token|
523
+ index = token.to_i
524
+ # `00` and `01` name no edge: mermaid looks the text up as a key.
525
+ next if index.to_s == token && index <= last
526
+
527
+ raise Parser::ParseError,
528
+ "The index #{token} for linkStyle is out of bounds."
529
+ end
530
+ end
531
+ private :check_link_indices
532
+
533
+ # mermaid lexes these two as keywords even inside the style text,
534
+ # so `stroke:default`, `stroke:1default` and a bare `interpolate`
535
+ # are refused while `stroke:defaults` and `stroke:x-default` are
536
+ # plain text.
537
+ LINK_STYLE_KEYWORD = Regexp.new(
538
+ "(?:\\A|(?<=[,:;]|#{JS_SPACE}))" \
539
+ "(?:#|[0-9]+)?(interpolate|default)(?![A-Za-z0-9_])",
540
+ )
541
+
542
+ # mermaid rewrites `#name;` as an entity before it parses, except
543
+ # the last `;` of a lowercase `style ...:#...;` run, then the last
544
+ # `;` of a `classDef ...:#...;` run applied the same way right
545
+ # after. `linkStyle` is neither lowercase nor `classDef`, so
546
+ # `linkStyle 0 stroke:#f00;` holds an entity no style can take.
547
+ STYLE_RUN = /style.*:(?:(?!#{JS_SPACE}).)*#.*;/
548
+ CLASS_DEF_RUN = /classDef.*:(?:(?!#{JS_SPACE}).)*#.*;/
549
+ ENTITY = /#\w+;/
550
+
551
+ private_constant :LINK_STYLE_KEYWORD, :STYLE_RUN, :CLASS_DEF_RUN,
552
+ :ENTITY
553
+
554
+ # Mermaid runs entity stripping over the whole source line, so a curve
555
+ # name and its properties share one entity check even though the
556
+ # grammar splits them into fields. A terminating `;` is part of an
557
+ # entity too.
558
+ def check_link_words(stmt)
559
+ [stmt[:link_curve], stmt[:link_props]].each do |text|
560
+ word = text.to_s[LINK_STYLE_KEYWORD, 1] or next
561
+ raise Parser::ParseError,
562
+ "linkStyle cannot use `#{word}` here: " \
563
+ "write `interpolate <curve>` first, then the styles."
564
+ end
565
+ text = [stmt[:link_curve], stmt[:link_props]].compact.join(" ")
566
+ check_link_entities("#{text}#{stmt[:link_end]}")
567
+ end
568
+ private :check_link_words
569
+
570
+ def check_link_entities(text)
571
+ entity = text.sub(STYLE_RUN, &:chop)
572
+ .sub(CLASS_DEF_RUN, &:chop)[ENTITY] or return
573
+
574
+ raise Parser::ParseError,
575
+ "linkStyle reads `#{entity}` as an HTML entity; " \
576
+ "drop the `;` after the colour."
577
+ end
578
+ private :check_link_entities
579
+
580
+ # `style Q ...` names a vertex, and mermaid draws it even when no
581
+ # other statement mentions it, unless an earlier edge carries that
582
+ # id: then mermaid drops the line and draws no vertex.
583
+ def declare_styled_node(diagram, target, context)
584
+ return if diagram.find_node(target.to_s)
585
+ return if context.edge_ids.include?(target.to_s)
586
+
587
+ node_data = { node_id: target, shape_type: "rect", label: target }
588
+ add_or_update_node(diagram, node_data)
589
+ end
590
+
591
+ # A subgraph cannot be its own ancestor. mmdc refuses that with
592
+ # "Setting b as parent of a would create a cycle", and the
593
+ # transform was discarding the ids needed to see it.
594
+ #
595
+ # The id is not always the written one. A subgraph declared with a
596
+ # FREE title gets a generated `subGraph<n>` — so `subgraph s Some
597
+ # Title` containing `s` is fine, and containing `subGraph0` is the
598
+ # cycle. Only a bare or bracket-titled declaration keeps its word.
599
+ def process_subgraph(diagram, stmt, context, parents = [])
600
+ id = subgraph_id(stmt, context)
601
+ ancestry = parents + [id]
602
+ record_subgraph(diagram, stmt, id, parents.last, context)
603
+ return unless stmt[:subgraph_statements]
604
+
605
+ sub_stmts = stmt[:subgraph_statements]
606
+ sub_stmts = [sub_stmts] unless sub_stmts.is_a?(Array)
607
+ process_statements(diagram, sub_stmts, context, ancestry)
608
+ end
609
+
610
+ # Ids are handed out in one pass before anything reads them.
611
+ # `subgraph_id` is asked for the same statement from several
612
+ # places, and a counter that ticked on every call skipped numbers:
613
+ # a subgraph nested in another came out `subGraph2`.
614
+ #
615
+ # mermaid numbers the innermost box first, so the walk goes down
616
+ # before it hands anything out. Measured on mmdc 11.12.0:
617
+ # `subgraph a One` holding `subgraph b Two` makes b subGraph0 and
618
+ # a subGraph1.
619
+ def assign_ids(statements, context)
620
+ statements = [statements] unless statements.is_a?(Array)
621
+
622
+ statements.each do |stmt|
623
+ next unless stmt.is_a?(Hash) && stmt[:subgraph_keyword]
624
+
625
+ if stmt[:subgraph_statements]
626
+ assign_ids(stmt[:subgraph_statements], context)
627
+ end
628
+ context.ids[stmt] = allocate_id(stmt, context)
629
+ end
630
+ end
631
+
632
+ # A quoted id arrives as a Hash, and `to_s` on that gave
633
+ # `{string: "s"}`, so a quoted self-parent slipped through.
634
+ def subgraph_id(stmt, context)
635
+ context.ids[stmt] ||= allocate_id(stmt, context)
636
+ end
637
+
638
+ def allocate_id(stmt, context)
639
+ generated_id?(stmt) ? context.next_generated_id : written_id(stmt)
640
+ end
641
+
642
+ # A free title always generates one. So does a bare id carrying
643
+ # whitespace — `subgraph "a b"` is subGraph0 titled "a b". A
644
+ # bracket title keeps the written id even then, so
645
+ # `subgraph "a b" [T]` stays `a b`. All measured on mmdc 11.12.0.
646
+ def generated_id?(stmt)
647
+ return true if free_titled?(stmt)
648
+
649
+ stmt[:subgraph_title].nil? && written_id(stmt).match?(/\s/)
650
+ end
651
+
652
+ def written_id(stmt)
653
+ plain_id(stmt[:subgraph_id])
654
+ end
655
+
656
+ # A quoted id arrives as `{string: <slice>}`, so a bare `to_s`
657
+ # gives `{string: "a"@21}` and never matches the id it names.
658
+ # Every place that compares an id has to unwrap it, not just the
659
+ # subgraph's own — the cycle walk reads node ids too.
660
+ def plain_id(raw)
661
+ return raw.to_s unless raw.is_a?(Hash)
662
+
663
+ value = raw.values.first
664
+ value.is_a?(Array) ? value.join : value.to_s
665
+ end
666
+
667
+ # The grammar captures a bracket title WITHOUT its brackets, so
668
+ # the text cannot tell the two forms apart. They carry different
669
+ # keys for that reason — testing for a leading `[` read every
670
+ # bracketed title as free and generated an id for it.
671
+ def free_titled?(stmt)
672
+ !stmt[:subgraph_free_title].nil?
673
+ end
674
+
675
+ # A box cannot end up inside itself. An ancestor chain only sees
676
+ # loops that nest, and two declarations can close one between
677
+ # them — `a` holding `b` while `b` holds `a` — so each edge is
678
+ # recorded as its claim is won, and the whole set is checked
679
+ # once the diagram has been read.
680
+ #
681
+ # Read from the ARBITRATED graph, not from what the source
682
+ # writes. A claim that lost makes no edge, because the box it
683
+ # named already belongs to somebody else and mermaid never hangs
684
+ # it there. Measured on mmdc 11.12.0: `subgraph c` holding `b`,
685
+ # then `b` holding `a`, then `a` holding `b` draws clusters `c`
686
+ # and `b` — the last claim loses, so nothing closes. Walking the
687
+ # written source instead refused a diagram mmdc draws.
688
+ def reject_ownership_cycles(context)
689
+ loop_found = Diagram::Containment.looping_pair(context.ownership)
690
+ return if loop_found.nil?
691
+
692
+ parent, child = loop_found
693
+ raise Parser::ParseError,
694
+ "Setting #{parent} as parent of #{child} would create " \
695
+ "a cycle."
696
+ end
697
+
698
+ # Declaration order is kept in the model. Ownership is independent:
699
+ # a parent declared later can adopt an earlier top-level subgraph.
700
+ #
701
+ # Being written inside a box is a claim like any other, so a
702
+ # declaration takes the id only if nothing named it earlier. When
703
+ # it loses, the box is still built and still holds what it
704
+ # contains — it just does not become anybody's child, and the
705
+ # earlier claimant adopts it once the walk is over.
706
+ #
707
+ # The first box wins a duplicated name. When a later declaration is
708
+ # nested, the winning box—not the just-built empty duplicate—is
709
+ # parented, matching mermaid's visible containment.
710
+ def record_subgraph(diagram, stmt, id, parent, context)
711
+ box = Diagram::FlowchartSubgraph.new
712
+ box.id = id
713
+ box.declared_title = subgraph_label(stmt)
714
+ diagram.subgraphs << box
715
+ holding = (context.boxes[id] ||= box)
716
+ holder = claim(parent, id, context)
717
+ return if holder.nil?
718
+
719
+ holding.parent_id = parent
720
+ holder.child_ids << id
721
+ end
722
+
723
+ # A bracketed title stands alone. A free one keeps the word that
724
+ # looks like an id, and the gap after it, because mermaid labels
725
+ # `subgraph s Some Title` with the whole run and generates the id
726
+ # separately. Measured against mmdc 11.12.0.
727
+ def subgraph_label(stmt)
728
+ return stmt[:subgraph_title].to_s if stmt[:subgraph_title]
729
+
730
+ if free_titled?(stmt)
731
+ return [written_id(stmt), stmt[:subgraph_free_gap],
732
+ stmt[:subgraph_free_title]].join
733
+ end
734
+
735
+ # The text that was too spaced to serve as an id becomes the
736
+ # title instead, trimmed: `subgraph " ab"` is titled "ab".
737
+ generated_id?(stmt) ? written_id(stmt).strip : nil
738
+ end
739
+
740
+ # The one ownership rule: the first box to name an id keeps it,
741
+ # whether it named a node or another box. Source order decides
742
+ # and nothing else. Measured against mmdc 11.12.0; the shapes
743
+ # are one apiece in flowchart_subgraph_ownership_spec.rb.
744
+ #
745
+ # A declaration at the TOP level claims nothing, which is why
746
+ # `subgraph b / X / end` followed by `subgraph c / b / end` still
747
+ # leaves b inside c.
748
+ #
749
+ # This used to be written twice, once here and once as `adopt`'s
750
+ # "unless the parent is already set". The two disagreed, and a
751
+ # later nested declaration took an id an earlier reference had
752
+ # already claimed.
753
+ #
754
+ # @return [Diagram::FlowchartSubgraph, nil] the box that took it
755
+ def claim(parent, id, context)
756
+ box = enclosing(parent, context)
757
+ return nil if box.nil? || id.empty? || context.claimed[id]
758
+
759
+ context.claimed[id] = true
760
+ # The edge the cycle check reads. Only a WINNING claim makes
761
+ # one, which is what stops a losing claim from closing a loop
762
+ # that mermaid never sees. No `include?` guard: an id is
763
+ # claimed once, so it can be listed only once.
764
+ (context.ownership[box.id] ||= []) << id
765
+ box
766
+ end
767
+
768
+ # `<<`, not `+=`. Each instance gets its own array from the
769
+ # attribute default, so appending touches nobody else, and `+=`
770
+ # went through the model's collection setter once per member —
771
+ # 1,600 nodes in one box took 2.3 seconds instead of half a
772
+ # millisecond.
773
+ def claim_member(parent, node_id, context)
774
+ box = claim(parent, node_id, context)
775
+ box.node_ids << node_id if box
776
+ end
777
+
778
+ # Indexed rather than searched. Scanning every box for every node
779
+ # made a 240-subgraph diagram take four times as long to parse as
780
+ # it did before subgraphs were modelled.
781
+ def enclosing(parent, context)
782
+ parent.nil? ? nil : context.boxes[parent]
783
+ end
784
+
785
+ # `direction` inside a box turns that box's contents, and mmdc
786
+ # accepts a top-level one without honouring it — measured on
787
+ # 11.12.0, where `flowchart TD` followed by `direction LR` still
788
+ # stacks its nodes vertically. So there is nothing to do when no
789
+ # box encloses it.
790
+ def set_direction(parent, value, context)
791
+ box = enclosing(parent, context)
792
+ box.direction = value.to_s if box
793
+ end
794
+
795
+ # An id that names a subgraph names the BOX, not a node beside it.
796
+ # `one --> two` joins two clusters, and a bare `b` inside
797
+ # `subgraph a` nests box b in a. Measured on mmdc 11.12.0: every
798
+ # form of the reference — bare, shaped, an edge endpoint, written
799
+ # before or after the declaration — draws no node.
800
+ #
801
+ # Left until the walk is over because an EMPTY box is the
802
+ # exception: mmdc draws `subgraph one` / `end` joined by an edge
803
+ # as a plain node, and nothing knows what a box holds until every
804
+ # statement has been read.
805
+ #
806
+ # The ids are collected first and the nodes swept once. Asking
807
+ # `find_node` per box rescans the whole collection each time, and
808
+ # a chain of 2,000 boxes spent 17 seconds in the transform where
809
+ # this takes under one.
810
+ def promote_referenced_subgraphs(diagram, context)
811
+ holders = holders_by_member(diagram)
812
+ promoted = surviving_boxes(context)
813
+ return if promoted.empty?
814
+
815
+ promoted.each_value { |box| adopt(holders[box.id], box) }
816
+ diagram.nodes.reject! { |node| promoted.key?(node.id) }
817
+ end
818
+
819
+ # A box is worth drawing when it holds something, and holding
820
+ # something is exactly what the model already calls `drawable?`.
821
+ #
822
+ # This used to settle by fixed point, re-checking whether taking
823
+ # a reference out of a box had left that box empty. Once `claim`
824
+ # became the single ownership rule that stopped being possible:
825
+ # an id sitting in a box's `node_ids` is an id that box WON, so
826
+ # no declaration parented it elsewhere and the box keeps it.
827
+ # `adopt` is the only later writer and it MOVES an id from
828
+ # `node_ids` to `child_ids` on the same holder, so no pass can
829
+ # leave a box holding less than it holds now.
830
+ #
831
+ # Measured before deleting the loop — 2,312 sources, 301 calls to
832
+ # the old predicate, zero that disagreed with `drawable?`.
833
+ def surviving_boxes(context)
834
+ context.boxes.select { |_id, box| box.drawable? }
835
+ end
836
+
837
+ def holders_by_member(diagram)
838
+ diagram.subgraphs.each_with_object({}) do |box, acc|
839
+ box.node_ids.each { |id| acc[id] = box }
840
+ end
841
+ end
842
+
843
+ # The box that claimed the reference becomes the parent, with
844
+ # nobody to argue with. Two guards used to say so defensively;
845
+ # both were unreachable, because `parent_id` has exactly two
846
+ # writers — `record_subgraph`, which only runs on a WINNING
847
+ # claim, and this method, which runs after the walk. Holding
848
+ # `box.id` in `node_ids` means this holder won that claim, so
849
+ # no declaration can have set a parent or listed the child.
850
+ # `promoted` is keyed by id, so no box is adopted twice either.
851
+ #
852
+ # Checked as well as argued, before the guards came out: 2,312
853
+ # sources, 50 adoptions, none with a parent already set and none
854
+ # where the holder already listed the child.
855
+ def adopt(holder, box)
856
+ return if holder.nil?
857
+
858
+ holder.node_ids.delete(box.id)
859
+ box.parent_id = holder.id
860
+ holder.child_ids << box.id
861
+ end
862
+
863
+ # The last two arguments are new, and both default, so a caller
864
+ # written against the old two-argument form still works: with no
865
+ # enclosing box there is nothing to claim, and the throwaway
866
+ # context is only there to keep the claim path from special-casing
867
+ # its absence.
868
+ def process_node_edge_statement(diagram, stmt, parent = nil,
869
+ context = Context.new)
870
+ edges = stmt[:edges]
871
+ nodes = [stmt[:node], *stmt[:group]]
872
+ sources = declare_group(diagram, nodes, parent, context,
873
+ linkless: edges.nil?)
874
+ return unless edges
875
+
876
+ edge_list(edges).each do |edge_data|
877
+ next unless edge_data.is_a?(Hash)
878
+
879
+ sources = process_edge(diagram, edge_data, sources, parent, context)
880
+ end
881
+ end
882
+
883
+ def edge_list(edges)
884
+ edges.is_a?(Array) ? edges : [edges]
885
+ end
886
+ private :edge_list
887
+
888
+ # `arrow` arrives wrapped with the other edge fields, so it cannot be
889
+ # unwrapped by a Parslet rule that matches only that key. Validate its
890
+ # token before the defensive target guard; the grammar currently makes
891
+ # targets mandatory, but the guard protects a future relaxed rule.
892
+ def process_edge(diagram, edge_data, sources, parent, context)
893
+ token = link_token(edge_data)
894
+ label = edge_label(edge_data)
895
+ rank_span = minimum_rank_span(edge_data)
896
+ return sources unless edge_data[:target]
897
+
898
+ nodes = [edge_data[:target], *edge_data[:group]]
899
+ targets = declare_group(diagram, nodes, parent, context)
900
+ record_edge_id(edge_data, context)
901
+ connect_groups(diagram, sources, targets, [token, label, rank_span])
902
+ targets
903
+ end
904
+ private :process_edge
905
+
906
+ def record_edge_id(edge_data, context)
907
+ context.edge_ids << edge_data[:edge_id].to_s if edge_data[:edge_id]
908
+ end
909
+ private :record_edge_id
910
+
911
+ # `A & B --> C & D` links every source to every target.
912
+ def connect_groups(diagram, sources, targets, link)
913
+ sources.product(targets).each do |source, target|
914
+ diagram.edges << create_edge(source[:node_id], target, *link)
915
+ end
916
+ end
917
+ private :connect_groups
918
+
919
+ # The text a link carries: nil when it has no label, or when the
920
+ # pipes hold only space. Written around the link,
921
+ # `A -- text --> B`, it arrives as the run itself; written after it,
922
+ # `A -->|text| B`, as the run between the pipes.
923
+ def edge_label(edge_data)
924
+ label = edge_data[:label]
925
+ return inline_label(label) if edge_data[:open]
926
+
927
+ piped_label(label[:pipe_text]) if label.is_a?(Hash)
928
+ end
929
+ private :edge_label
930
+
931
+ # @raise [Parser::ParseError] on a label that nothing is left of
932
+ def inline_label(label)
933
+ text = unquoted_label(label)
934
+ return text unless text.empty?
935
+
936
+ raise Parser::ParseError, "An inline link label cannot be empty."
937
+ end
938
+ private :inline_label
939
+
940
+ # Mermaid trims the text between pipes, and `| |` is a label of
941
+ # nothing rather than an error. Nothing left is nil, so the link
942
+ # has no label.
943
+ def piped_label(text)
944
+ trimmed = unquoted_label(text).gsub(LABEL_EDGE_SPACE, "")
945
+ trimmed unless trimmed.empty?
946
+ end
947
+ private :piped_label
948
+
949
+ # Mermaid deletes a whole comment line before it lexes, and the
950
+ # quotes of a quoted run are delimiters, not text.
951
+ def unquoted_label(label)
952
+ label.to_s.gsub(COMMENT_LINE, "").delete('"')
953
+ end
954
+ private :unquoted_label
955
+
956
+ # The nodes one side of a link names: the first, and any that
957
+ # `&` joined to it. Returns their node data, in source order.
958
+ #
959
+ # A member that addresses an edge id (`e1@{ ... }`, `e1:::foo`) names
960
+ # no node and changes no node that shares the id, except for its
961
+ # class. Such a shared node still belongs to an enclosing subgraph
962
+ # that names it. In a link, the id is still returned as its endpoint.
963
+ def declare_group(diagram, nodes, parent, context, linkless: false)
964
+ nodes.filter_map do |node_hash|
965
+ declare_node(diagram, node_hash, parent, context, linkless)
966
+ end
967
+ end
968
+ private :declare_group
969
+
970
+ def declare_node(diagram, node_hash, parent, context, linkless)
971
+ if edge_reference?(node_hash, context)
972
+ return declare_edge_reference(
973
+ diagram, node_hash, parent, context, linkless
974
+ )
975
+ end
976
+
977
+ node_data = extract_node_data(node_hash)
978
+ add_or_update_node(diagram, node_data)
979
+ claim_member(parent, node_data[:node_id].to_s, context)
980
+ node_data
981
+ end
982
+ private :declare_node
983
+
984
+ def declare_edge_reference(
985
+ diagram, node_hash, parent, context, linkless
986
+ )
987
+ metadata_document(node_hash[:metadata])
988
+ shared = class_shared_node(diagram, node_hash)
989
+ claim_member(parent, shared.id, context) if shared
990
+ { node_id: node_hash[:node_id] } unless linkless
991
+ end
992
+ private :declare_edge_reference
993
+
994
+ # Any mention of a known edge id — bare (`e1`), shaped (`e1[x]`),
995
+ # carrying `@{...}`/`:::foo`, or used as a link endpoint — addresses
996
+ # the edge named `e1`, even when a node is also called `e1`: mermaid
997
+ # checks the edges first and stops there, so its shape, label and
998
+ # metadata reach no node.
999
+ def edge_reference?(node_hash, context)
1000
+ context.edge_ids.include?(node_hash[:node_id].to_s)
1001
+ end
1002
+ private :edge_reference?
1003
+
1004
+ # Mermaid gives a `:::foo` class to every node, edge and subgraph
1005
+ # with that id, so a node sharing an edge's id still takes it.
1006
+ def class_shared_node(diagram, node_hash)
1007
+ node = diagram.find_node(node_hash[:node_id].to_s)
1008
+ node.classes = node_hash[:inline_class].to_s if
1009
+ node && node_hash[:inline_class]
1010
+ node
1011
+ end
1012
+ private :class_shared_node
1013
+ # A link written around its label, `A -- text --> B`, arrives in
1014
+ # two halves, and their concatenation reads like the one-piece
1015
+ # link of the same kind: `-- -->` is `---->`.
1016
+ #
1017
+ # Mermaid reads a `<` opening the closing half as the start head
1018
+ # of the whole link, so it moves to the front, and it refuses an
1019
+ # `x` or `o` opening half whose closing half does not end in the
1020
+ # same marker; a `<` needs a `>` to close it. An `x` or `o` at the
1021
+ # very start of the closing half of a link that already opens with
1022
+ # `<` draws nothing of its own: `A <-- t x--> B` and
1023
+ # `A <-- t o--> B` render as a plain double arrow, measured against
1024
+ # mermaid 11.12.0's own parser.
1025
+ # @raise [Parser::ParseError] on an unmatched opening marker
1026
+ def link_token(edge_data)
1027
+ return edge_data[:arrow][:token].to_s if edge_data[:arrow]
1028
+
1029
+ open = edge_data[:open].to_s
1030
+ raw_close = edge_data[:close].to_s
1031
+ reject_unmatched_marker(open, raw_close)
1032
+ close = closing_half(open, raw_close)
1033
+ return "#{open}#{close}" unless lifts_start_head?(open, close)
1034
+
1035
+ "#{close[0]}#{open}#{close[1..]}"
1036
+ end
1037
+
1038
+ def closing_half(open, close)
1039
+ return close unless open.start_with?("<") && close.match?(/\A[ox]/)
1040
+
1041
+ close[1..]
1042
+ end
1043
+ private :closing_half
1044
+
1045
+ # The start head of the closing half, when the opening half has
1046
+ # none, belongs to the whole link: `<` always, `x` or `o` when the
1047
+ # other end carries the same one.
1048
+ def lifts_start_head?(open, close)
1049
+ return false if open.match?(/\A[ox<]/)
1050
+
1051
+ close.start_with?("<") ||
1052
+ (close.match?(/\A[ox]/) && close[-1] == close[0])
1053
+ end
1054
+ private :lifts_start_head?
1055
+
1056
+ def reject_unmatched_marker(open, close)
1057
+ return if marker_closed?(open[0], close)
1058
+
1059
+ raise Parser::ParseError,
1060
+ "The link #{open}#{close} opens with #{open[0]} " \
1061
+ "and does not close with it."
1062
+ end
1063
+
1064
+ # An opening `<` closes on a head and takes no second `<` start
1065
+ # marker on the closing half, but an `x` or `o` there draws
1066
+ # nothing and is dropped by `link_token` above; an opening `x` or
1067
+ # `o` takes none of its own kind again (`x-- t o--x` draws,
1068
+ # `x-- t x--x` does not). A thick closing half that opens with its
1069
+ # own `<` also needs a head: mmdc refuses `A == t <==x B`,
1070
+ # `A == t <=== B` and `A == t x==> B`, and draws the solid and
1071
+ # dotted forms.
1072
+ def marker_closed?(marker, close)
1073
+ return false if invalid_thick_closure?(close)
1074
+
1075
+ case marker
1076
+ when "x", "o" then close[-1] == marker && close[0] != marker
1077
+ when "<" then close[-1] == ">" && !close.start_with?("<")
1078
+ else true
1079
+ end
1080
+ end
1081
+ private :marker_closed?
1082
+
1083
+ def invalid_thick_closure?(close)
1084
+ close.match?(/\A[ox<]=/) && close[-1] != head_for(close[0])
1085
+ end
1086
+ private :invalid_thick_closure?
1087
+
1088
+ def head_for(start)
1089
+ start == "<" ? ">" : start
1090
+ end
1091
+ private :head_for
1092
+ private :reject_unmatched_marker
1093
+ private :link_token
1094
+
1095
+ # A second mention of a node changes only what it actually says.
1096
+ # Treating an absent shape as `rect` and an absent label as the id
1097
+ # meant `A(keep)` followed by `A@{ shape: rect }` kept the round
1098
+ # shape and lost the label — mermaid does the opposite of both.
1099
+ def add_or_update_node(diagram, node_data)
1100
+ return unless node_data
1101
+
1102
+ existing = diagram.find_node(node_data[:node_id])
1103
+ return diagram.nodes << create_node(node_data) unless existing
1104
+
1105
+ update_node(existing, node_data)
1106
+ end
1107
+
1108
+ def update_node(node, data)
1109
+ node.label = data[:label] if data[:label]
1110
+ node.shape = data[:shape_type] if data[:shape_type]
1111
+ node.classes = data[:classes] if data[:classes]
1112
+ end
1113
+ private :update_node
1114
+
1115
+ # Extract node data from a parse tree. Metadata wins over the bracket
1116
+ # form when both provide a shape or label, matching mermaid.
1117
+ def extract_node_data(node_hash)
1118
+ return nil unless node_hash
1119
+
1120
+ node_id = node_hash[:node_id].to_s
1121
+ shape_type, label = bracket_shape(node_hash[:shape])
1122
+
1123
+ entries = metadata_entries(node_hash[:metadata])
1124
+
1125
+ {
1126
+ node_id: node_id,
1127
+ shape_type: metadata_shape(entries["shape"]) || shape_type,
1128
+ label: entries["label"] || label,
1129
+ classes: node_hash[:inline_class]&.to_s,
1130
+ }
1131
+ end
1132
+
1133
+ # nil means the source said nothing, which is not the same as
1134
+ # saying `rect` or naming the node after itself. `create_node`
1135
+ # supplies the defaults, so only a real mention overwrites.
1136
+ def bracket_shape(shape_data)
1137
+ return [nil, nil] unless shape_data
1138
+
1139
+ delims = "#{shape_data[:open]}#{shape_data[:close]}"
1140
+ label = shape_data[:label]
1141
+ # `A[ ]` is an EMPTY label, not an absent one: mmdc draws a node
1142
+ # with nothing in it, and treating it as absent named the node
1143
+ # after itself and kept an older label on a re-mention.
1144
+ label = label.to_s.gsub(COMMENT_LINE, "") if delims == "()"
1145
+ [SHAPE_MAP[delims] || "rect", label&.to_s&.strip]
1146
+ end
1147
+ end
1148
+ end
1149
+ end
1150
+ end