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,842 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../layout/er_diagram"
5
+
6
+ module Etassia
7
+ module Renderer
8
+ # ER diagram renderer for converting graphs to SVG.
9
+ #
10
+ # Converts a laid-out graph structure (with computed positions) into
11
+ # SVG using the Svg builder classes. Handles ER entity boxes with
12
+ # attributes, PK/FK markers, and relationship lines with crow's foot
13
+ # cardinality notation.
14
+ #
15
+ # @example Render an ER diagram
16
+ # renderer = ErDiagram.new
17
+ # svg = renderer.render(laid_out_graph)
18
+ class ErDiagram < Base
19
+ # Renders final ER geometry to SVG. Hash input remains accepted as a
20
+ # compatibility boundary and is converted before rendering.
21
+ #
22
+ # @param scene [Layout::ErDiagram::Scene, Hash]
23
+ # @return [Svg::Document] the rendered SVG document
24
+ def render(scene)
25
+ return render_graph(scene) if scene.is_a?(Hash)
26
+
27
+ svg = create_document(scene)
28
+ render_relationships(scene, svg)
29
+ render_entities(scene, svg)
30
+
31
+ svg
32
+ end
33
+
34
+ protected
35
+
36
+ def render_graph(graph)
37
+ padding = Layout::ErDiagram.diagram_padding(graph) * 2
38
+ scene = Layout::ErDiagram::Scene.new(
39
+ width: calculate_width(graph) + padding,
40
+ height: calculate_height(graph) + padding,
41
+ )
42
+ svg = create_document(scene)
43
+ render_relationships(graph, svg) if graph[:edges]
44
+ render_entities(graph, svg) if graph[:children]
45
+ svg
46
+ end
47
+
48
+ def emit_entities(nodes, class_defs, svg)
49
+ nodes.each { |node| emit_entity(node, class_defs, svg) }
50
+ end
51
+
52
+ def emit_entity(node, class_defs, svg)
53
+ styles = entity_styles(node, class_defs)
54
+ group = Svg::Group.new.tap { |item| item.id = "entity-#{node.id}" }
55
+ group.children << entity_box(node, styles)
56
+ emit_entity_content(node, group, styles)
57
+ svg << group
58
+ end
59
+
60
+ def entity_box(node, styles)
61
+ attributed = node.attributes.any?
62
+ rect = Svg::Rect.new
63
+ apply_entity_geometry(rect, node)
64
+ apply_entity_styles(rect, styles, attributed)
65
+ rect
66
+ end
67
+
68
+ def apply_entity_geometry(rect, node)
69
+ rect.x = node.x
70
+ rect.y = node.y
71
+ rect.width = node.width
72
+ rect.height = node.height
73
+ end
74
+
75
+ def apply_entity_styles(rect, styles, attributed)
76
+ rect.fill = box_property(styles, "fill", attributed) ||
77
+ theme_color(:node_fill)
78
+ rect.stroke = entity_stroke(styles, attributed)
79
+ rect.stroke_width =
80
+ box_property(styles, "stroke-width", attributed) || "2"
81
+ end
82
+
83
+ def entity_stroke(styles, attributed)
84
+ box_property(styles, "stroke", attributed) ||
85
+ theme_color(:node_stroke)
86
+ end
87
+
88
+ def emit_entity_content(node, group, styles)
89
+ emit_entity_header(node, group, styles)
90
+ node.attributes.each do |attribute|
91
+ emit_attribute(attribute, styles, group)
92
+ end
93
+ end
94
+
95
+ def emit_entity_header(node, group, styles)
96
+ group.children << entity_header_text(node.labels.first, styles)
97
+ group.children << entity_separator(
98
+ node.separator, styles, node.attributes.any?
99
+ )
100
+ end
101
+
102
+ def entity_header_text(label, styles)
103
+ Svg::Text.new(
104
+ x: label.x,
105
+ y: label.y,
106
+ content: label.text,
107
+ fill: styles["color"] || theme_color(:label_text),
108
+ font_family: "Arial, sans-serif",
109
+ font_size: font_size_value(label.font_size),
110
+ text_anchor: "middle",
111
+ font_weight: "bold",
112
+ )
113
+ end
114
+
115
+ def entity_separator(geometry, styles, attributed)
116
+ Svg::Line.new.tap do |line|
117
+ line.x1 = geometry.x1
118
+ line.y1 = geometry.y1
119
+ line.x2 = geometry.x2
120
+ line.y2 = geometry.y2
121
+ line.stroke = entity_stroke(styles, attributed)
122
+ line.stroke_width = "1"
123
+ end
124
+ end
125
+
126
+ def emit_attribute(attribute, styles, group)
127
+ text = Svg::Text.new(
128
+ x: attribute.x,
129
+ y: attribute.y,
130
+ content: attribute.text,
131
+ fill: styles["color"] || theme_color(:label_text),
132
+ font_family: "monospace",
133
+ font_size: font_size_value(attribute.font_size),
134
+ )
135
+ group.children << text
136
+ end
137
+
138
+ # Every entity carries an implicit `default` class ahead of whatever
139
+ # it was explicitly assigned — verified against mermaid's own db,
140
+ # whose `cssClasses` opens with the literal string "default" for
141
+ # every entity, assigned or not (`"default"`, `"default a"`,
142
+ # `"default a b a"`). A `classDef default` is stored like any other
143
+ # name (grammar and process_class_def need no change for this); this
144
+ # is the one place it has to be applied even when nothing assigned it.
145
+ DEFAULT_CLASS = "default"
146
+ private_constant :DEFAULT_CLASS
147
+
148
+ # Resolves the style properties an entity's classes apply, by name and
149
+ # not by position — an index-keyed lookup would silently swap two
150
+ # entities' colours the moment their class lists diverge in length.
151
+ # Classes merge left to right: a later class overrides an earlier one
152
+ # only on a property both declare, and each contributes any property
153
+ # the other did not. NOT deduped upstream, so a repeated assignment
154
+ # moves that class to the end and lets it win a later conflict —
155
+ # verified against mermaid: `CAR:::a,b` then `CAR:::a` resolves
156
+ # `fill:red` (the trailing "a"), not `fill:blue`.
157
+ #
158
+ # @param node [Hash] the graph node, holding metadata[:classes]
159
+ # @param class_defs [Hash{String => String}] declared classDef styles
160
+ # @return [Hash{String => String}] resolved property => value
161
+ # Classes merge left to right into ONE Hash, not per-class then
162
+ # `Hash#merge!` — mermaid resolves every class's declarations into a
163
+ # single Map at the very end (`styles2Map` in its own bundle), so a
164
+ # chunk from an earlier class and a same-exact-case chunk from a later
165
+ # class must update the SAME Map slot in the SAME left-to-right pass.
166
+ # Plain assignment into one accumulator reproduces that: a repeated
167
+ # exact key updates its value in place (keeping its first position,
168
+ # same as `Map#set` on an existing key) and a new key is appended.
169
+ def entity_styles(node, class_defs)
170
+ classes = [DEFAULT_CLASS, *node.classes]
171
+
172
+ classes.each_with_object({}) do |class_name, styles|
173
+ declaration = class_defs.find { |item| item.name == class_name }
174
+ next unless declaration
175
+
176
+ apply_chunks(class_chunks(declaration.declaration), styles)
177
+ end
178
+ end
179
+
180
+ # Splits a `classDef` style run ("fill:#f96,stroke:#333") into its raw
181
+ # comma-separated chunks, then REPLAYS a copy of every chunk whose
182
+ # text contains the lowercase substring "color" — anywhere, key or
183
+ # value — onto the end of the list, EXCEPT one declaring `fill`.
184
+ #
185
+ # Verified against mermaid's own db (`ErDB#addClass`): each style
186
+ # chunk is tested with `/color/.exec(s)` (no `i` flag, so only a
187
+ # literal lowercase "color" substring matches) and, on a match, pushed
188
+ # a SECOND time into a separate `textStyles` array that gets appended
189
+ # after this class's own `styles` array when compiling. A declaration
190
+ # naming `stroke:currentcolor` this way ends up LAST among same-key
191
+ # entries even when a later, unrelated `stroke:red` chunk follows it
192
+ # in source — `classDef a stroke:currentcolor,stroke:blue`
193
+ # resolves to `currentcolor`, not `blue`, because the replay
194
+ # lands after both.
195
+ #
196
+ # `textStyles` is applied after this class's own styles, so a replayed
197
+ # chunk lands last among same-key entries. mermaid defuses that for
198
+ # `fill` by RENAMING it: the chunk pushed into `textStyles` becomes
199
+ # `bgFill:...`, which no longer collides with the box's `fill`.
200
+ #
201
+ # That rename is CASE-SENSITIVE, which is why the exemption below is
202
+ # too. Executing mermaid's own db API for these declarations returns
203
+ #
204
+ # textStyles: ["bgFill:currentcolor", "FILL:currentcolor",
205
+ # "stroke:currentcolor"]
206
+ #
207
+ # — lowercase `fill` rewritten, `FILL` left alone to collide as before.
208
+ # Folding the key here would exempt `FILL` as well and change a
209
+ # behaviour mermaid does not change.
210
+ #
211
+ # Measured with mmdc on an erDiagram entity carrying an attribute
212
+ # block, one class assigned:
213
+ #
214
+ # stroke:currentcolor,stroke:blue -> currentcolor
215
+ # fill:currentcolor,fill:blue -> blue
216
+ # stroke:green,stroke:blue -> blue
217
+ # fill:green,fill:blue -> blue
218
+ #
219
+ # Row one is why the replay exists. Row two is why lowercase `fill`
220
+ # is exempt: replaying it reverses that row to `currentcolor`.
221
+ #
222
+ # @param declaration [String] raw style text for one class
223
+ # @return [Array<String>] chunks, with colour-bearing ones repeated
224
+ def class_chunks(declaration)
225
+ chunks = declaration.split(",")
226
+ chunks + chunks.select { |chunk| replayed_chunk?(chunk) }
227
+ end
228
+
229
+ # Whether CHUNK is one whose `textStyles` replay is still visible on
230
+ # the entity BOX. See `class_chunks` for the measurement behind the
231
+ # `fill` exemption.
232
+ #
233
+ # The key is compared EXACTLY, not folded. mermaid's rename of `fill`
234
+ # to `bgFill` is case-sensitive, so `FILL:` is still replayed and
235
+ # still collides. Folding here would exempt it too, and change a
236
+ # behaviour this commit has no business changing.
237
+ #
238
+ # @param chunk [String] one raw comma-separated chunk
239
+ # @return [Boolean]
240
+ def replayed_chunk?(chunk)
241
+ return false unless chunk.include?("color")
242
+
243
+ key, value = mermaid_split(chunk)
244
+ !value.nil? && key.strip != "fill"
245
+ end
246
+
247
+ # Applies a run of raw chunks into the shared, exact-case styles Hash,
248
+ # left to right. A chunk with no colon is skipped rather than raising
249
+ # — `classDef x foo` and `classDef x font-family:Arial,sans-serif`
250
+ # both parse under mermaid, and the second is exactly this shape after
251
+ # the comma split, so failing here would crash a render on
252
+ # mermaid-valid input.
253
+ #
254
+ # Neither side is folded. Verified against mermaid's own db: it stores
255
+ # a style declaration VERBATIM, case untouched ("FILL:red" stays
256
+ # "FILL:red"). Case-insensitive matching is the BROWSER's doing, on
257
+ # the finished CSS text, not mermaid's — so folding here, before that
258
+ # point, changes results mermaid never produces:
259
+ #
260
+ # - `fill:red,FILL:blue,fill:green` must compute blue in Chrome, not
261
+ # green. Downcasing collapses all three into one Ruby key up front,
262
+ # which loses the exact-case Hash entries mermaid's own Map keeps
263
+ # ("fill" and "FILL" are different keys to it too) and makes the
264
+ # LAST literal chunk win regardless of case — wrong order. Plain
265
+ # assignment into one accumulator, above, already reproduces the
266
+ # ordering a case-preserving Map gives mermaid; box_style below is
267
+ # what does the browser's case-insensitive resolution, once, at
268
+ # lookup.
269
+ # - `COLOR:red` (uppercase) must be a BOX style, not a label one —
270
+ # mermaid's own isLabelStyle check (`key === "color"`,
271
+ # handDrawnShapeStyles.ts) is exact-case, so `COLOR` fails it.
272
+ # Downcasing here would fold it into the same key as a real
273
+ # lowercase `color:`, and a caller reading text color
274
+ # (`styles['color']`) would pick it up and colour the wrong
275
+ # element.
276
+ #
277
+ # @param chunks [Array<String>] raw "key:value" chunks, in order
278
+ # @param styles [Hash{String => String}] accumulator, mutated in place
279
+ # @return [Hash{String => String}] the same accumulator, for chaining
280
+ def apply_chunks(chunks, styles)
281
+ chunks.each do |chunk|
282
+ key, value = mermaid_split(chunk)
283
+ next unless value
284
+
285
+ styles[key.strip] = value.strip
286
+ end
287
+ end
288
+
289
+ # Splits one "key:value" chunk the way mermaid's own `styles2Map`
290
+ # does: `style.split(":")` with NO limit, destructured to only the
291
+ # first two elements. A third colon-separated segment is silently
292
+ # DROPPED, not folded into the value — `fill:red:blue` resolves to
293
+ # `red`, and Chrome computes red from mermaid's own output for that
294
+ # source. `String#split(':', 2)` (the previous behaviour here) instead
295
+ # keeps everything after the first colon, giving `red:blue`, which
296
+ # mermaid never produces.
297
+ #
298
+ # `-1` is required to match JavaScript's `split`: Ruby's default
299
+ # `split` drops a TRAILING empty field, so `"fill:".split(':')` gives
300
+ # `["fill"]` and a value-clearing declaration reads as "no colon" and
301
+ # is skipped, leaving a stale earlier `fill:red` in place. JavaScript
302
+ # (and this, with `-1`) both give `["fill", ""]` instead — `present`
303
+ # below is what then treats that resolved empty value as "never
304
+ # validly declared" once a caller reads it back, matching a browser
305
+ # dropping an empty CSS declaration and mermaid's own JS-falsy read.
306
+ #
307
+ # @param chunk [String] one raw "key:value(:ignored)" chunk
308
+ # @return [Array(String, String), Array(String, nil), Array()]
309
+ # [key, value] — value is nil when the chunk has no colon, and both
310
+ # are nil (empty array) for an empty chunk
311
+ def mermaid_split(chunk)
312
+ parts = chunk.split(":", -1)
313
+ [parts[0], parts[1]]
314
+ end
315
+
316
+ # An empty string is a validly-parsed but empty CSS value — mermaid's
317
+ # own override read is JS-falsy on it, and the browser drops an empty
318
+ # declaration (`fill:`) during cascade — so both treat it as though
319
+ # the property was never declared. Applied to the exact-key
320
+ # (attributed) read below; the case-insensitive `box_style` path
321
+ # below reaches the same result through `css_valid?` instead, since
322
+ # an empty string is not valid CSS for any property this file models.
323
+ #
324
+ # @param value [String, nil]
325
+ # @return [String, nil] VALUE, or nil if it is nil or empty
326
+ def present(value)
327
+ value.nil? || value.empty? ? nil : value
328
+ end
329
+
330
+ # Picks the entry nearest the end of VALUES whose value passes the
331
+ # block, treating an explicitly-cleared empty value (see `present`)
332
+ # as absent rather than invalid. When nothing validates, falls back
333
+ # to the raw last-declared value instead of nil — a hostile or
334
+ # malformed value must still reach the rendered SVG verbatim
335
+ # (escaped by the XML serializer, not by us silently discarding it
336
+ # for our own default), which is exactly the guarantee the D2
337
+ # integration spec pins. Only used where that guarantee applies —
338
+ # see `ambient_color` below for the one caller that must NOT fall
339
+ # back this way.
340
+ #
341
+ # @param values [Array<String, nil>] raw candidate values, in
342
+ # source order
343
+ # @yield [String] validity predicate
344
+ # @return [String, nil] the winning value, or nil if VALUES held
345
+ # nothing but absent (nil or empty) entries
346
+ def last_valid_or_raw(values, &)
347
+ present_values = values.filter_map { |value| present(value) }
348
+ return nil if present_values.empty?
349
+
350
+ present_values.reverse_each.find(&) || present_values.last
351
+ end
352
+
353
+ # Case-insensitive box lookup for a BARE entity, modeling the
354
+ # browser's cascade on the box's `style=` attribute: among every
355
+ # entry whose key matches PROPERTY case-insensitively, in source
356
+ # order, the value nearest the end wins — same as the last matching
357
+ # CSS declaration in a rendered `style` attribute — but an entry
358
+ # whose value the browser's CSS parser would reject is skipped,
359
+ # exactly as an invalid declaration never reaches the cascade at
360
+ # all. Do not use this for `color` — see `apply_chunks`'s second
361
+ # bullet.
362
+ #
363
+ # @param styles [Hash{String => String}] resolved, exact-case entity
364
+ # styles
365
+ # @param property [String] lowercase property name to resolve
366
+ # @return [String, nil] the winning value, or nil if PROPERTY was
367
+ # never declared under any case
368
+ def box_style(styles, property)
369
+ values = styles.select { |key, _| key.downcase == property }.values
370
+ last_valid_or_raw(values) { |value| css_valid?(property, value) }
371
+ end
372
+
373
+ # `currentColor` on a bare entity's box resolves, in a real browser,
374
+ # against that SAME element's `color` CSS property — which mermaid
375
+ # routes onto the box's `style=` attribute under any casing EXCEPT a
376
+ # literal lowercase `color` key (that one is a label style, see the
377
+ # exact-case `styles['color']` reads above, and never reaches the
378
+ # box's style attribute at all). Substitute the concrete value now,
379
+ # since a standalone SVG has no browser cascade left to resolve it
380
+ # at paint time.
381
+ #
382
+ # @param value [String, nil] PROPERTY's already-resolved raw value
383
+ # @param styles [Hash{String => String}] resolved, exact-case entity
384
+ # styles
385
+ # @return [String, nil] VALUE, or the ambient colour declared for
386
+ # this box if VALUE is `currentColor` (any case) and one exists
387
+ def resolve_current_color(value, styles)
388
+ return value unless value&.downcase == "currentcolor"
389
+
390
+ ambient_color(styles) || value
391
+ end
392
+
393
+ # The ambient colour a bare entity's box would compute `color` as:
394
+ # every key matching `color` case-insensitively EXCEPT the literal
395
+ # lowercase key, which is the label style excluded from the box's
396
+ # own `style=` attribute — see `resolve_current_color` above. Unlike
397
+ # `box_style`, an invalid ambient declaration is simply absent
398
+ # rather than falling back to its raw text — `resolve_current_color`
399
+ # already has a safe literal default (leave `currentColor` as
400
+ # written, which a standalone browser resolves to black, matching
401
+ # mermaid with no ambient colour set), so there is no verbatim
402
+ # guarantee to uphold here the way there is for the fill/stroke
403
+ # attribute itself.
404
+ #
405
+ # @param styles [Hash{String => String}] resolved, exact-case entity
406
+ # styles
407
+ # @return [String, nil] the winning ambient colour, or nil if none
408
+ # was validly declared
409
+ def ambient_color(styles)
410
+ values = styles.select do |key, _value|
411
+ key != "color" && key.downcase == "color"
412
+ end.values
413
+ values.filter_map { |value| present(value) }
414
+ .reverse_each.find { |value| css_color?(value) }
415
+ end
416
+
417
+ # Colour-valued box properties resolve `currentColor` against the
418
+ # box's ambient colour (see `resolve_current_color`); length-valued
419
+ # ones do not.
420
+ COLOR_PROPERTIES = %w[fill stroke].freeze
421
+ private_constant :COLOR_PROPERTIES
422
+
423
+ # Whether VALUE is syntactically valid CSS for PROPERTY. A small
424
+ # table, not a CSS grammar — enough to tell a real declaration from
425
+ # `FILL:bogus`, not to validate every legal `rgb()` argument.
426
+ # `box_property` only asks about `fill`, `stroke` and `stroke-width`.
427
+ #
428
+ # @param property [String] "fill", "stroke" or "stroke-width"
429
+ # @param value [String]
430
+ # @return [Boolean]
431
+ def css_valid?(property, value)
432
+ return css_length?(value) if property == "stroke-width"
433
+
434
+ css_color?(value)
435
+ end
436
+
437
+ CSS_HEX_COLOR = /\A#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\z/i
438
+ private_constant :CSS_HEX_COLOR
439
+
440
+ CSS_COLOR_FUNCTION = /\A(?:rgb|rgba|hsl|hsla)\(.+\)\z/i
441
+ private_constant :CSS_COLOR_FUNCTION
442
+
443
+ CSS_LENGTH = %r{
444
+ \A\d+(?:\.\d+)?(?:px|em|rem|%|pt|cm|mm|in|pc|ex|ch|vw|vh)?\z
445
+ }ix
446
+ private_constant :CSS_LENGTH
447
+
448
+ # The CSS Color Module Level 4 extended keyword set, plus `none` (a
449
+ # valid `fill`/`stroke` paint keyword, not a general colour) and the
450
+ # two keywords that resolve dynamically (`currentcolor`) or to
451
+ # nothing (`transparent`). Not exhaustive of every legal CSS
452
+ # <color> (system colours, `color(...)`, `lab()` and friends are
453
+ # not corpus-observed here) — sufficient to separate a real
454
+ # declaration from `bogus`.
455
+ CSS_COLOR_KEYWORDS = %w[
456
+ aliceblue antiquewhite aqua aquamarine azure beige bisque black
457
+ blanchedalmond blue blueviolet brown burlywood cadetblue
458
+ chartreuse chocolate coral cornflowerblue cornsilk crimson cyan
459
+ currentcolor darkblue darkcyan darkgoldenrod darkgray darkgreen
460
+ darkgrey darkkhaki darkmagenta darkolivegreen darkorange
461
+ darkorchid darkred darksalmon darkseagreen darkslateblue
462
+ darkslategray darkslategrey darkturquoise darkviolet deeppink
463
+ deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite
464
+ forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray
465
+ green greenyellow grey honeydew hotpink indianred indigo ivory
466
+ khaki lavender lavenderblush lawngreen lemonchiffon lightblue
467
+ lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen
468
+ lightgrey lightpink lightsalmon lightseagreen lightskyblue
469
+ lightslategray lightslategrey lightsteelblue lightyellow lime
470
+ limegreen linen magenta maroon mediumaquamarine mediumblue
471
+ mediumorchid mediumpurple mediumseagreen mediumslateblue
472
+ mediumspringgreen mediumturquoise mediumvioletred midnightblue
473
+ mintcream mistyrose moccasin navajowhite navy none oldlace olive
474
+ olivedrab orange orangered orchid palegoldenrod palegreen
475
+ paleturquoise palevioletred papayawhip peachpuff peru pink plum
476
+ powderblue purple rebeccapurple red rosybrown royalblue
477
+ saddlebrown salmon sandybrown seagreen seashell sienna silver
478
+ skyblue slateblue slategray slategrey snow springgreen steelblue
479
+ tan teal thistle tomato transparent turquoise violet wheat white
480
+ whitesmoke yellow yellowgreen
481
+ ].freeze
482
+ private_constant :CSS_COLOR_KEYWORDS
483
+
484
+ # @param value [String]
485
+ # @return [Boolean] whether VALUE is a CSS colour (or the `none`
486
+ # paint keyword) mermaid's browser cascade would accept, rather
487
+ # than dropping the declaration
488
+ def css_color?(value)
489
+ downcased = value.downcase
490
+ CSS_COLOR_KEYWORDS.include?(downcased) ||
491
+ CSS_HEX_COLOR.match?(value) ||
492
+ CSS_COLOR_FUNCTION.match?(value)
493
+ end
494
+
495
+ # @param value [String]
496
+ # @return [Boolean] whether VALUE is a CSS <length> `stroke-width`
497
+ # would accept
498
+ def css_length?(value)
499
+ CSS_LENGTH.match?(value)
500
+ end
501
+
502
+ # An attributed entity's outer box does NOT go through the browser's
503
+ # CSS cascade the way a bare entity's does — mermaid draws it via a
504
+ # different, row-based shape whose fill, stroke, AND stroke-width
505
+ # each come from a direct, EXACT-key Map read (`stylesMap.get(...)`,
506
+ # mermaid's own `userNodeOverrides`) rather than an inline
507
+ # `style="..."` attribute the browser resolves case-insensitively.
508
+ # Verified by Codex against the installed mermaid 11.16.1 bundle and
509
+ # a real Chrome computation, for all three properties:
510
+ # `classDef a fill:red,FILL:blue,fill:green` on a bare CAR computes
511
+ # blue (what `box_style` gives), but on `CAR:::a { string make }` it
512
+ # computes green — the last literal lowercase `fill`, ignoring
513
+ # `FILL` entirely. `stroke:red,STROKE:blue,stroke:green,
514
+ # stroke-width:2px,STROKE-WIDTH:8px,stroke-width:4px` computes
515
+ # green/4px attributed, blue/8px bare — the same split, on both
516
+ # properties.
517
+ #
518
+ # `currentColor` is never resolved against an ambient colour on this
519
+ # path — an attributed box carries no `style=` attribute for the
520
+ # browser to cascade a `color` property through, so it has no
521
+ # ambient colour to read under any case, and the exact-key value is
522
+ # used verbatim.
523
+ #
524
+ # @param styles [Hash{String => String}] resolved, exact-case entity
525
+ # styles
526
+ # @param property [String] lowercase property name to resolve
527
+ # @param attributed [Boolean] whether this entity has any attributes
528
+ # @return [String, nil] the winning value, or nil if never validly
529
+ # declared under the case this path reads
530
+ def box_property(styles, property, attributed)
531
+ if attributed
532
+ present(styles[property])
533
+ else
534
+ value = box_style(styles, property)
535
+ if COLOR_PROPERTIES.include?(property)
536
+ resolve_current_color(value, styles)
537
+ else
538
+ value
539
+ end
540
+ end
541
+ end
542
+
543
+ def emit_relationships(edges, svg)
544
+ edges.each { |edge| emit_relationship(edge, svg) }
545
+ end
546
+
547
+ def emit_relationship(edge, svg)
548
+ group = Svg::Group.new.tap { |item| item.id = "rel-#{edge.id}" }
549
+ group.children.concat(relationship_shapes(edge))
550
+ emit_marker(edge.source_marker, group)
551
+ emit_marker(edge.target_marker, group)
552
+ emit_relationship_label(edge, group)
553
+ svg << group
554
+ end
555
+
556
+ def relationship_shapes(edge)
557
+ edge.sections.map do |section|
558
+ relationship_shape(section, edge.relationship_type)
559
+ end
560
+ end
561
+
562
+ def emit_relationship_label(edge, group)
563
+ return if edge.labels.empty?
564
+
565
+ group.children << relationship_text(edge.labels.first)
566
+ end
567
+
568
+ def relationship_shape(section, relationship_type)
569
+ return relationship_line(section, relationship_type) if
570
+ section.bend_points.empty?
571
+
572
+ relationship_path(section, relationship_type)
573
+ end
574
+
575
+ def relationship_line(section, relationship_type)
576
+ Svg::Line.new.tap do |line|
577
+ apply_line_geometry(line, section)
578
+ relationship_stroke(line, relationship_type)
579
+ end
580
+ end
581
+
582
+ def apply_line_geometry(line, section)
583
+ line.x1 = section.start_point.x
584
+ line.y1 = section.start_point.y
585
+ line.x2 = section.end_point.x
586
+ line.y2 = section.end_point.y
587
+ end
588
+
589
+ def relationship_path(section, relationship_type)
590
+ Svg::Path.new.tap do |path|
591
+ path.d = relationship_path_data(section)
592
+ path.fill = "none"
593
+ relationship_stroke(path, relationship_type)
594
+ end
595
+ end
596
+
597
+ def relationship_path_data(section)
598
+ points = [section.start_point, *section.bend_points, section.end_point]
599
+ first = points.shift
600
+ (["M #{point_pair(first)}"] +
601
+ points.map { |point| "L #{point_pair(point)}" }).join(" ")
602
+ end
603
+
604
+ def point_pair(point)
605
+ "#{point.x} #{point.y}"
606
+ end
607
+
608
+ def relationship_stroke(shape, relationship_type)
609
+ shape.stroke = theme_color(:edge_stroke)
610
+ shape.stroke_width = "2"
611
+ if relationship_type == "non-identifying"
612
+ shape.stroke_dasharray = "5,5"
613
+ end
614
+ end
615
+
616
+ def emit_marker(marker, group)
617
+ circles = marker.circles.map { |geometry| marker_circle(geometry) }
618
+ lines = marker.lines.map { |geometry| marker_line(geometry) }
619
+ children = marker.circle_first ? circles + lines : lines + circles
620
+ group.children.concat(children)
621
+ end
622
+
623
+ def marker_line(geometry)
624
+ Svg::Line.new.tap do |line|
625
+ line.x1 = geometry.x1
626
+ line.y1 = geometry.y1
627
+ line.x2 = geometry.x2
628
+ line.y2 = geometry.y2
629
+ line.stroke = theme_color(:edge_stroke)
630
+ line.stroke_width = "2"
631
+ end
632
+ end
633
+
634
+ def marker_circle(geometry)
635
+ Svg::Circle.new.tap do |circle|
636
+ circle.cx = geometry.cx
637
+ circle.cy = geometry.cy
638
+ circle.r = geometry.radius
639
+ circle.fill = "none"
640
+ circle.stroke = theme_color(:edge_stroke)
641
+ circle.stroke_width = "2"
642
+ end
643
+ end
644
+
645
+ def relationship_text(label)
646
+ Svg::Text.new(
647
+ x: label.x,
648
+ y: label.y,
649
+ content: label.text,
650
+ fill: theme_color(:label_text),
651
+ font_family: "Arial, sans-serif",
652
+ font_size: font_size_value(label.font_size),
653
+ text_anchor: "middle",
654
+ )
655
+ end
656
+
657
+ def font_size_value(font_size)
658
+ font_size.to_i == font_size ? font_size.to_i.to_s : font_size.to_s
659
+ end
660
+
661
+ # Released protected hooks. Hash compatibility delegates geometry
662
+ # recovery to Layout; typed scenes already carry final geometry.
663
+ def calculate_width(graph)
664
+ Layout::ErDiagram.content_width(graph)
665
+ end
666
+
667
+ def calculate_height(graph)
668
+ Layout::ErDiagram.content_height(graph)
669
+ end
670
+
671
+ def render_entities(graph, svg)
672
+ if graph.is_a?(Layout::ErDiagram::Scene)
673
+ return emit_entities(graph.children, graph.class_defs, svg)
674
+ end
675
+
676
+ render_graph_entities(graph, svg)
677
+ end
678
+
679
+ def render_graph_entities(graph, svg)
680
+ previous = @compatibility_class_defs
681
+ @compatibility_class_defs = Layout::ErDiagram.from_graph(
682
+ graph, theme: theme
683
+ ).class_defs
684
+ graph[:children].each { |node| render_entity(node, svg) }
685
+ ensure
686
+ @compatibility_class_defs = previous
687
+ end
688
+
689
+ def render_entity(node, svg)
690
+ entity = Layout::ErDiagram.entity_from_hash(node, theme: theme)
691
+ styles = entity_styles(entity, @compatibility_class_defs || [])
692
+ group = Svg::Group.new.tap { |item| item.id = "entity-#{entity.id}" }
693
+ group.children << entity_box(entity, styles)
694
+ with_compatibility_styles(styles) do
695
+ render_entity_content(node, node[:metadata] || {}, group)
696
+ end
697
+ svg << group
698
+ end
699
+
700
+ def with_compatibility_styles(styles)
701
+ previous = @compatibility_styles
702
+ @compatibility_styles = styles
703
+ yield
704
+ ensure
705
+ @compatibility_styles = previous
706
+ end
707
+
708
+ def render_entity_content(node, metadata, group)
709
+ entity = Layout::ErDiagram.entity_from_hash(
710
+ node.merge(metadata: metadata), theme: theme
711
+ )
712
+ emit_entity_header(entity, group, @compatibility_styles || {})
713
+ (metadata[:attributes] || []).zip(entity.attributes) do |attribute, row|
714
+ render_attribute(
715
+ entity.x, row.y - row.font_size, entity.width, attribute, group
716
+ )
717
+ end
718
+ end
719
+
720
+ def render_attribute(x_pos, y_pos, _width, attribute, group)
721
+ row = Layout::ErDiagram.attribute_row(
722
+ x_pos, y_pos, attribute, theme: theme
723
+ )
724
+ emit_attribute(row, @compatibility_styles || {}, group)
725
+ y_pos + Layout::ErDiagram::TEXT_LINE_HEIGHT
726
+ end
727
+
728
+ def render_relationships(graph, svg)
729
+ if graph.is_a?(Layout::ErDiagram::Scene)
730
+ return emit_relationships(graph.edges, svg)
731
+ end
732
+
733
+ graph[:edges].each do |edge|
734
+ render_relationship(edge, graph, svg)
735
+ end
736
+ end
737
+
738
+ def render_relationship(edge, graph, svg)
739
+ endpoints = relationship_endpoints(edge, graph)
740
+ return unless endpoints
741
+
742
+ svg << compatibility_relationship_group(edge, *endpoints)
743
+ end
744
+
745
+ def relationship_endpoints(edge, graph)
746
+ source = find_node(graph, edge[:sources]&.first)
747
+ target = find_node(graph, edge[:targets]&.first)
748
+ [source, target] if source && target
749
+ end
750
+
751
+ def compatibility_relationship_group(edge, source, target)
752
+ metadata = edge[:metadata] || {}
753
+ from = calculate_connection_point(source, target)
754
+ to = calculate_connection_point(target, source)
755
+ group = Svg::Group.new.tap { |item| item.id = "rel-#{edge[:id]}" }
756
+ render_relationship_line(
757
+ from, to, metadata[:relationship_type] || "non-identifying", group
758
+ )
759
+ render_compatibility_cardinalities(metadata, from, to, group)
760
+ render_relationship_label(edge, from, to, group)
761
+ group
762
+ end
763
+
764
+ def render_compatibility_cardinalities(metadata, from, to, group)
765
+ if metadata[:cardinality_from]
766
+ render_cardinality(
767
+ from, to, metadata[:cardinality_from], :source, group
768
+ )
769
+ end
770
+ if metadata[:cardinality_to]
771
+ render_cardinality(
772
+ to, from, metadata[:cardinality_to], :target, group
773
+ )
774
+ end
775
+ end
776
+
777
+ def find_node(graph, node_id)
778
+ Layout::ErDiagram.find_node(graph, node_id)
779
+ end
780
+
781
+ def calculate_connection_point(from_node, to_node)
782
+ point = Layout::ErDiagram.connection_point(from_node, to_node)
783
+ { x: point.x, y: point.y }
784
+ end
785
+
786
+ def render_relationship_line(from, to, rel_type, group)
787
+ section = Layout::ErDiagram.section(from, to)
788
+ group.children << relationship_line(section, rel_type)
789
+ end
790
+
791
+ def render_cardinality(point, opposite_point, cardinality, _side, group)
792
+ case cardinality
793
+ when "one"
794
+ render_one_marker(point, opposite_point, group)
795
+ when "zero_or_more"
796
+ render_zero_or_more_marker(point, opposite_point, group)
797
+ when "one_or_more"
798
+ render_one_or_more_marker(point, opposite_point, group)
799
+ when "zero_or_one"
800
+ render_zero_or_one_marker(point, opposite_point, group)
801
+ end
802
+ end
803
+
804
+ def render_one_marker(point, opposite_point, group)
805
+ marker = Layout::ErDiagram.one_marker(point, opposite_point)
806
+ emit_marker(marker, group)
807
+ end
808
+
809
+ def render_zero_or_more_marker(point, opposite_point, group)
810
+ render_circle_marker(point, opposite_point, group)
811
+ render_crows_foot(point, opposite_point, group)
812
+ end
813
+
814
+ def render_one_or_more_marker(point, opposite_point, group)
815
+ render_one_marker(point, opposite_point, group)
816
+ render_crows_foot(point, opposite_point, group)
817
+ end
818
+
819
+ def render_zero_or_one_marker(point, opposite_point, group)
820
+ render_circle_marker(point, opposite_point, group)
821
+ render_one_marker(point, opposite_point, group)
822
+ end
823
+
824
+ def render_circle_marker(point, opposite_point, group)
825
+ marker = Layout::ErDiagram.circle_marker(point, opposite_point)
826
+ emit_marker(marker, group)
827
+ end
828
+
829
+ def render_crows_foot(point, opposite_point, group)
830
+ marker = Layout::ErDiagram.crows_marker(point, opposite_point)
831
+ emit_marker(marker, group)
832
+ end
833
+
834
+ def render_relationship_label(edge, from, to, group)
835
+ label = Layout::ErDiagram.edge_label(
836
+ edge, from, to, theme: theme
837
+ )
838
+ group.children << relationship_text(label) if label
839
+ end
840
+ end
841
+ end
842
+ end