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,1722 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "common"
4
+ require_relative "mermaid_unicode_text"
5
+
6
+ module Etassia
7
+ module Parser
8
+ module Grammars
9
+ # Parslet grammar for Flowchart diagrams.
10
+ #
11
+ # Handles flowchart syntax including nodes with various shapes,
12
+ # edges with labels, edge chaining, subgraphs, and styling directives.
13
+ class Flowchart < Common
14
+ # Mermaid lexes these as keywords, so `end.` and `1end` are not
15
+ # node ids even though the characters are legal.
16
+ #
17
+ # `direction`, `accTitle`, `accDescr`, `default` and `callback` are
18
+ # NOT among them — mmdc draws `default-->Z`. `href`, `call` and
19
+ # `click` are keywords too but end a word differently, so they live
20
+ # in `spaced_keyword` instead. `direction TB` on its own line is
21
+ # still not parsed here, though mmdc draws it.
22
+ #
23
+ # Longest first, because Parslet does not backtrack into an
24
+ # alternative that already matched: `class` ahead of `classDef`
25
+ # would take the first five characters and then fail the boundary.
26
+ RESERVED_WORDS = %w[
27
+ swimlane-beta interpolate flowchart linkStyle subgraph classDef
28
+ _parent _blank _self graph style class _top end
29
+ ].freeze
30
+
31
+ # The words reserved in a subgraph id, which is NOT the node list.
32
+ # A statement keyword is plain text here (`subgraph graph [T]`
33
+ # renders), so those drop out. `default` is reserved here and
34
+ # nowhere else: `subgraph default [T]` does not render while
35
+ # `default-->Z` does.
36
+ SUBGRAPH_RESERVED = %w[
37
+ interpolate _parent default _blank _self _top
38
+ ].freeze
39
+ private_constant :RESERVED_WORDS, :SUBGRAPH_RESERVED
40
+
41
+ # Mermaid restarts its lexer at certain characters and looks for
42
+ # the target again behind each restart. Several rules walk that
43
+ # path, so they share it here.
44
+ #
45
+ # A `repeat` rather than a recursive rule: a recursive one blew the
46
+ # Ruby stack on a 2000-character id that mmdc draws.
47
+ def token_at_lexer_restart(target)
48
+ (target.absent? >> restart_step).repeat >> target
49
+ end
50
+ private :token_at_lexer_restart
51
+
52
+ root(:diagram)
53
+
54
+ # Main diagram structure
55
+ rule(:diagram) do
56
+ ws? >>
57
+ header >>
58
+ ws? >>
59
+ statements.maybe >>
60
+ ws?
61
+ end
62
+
63
+ # The direction shares the keyword's line, so `graph` newline `TD`
64
+ # is a diagram with a node called TD. Nothing else may follow the
65
+ # keyword: mmdc refuses `graph X;A`, `graph ;A` and `graph TDx`.
66
+ # A word direction needs a gap; a glyph one does not. mmdc renders
67
+ # `graph<` and `graph >` alike, and refuses `graphTD`.
68
+ #
69
+ # The two branches exist because a separator only closes the header
70
+ # when a direction came first — `graph TD;A` renders and `graph;A`
71
+ # does not. A lookahead cannot tell them apart: by the time the
72
+ # header ends, the direction has already been consumed.
73
+ # `flowchart-elk` only selects mmdc's ELK renderer -- the diagram
74
+ # body is ordinary flowchart syntax, and etassia has no ELK layout
75
+ # of its own yet (see the TODO on `Layout::Grid`), so it falls
76
+ # back to the same grid every other flowchart uses. `-elk` is
77
+ # matched here, in the grammar, rather than rewritten out of the
78
+ # source before parsing: a source rewrite shifts every column
79
+ # after the keyword, so a parse error on a `flowchart-elk`
80
+ # diagram would quote the wrong text and point at the wrong
81
+ # column. `flowchart-elk` must be tried before the plain
82
+ # `flowchart` alternative, or the bare keyword would match first
83
+ # and leave `-elk` to fail against the direction.
84
+ rule(:header) do
85
+ (
86
+ str("flowchart-elk") | str("flowchart") | str("graph")
87
+ ).as(:header) >>
88
+ (directed_header | undirected_header)
89
+ end
90
+
91
+ rule(:directed_header) do
92
+ header_direction.as(:direction) >>
93
+ (separator | (space? >> (newline | eof)))
94
+ end
95
+
96
+ rule(:undirected_header) do
97
+ direction.absent?.as(:direction) >> space? >> (newline | eof)
98
+ end
99
+
100
+ rule(:header_direction) do
101
+ (space.repeat(1) >> direction) |
102
+ glyph_direction.as(:dir_value)
103
+ end
104
+
105
+ # A `;` between statements on one line. `line_end` is deliberately
106
+ # left alone: `style_property` and `click_action` scan a value up to
107
+ # the physical end of the line via `line_end.absent?`, so widening it
108
+ # would truncate `style A fill:#f9f;stroke:#333` at the semicolon.
109
+ #
110
+ # Repeat only this rule, never `statement_end` — the `line_end` arm
111
+ # succeeds zero-width at EOF, so repeating that would not terminate.
112
+ # A comment cannot open on the same line as the separator: mmdc
113
+ # rejects `graph TD;%% c`, while `;` then a newline then `%% c` is
114
+ # ordinary.
115
+ rule(:separator) do
116
+ (semicolon >> space?).repeat(1) >> str("%%").absent?
117
+ end
118
+
119
+ # Node statements tolerate a space before the separator
120
+ # (`graph TD;A ;`). The header and a class assignment do not: mmdc
121
+ # rejects `graph TD ;A` and `class A foo ;B`.
122
+ rule(:loose_separator) { space? >> separator }
123
+
124
+ rule(:statement_end) { separator | line_end }
125
+ rule(:loose_statement_end) { loose_separator | line_end }
126
+
127
+ # mermaid's full set, aliases included: `BR` and `v` are another
128
+ # down, and three arrow glyphs stand in for the words. `v` is a
129
+ # word here, not a glyph — it needs the same gap `TD` does, while
130
+ # `<`, `>` and `^` do not.
131
+ rule(:direction) { (word_direction | glyph_direction).as(:dir_value) }
132
+
133
+ rule(:word_direction) do
134
+ str("TD") | str("TB") | str("BT") | str("BR") | str("LR") |
135
+ str("RL") | str("v")
136
+ end
137
+
138
+ # These three need no gap after the keyword — mmdc draws `graph<`.
139
+ rule(:glyph_direction) { match["<>^"] }
140
+
141
+ # `direction LR` inside a subgraph turns that box's contents.
142
+ # mmdc 11.12.0 accepts a top-level one too but does not honour it,
143
+ # so it is a statement anywhere and the transform decides whether
144
+ # anything encloses it.
145
+ # `statement_end`, not `line_end`: a semicolon separates this from
146
+ # the next statement the way it separates every other one. mmdc
147
+ # 11.12.0 renders `direction LR;A`, and `line_end` takes the
148
+ # semicolon only when a newline follows it.
149
+ rule(:direction_statement) do
150
+ str("direction").as(:direction_keyword) >> space >>
151
+ statement_direction >> statement_end
152
+ end
153
+
154
+ # The five plain words only, NOT the header's set. mmdc takes
155
+ # `graph <`, `graph v` and `graph BR`, and refuses every one of
156
+ # them after `direction` — measured on 11.12.0. Reusing the
157
+ # header rule here accepted three sources mermaid will not draw.
158
+ rule(:statement_direction) do
159
+ (str("TD") | str("TB") | str("BT") | str("LR") |
160
+ str("RL")).as(:dir_value)
161
+ end
162
+
163
+ rule(:statements) do
164
+ ((separator | statement) >> ws?).repeat(1)
165
+ end
166
+
167
+ rule(:statement) do
168
+ accessibility_statement |
169
+ direction_statement |
170
+ subgraph_statement |
171
+ style_statement |
172
+ class_def_statement |
173
+ class_assignment_statement |
174
+ link_style_statement |
175
+ click_statement |
176
+ node_edge_statement |
177
+ standalone_node
178
+ end
179
+
180
+ # `accTitle:` and `accDescr:` carry the text mermaid puts in the
181
+ # SVG's aria attributes.
182
+ rule(:accessibility_statement) do
183
+ acc_descr_block | acc_line
184
+ end
185
+
186
+ # Mermaid separates the keyword from its delimiter with \s*, which
187
+ # crosses newlines, so the gap here is `acc_gap` and not `space?`.
188
+ # It is whitespace only, never a comment: mmdc keeps `%% comment`
189
+ # in `accTitle: %% comment` as the title text.
190
+ #
191
+ # The text runs to the PHYSICAL end of the line, not to `line_end`,
192
+ # for the same reason: a `%%` inside a title is ordinary text.
193
+ # The text may be empty — mmdc renders `accTitle:` with nothing
194
+ # after it.
195
+ rule(:acc_line) do
196
+ (str("accTitle") | str("accDescr")).as(:acc_keyword) >>
197
+ acc_gap >> str(":") >> acc_gap >>
198
+ (newline.absent? >> any).repeat.as(:acc_text) >> (newline | eof)
199
+ end
200
+
201
+ # Whitespace crosses newlines, and once a newline is crossed a
202
+ # STANDALONE comment line is skipped whole: mmdc reads
203
+ # `accTitle:` newline `%% c` newline `Real` as the title `Real`.
204
+ # A comment on the delimiter's OWN line stays text, which is why
205
+ # the skipping only starts after the first newline.
206
+ rule(:acc_gap) do
207
+ line_space.repeat >>
208
+ (newline >> (line_space | newline | comment).repeat).repeat
209
+ end
210
+
211
+ # Whitespace that stays on the line: mermaid's `\s` without the
212
+ # newline. It is wider than a space and a tab — mmdc reads
213
+ # `accTitle` no-break-space `:` as a title.
214
+ #
215
+ # No carriage return: the parser folds every one into a newline
216
+ # before the grammar sees it, the way mermaid does.
217
+ #
218
+ # Ruby's `[[:space:]]` is not the same set in either direction. It
219
+ # misses U+FEFF, which mmdc treats as a space, and it adds U+0085,
220
+ # which mmdc does not. So the set is spelled out here, once, and
221
+ # reused everywhere a raw `Regexp` needs the same characters (see
222
+ # `EDGE_ID_END_AHEAD` below) so a future correction to this
223
+ # hand-tuned set cannot drift between the two.
224
+ LINE_SPACE_CHARS = '\t\v\f\x20\u00A0\u1680' \
225
+ '\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF'
226
+ private_constant :LINE_SPACE_CHARS
227
+
228
+ rule(:line_space) { match[LINE_SPACE_CHARS] }
229
+
230
+ # Mermaid deletes whole comment LINES before it parses anything,
231
+ # so a `}` inside one does not close a block: `accDescr {` /
232
+ # `text` / `%% }` swallows every line after it.
233
+ #
234
+ # A comment starts at a newline and stops at its own line end,
235
+ # leaving that newline behind. The next line claims it, which is
236
+ # how two comment lines in a row both match, and running out of
237
+ # source just ends the repeat. The strip is anchored to the line
238
+ # start, so mmdc closes the block in `accDescr {text %% }`.
239
+ rule(:comment_line) do
240
+ newline >> line_space.repeat >> str("%%") >>
241
+ (newline.absent? >> any).repeat
242
+ end
243
+
244
+ rule(:acc_block_body) do
245
+ (comment_line | (str("}").absent? >> any)).repeat
246
+ end
247
+
248
+ # The block form ends at its closing brace, with no line end
249
+ # required after it: mermaid renders `accDescr {Desc}A-->B`.
250
+ #
251
+ # An unterminated block runs to the end of the source rather than
252
+ # failing, because mmdc draws `accDescr {Unterminated` and
253
+ # swallows every line after it.
254
+ rule(:acc_descr_block) do
255
+ str("accDescr").as(:acc_keyword) >> acc_gap >> str("{") >>
256
+ acc_block_body.as(:acc_text) >>
257
+ (str("}") >> line_space.repeat >> semicolon.maybe).maybe
258
+ end
259
+
260
+ # Subgraph: subgraph id [title] ... end, or `subgraph id title`
261
+ # with the rest of the line as the title.
262
+ rule(:subgraph_statement) do
263
+ str("subgraph").as(:subgraph_keyword) >> space >>
264
+ subgraph_id.as(:subgraph_id) >>
265
+ subgraph_name_and_title >>
266
+ declaration_end >>
267
+ ws? >>
268
+ statements.maybe.as(:subgraph_statements) >>
269
+ ws? >>
270
+ str("end").as(:subgraph_end) >>
271
+ subgraph_close
272
+ end
273
+
274
+ # What sits between the subgraph id and the end of its line.
275
+ #
276
+ # Two readings met here. This branch reads the rest of the line as
277
+ # the box TITLE, because the cluster renderer needs that text. The
278
+ # `end`-word walk on main reads extra WORDS in front of an optional
279
+ # bracketed title and throws the words away. Both are kept: the
280
+ # title is tried first, and it only wins when it reaches the end of
281
+ # the declaration, so `subgraph A Title` is titled `A Title` while
282
+ # `subgraph A B [T]` still falls through to the word walk and is
283
+ # titled `T`.
284
+ rule(:subgraph_name_and_title) do
285
+ (subgraph_title >> declaration_end.present?) |
286
+ subgraph_trailing_name
287
+ end
288
+
289
+ # The declaration owns the rest of its line. A bracketed title has
290
+ # to be the last thing on it — mmdc refuses `subgraph s [T] A` and
291
+ # `subgraph s [T] %% note`, while `subgraph s %% note` is fine
292
+ # because the comment simply becomes the title text.
293
+ # All the whitespace, not just the first space: consuming one let
294
+ # `subgraph s [Title] A` slip past the bracket guard below and be
295
+ # read as free text, which mmdc refuses.
296
+ # A bracketed title may sit straight against the id — mmdc renders
297
+ # `subgraph s[Title]` — while a free one needs a gap to start.
298
+ # The gap is captured, not just skipped. A free title's label is
299
+ # the source from the id onwards, so `subgraph s Title` is
300
+ # labelled `s Title` and collapsing the run would misquote it.
301
+ rule(:subgraph_title) do
302
+ (space.repeat >> bracketed_title) |
303
+ (space.repeat(1).as(:subgraph_free_gap) >> free_title)
304
+ end
305
+
306
+ rule(:bracketed_title) { bracket_title >> bracket_title_end }
307
+
308
+ rule(:bracket_title) do
309
+ lbracket >> (rbracket.absent? >> any).repeat(1).as(:subgraph_title) >>
310
+ rbracket
311
+ end
312
+
313
+ # Nothing at all may follow a bracketed title, not even a space:
314
+ # mmdc refuses `subgraph s [Title] ` before the newline.
315
+ rule(:bracket_title_end) do
316
+ ((semicolon_run >> no_comment) | newline | eof).present?
317
+ end
318
+
319
+ # A flat character class, NOT `declaration_end.absent? >> any`.
320
+ # That re-ran `space?` at every byte, so a title carrying a run of
321
+ # spaces parsed in quadratic time — 4k spaces took 3.7 seconds and
322
+ # 16k took over a minute, on a source mmdc renders.
323
+ #
324
+ # The structural characters are excluded because mermaid refuses
325
+ # them here: `subgraph s Title (More)`, `<More>` and `{More}` are
326
+ # all rejected.
327
+ rule(:free_title) do
328
+ lbracket.absent? >>
329
+ (title_run >> (space.repeat(1) >> title_run).repeat)
330
+ .as(:subgraph_free_title)
331
+ end
332
+
333
+ rule(:title_run) { comment_word | title_word }
334
+
335
+ # Mermaid only strips a comment at the start of a line, so behind
336
+ # the id it stays text and owns the rest of the line.
337
+ rule(:comment_word) do
338
+ str("%%") >> (newline.absent? >> any).repeat
339
+ end
340
+
341
+ # Main's guarded name word, not a loose character run. A free
342
+ # title's words are the same words a trailing name may hold, so
343
+ # `subgraph A end` and `subgraph A interpolate` stay refused while
344
+ # the text still becomes the box title.
345
+ rule(:title_word) { subgraph_name_word }
346
+
347
+ # A semicolon run separates statements here as it does after `end`,
348
+ # and a comment is not a statement: mmdc renders `subgraph s;;A`
349
+ # and refuses `subgraph s; %% note`.
350
+ rule(:declaration_end) do
351
+ line_space.repeat >> ((semicolon_run >> no_comment) | newline | eof)
352
+ end
353
+
354
+ rule(:subgraph_close) do
355
+ (space.repeat(1) >> close_at_space) |
356
+ (space? >> (close_at_line_end | close_at_semicolon))
357
+ end
358
+
359
+ # Whitespace alone separates `end` from what follows: mmdc renders
360
+ # `end B-->C`. Referencing `statement` here would be recursive, so
361
+ # the guard is what a statement cannot start with — a comment, a
362
+ # separator, or the end of the line.
363
+ rule(:close_at_space) do
364
+ (str("%%") | semicolon | newline | eof).absent?
365
+ end
366
+
367
+ # `end` finishes its line, with any number of trailing semicolons.
368
+ # Nothing else may follow, so `end %% note` is refused here — mmdc
369
+ # takes `end` and then a comment on its own line.
370
+ rule(:close_at_line_end) do
371
+ semicolon_run.maybe >> space? >> (newline | eof)
372
+ end
373
+
374
+ # A semicolon separates statements, so the next one may sit on the
375
+ # same line: mmdc renders `end; B-->C` and `end;end`. A comment is
376
+ # still not a statement, and `end; %% note` is refused.
377
+ rule(:close_at_semicolon) do
378
+ semicolon_run >> str("%%").absent?
379
+ end
380
+
381
+ rule(:semicolon_run) { (semicolon >> space?).repeat(1) }
382
+
383
+ # A comment is not a statement, so it cannot follow a separator on
384
+ # the same line: mmdc refuses `end; %% note` and
385
+ # `subgraph s; %% note`.
386
+ rule(:no_comment) { str("%%").absent? }
387
+
388
+ # An unbracketed name runs on past the first space: mermaid titles
389
+ # `subgraph 1 abc` "1 abc". Every trailing word carries the same
390
+ # guards as the first one, `end` included, because they all go
391
+ # through `subgraph_name_word`. So mmdc's refusals
392
+ # (`subgraph A interpolate`, `subgraph A 1default`,
393
+ # `subgraph A .-`) are refused here too, while `subgraph A end [T]`
394
+ # and `subgraph A end B` draw: what ends a subgraph is `end` with
395
+ # the LINE behind it, the position and not the word.
396
+ #
397
+ # The charset is narrower than mermaid's title text, so only id
398
+ # words are consumed. mmdc titles `subgraph A B:C` "A B:C" and this
399
+ # refuses it — left for a change that models mermaid's own
400
+ # `textNoTags` production. Nothing reads the name yet, so the extra
401
+ # words are consumed, not kept.
402
+ #
403
+ # The bracketed title comes AFTER those words and this is the only
404
+ # rule that reads it, so `subgraph A B [T]` and `subgraph A B C [T]`
405
+ # draw. `subgraph A [T] X` stays refused, as mmdc refuses it:
406
+ # nothing may follow the title on the line.
407
+ rule(:subgraph_trailing_name) do
408
+ (space.repeat(1) >> subgraph_name_word).repeat >>
409
+ subgraph_title.maybe
410
+ end
411
+
412
+ rule(:subgraph_end_word) { str("end") >> word_boundary }
413
+
414
+ # Style: style nodeId fill:#f9f
415
+ rule(:style_statement) do
416
+ str("style").as(:style_keyword) >> space >>
417
+ reserved_keyword.absent? >> node_id.as(:style_target) >>
418
+ (space >> style_property_list).as(:style_props) >>
419
+ statement_end
420
+ end
421
+
422
+ # What a `;` does inside a declaration turns on the value, not on
423
+ # the text after it:
424
+ #
425
+ # style A fill:red;B -> nodes A and B (the `;` ends the style)
426
+ # style A fill:#f9f;B -> node A only (the `;` is value text)
427
+ #
428
+ # The plain branch is guarded rather than merely second, so a bad
429
+ # hashed tail fails the statement instead of falling through and
430
+ # drawing a node mermaid refuses.
431
+ rule(:style_property_list) do
432
+ empty_comma_item.absent? >>
433
+ (hashed_property_list | (hashed_head.absent? >> style_property))
434
+ end
435
+
436
+ # mermaid joins declarations with a comma and refuses an empty one:
437
+ # `,a:b`, `a:b,,c:d` and `a:b,`. After a `#` a `;` no longer ends
438
+ # the item, so `fill:#f9f,;B` is fine while `fill:red,;B` is not.
439
+ rule(:empty_comma_item) do
440
+ comma |
441
+ (hash_char.maybe >>
442
+ hash_terminated_run(comma_gap).repeat >>
443
+ (comma_gap | (hash_char >> hashed_comma_gap_scan)))
444
+ end
445
+
446
+ rule(:hashed_comma_gap_scan) do
447
+ declaration_run(hashed_comma_gap).repeat >> hashed_comma_gap
448
+ end
449
+
450
+ rule(:comma_gap) do
451
+ hashed_comma_gap | (comma >> semicolon)
452
+ end
453
+
454
+ rule(:hashed_comma_gap) { comma >> (comma | newline | eof) }
455
+
456
+ # After a `#` the declaration carries at most one `;`. mmdc
457
+ # takes `fill:#f9f;stroke:#333` and `fill:#f9f;B`, and refuses
458
+ # `fill:#f9f;B;C` and `fill:#f9f;;B`, so the tail has to end at the
459
+ # line rather than hand a second `;` back as a separator.
460
+ rule(:hashed_property_list) do
461
+ hashed_head >> declaration_run.repeat >>
462
+ ((semicolon >> space? >> hashed_tail) | semicolon.absent?)
463
+ end
464
+
465
+ # The `#` has to arrive before the first `;` for the swallow to
466
+ # start: mmdc reads `style A fill:red;stroke:#333` as a style plus a
467
+ # node, because the `;` comes first.
468
+ # A `#` after a space, as in `stroke: #fff`, is ordinary text.
469
+ rule(:hashed_head) do
470
+ hash_char.maybe >> hash_terminated_run.repeat >> hash_char
471
+ end
472
+
473
+ # What may follow that one `;` is a style component, not a node and
474
+ # not an edge, so it stops at anything structural. It ends where
475
+ # `line_end` ends and nowhere else: a `%%` here is ordinary
476
+ # declaration text.
477
+ rule(:hashed_tail) do
478
+ declaration_run(structural_token).repeat >>
479
+ space? >> (newline | eof).present?
480
+ end
481
+
482
+ # What mermaid keeps for shapes and edges. It is refused wherever
483
+ # mermaid expects a bare word — in a style declaration after a `#`
484
+ # and in a callback name alike.
485
+ rule(:structural_token) { compound_token | structural_char }
486
+
487
+ # Some of what mermaid reserves is longer than one character, and
488
+ # every character in it is legal on its own. In a style tail and in
489
+ # a callback name alike, `B-C`, `B.C` and `B::C` are ordinary text
490
+ # while `B--C`, `B-.C` and `B:::C` are errors.
491
+ rule(:compound_token) { str(":::") | str("--") | str("-.") }
492
+
493
+ # The single-character half of the set. Every other printable ASCII
494
+ # character is fine in both places.
495
+ rule(:structural_char) { match['\\[\\]{}()<>|~@=^'] }
496
+
497
+ # A run of declaration text, or of interior spaces, matched whole
498
+ # rather than one character at a time: checking `line_end` (hence
499
+ # the unbounded `space?` inside it) at every character re-scans to
500
+ # the end of a space run from every offset inside it — quadratic in
501
+ # the run's length. A space run costs one pass here instead, kept
502
+ # or dropped exactly as `line_end.absent?` did: followed by more
503
+ # content it is ordinary text, running straight to `;`/newline/eof
504
+ # it is not. `extra_guards` exclude characters a caller treats as
505
+ # its own boundary, on top of `;` and a line's end.
506
+ def declaration_run(*extra_guards)
507
+ guards = extra_guards + [semicolon, newline, space]
508
+ guarded_declaration_char(guards).repeat(1) | declaration_space
509
+ end
510
+ private :declaration_run
511
+
512
+ def guarded_declaration_char(guards)
513
+ guards.map(&:absent?).reduce(:>>) >> any
514
+ end
515
+ private :guarded_declaration_char
516
+
517
+ def declaration_space
518
+ space.repeat(1) >> (newline | eof).absent?
519
+ end
520
+ private :declaration_space
521
+
522
+ # Same shape as `declaration_run`, but a run also stops at a bare
523
+ # `#`: a space immediately before one, as in `stroke: #fff`, is
524
+ # ordinary text, so both are swallowed together — otherwise the `#`
525
+ # would be mistaken for the marker `hashed_head`/`empty_comma_item`
526
+ # scan for.
527
+ def hash_terminated_run(*extra_guards)
528
+ guards = extra_guards + [hash_char, semicolon, newline, space]
529
+ guarded_declaration_char(guards).repeat(1) | hash_terminated_space
530
+ end
531
+ private :hash_terminated_run
532
+
533
+ def hash_terminated_space
534
+ space.repeat(1) >> (hash_char | (newline | eof).absent?)
535
+ end
536
+ private :hash_terminated_space
537
+
538
+ # Permissive: mermaid takes `style A red`, `style A fill:` and
539
+ # `style A fill :red`, so `name:value` is not required. One
540
+ # character minimum though — mmdc refuses a bare `style A`.
541
+ #
542
+ # Without a hash the `;` always ends the statement, so a second
543
+ # declaration is left to be parsed as one. mermaid draws
544
+ # `style A fill:red;stroke:blue` as a style plus a node called
545
+ # `stroke:blue`; a node id here takes no colon, so we refuse the
546
+ # line rather than draw a diagram one node short.
547
+ rule(:style_property) { declaration_run.repeat(1) }
548
+
549
+ # `linkStyle 0,1 stroke:red` styles edges by position, and
550
+ # `linkStyle default ...` every one. Mermaid takes the numbers with
551
+ # no space around a comma, and refuses `linkStyle 0 ,1 ...`.
552
+ rule(:link_style_statement) do
553
+ str("linkStyle").as(:link_style_keyword) >> line_space >>
554
+ link_style_targets.as(:link_targets) >> line_space >>
555
+ link_style_props >> statement_end.as(:link_end)
556
+ end
557
+
558
+ # `interpolate basis` goes first, with one space before the curve
559
+ # name: mermaid refuses `interpolate basis` and
560
+ # `stroke:red interpolate basis`. The builder refuses the word
561
+ # `interpolate` or `default` anywhere else.
562
+ rule(:link_style_props) do
563
+ (str("interpolate") >> line_space >>
564
+ match["^#{LINE_SPACE_CHARS}\n;"].repeat(1).as(:link_curve) >>
565
+ (line_space.repeat(1) >> style_property_list.as(:link_props))
566
+ .maybe) |
567
+ style_property_list.as(:link_props)
568
+ end
569
+
570
+ rule(:link_style_targets) do
571
+ str("default") | (link_index >> (comma >> link_index).repeat)
572
+ end
573
+
574
+ rule(:link_index) { match["0-9"].repeat(1) }
575
+
576
+ # ClassDef: classDef className fill:#f9f
577
+ rule(:class_def_statement) do
578
+ str("classDef").as(:classdef_keyword) >> space >>
579
+ class_name_list.as(:class_name) >>
580
+ (space >> style_property_list).as(:class_props) >>
581
+ statement_end
582
+ end
583
+
584
+ # `classDef a,b props` styles both classes.
585
+ rule(:class_name_list) do
586
+ identifier >> (comma >> identifier).repeat
587
+ end
588
+
589
+ # Class assignment: class nodeId className
590
+ rule(:class_assignment_statement) do
591
+ str("class").as(:class_keyword) >> space >>
592
+ reserved_keyword.absent? >> node_id.as(:class_target) >> space >>
593
+ identifier.as(:class_name) >>
594
+ statement_end
595
+ end
596
+
597
+ # Click: click nodeId href (may not fully implement, just parse)
598
+ #
599
+ # The gap after the keyword is a RUN of whitespace, unlike the one
600
+ # before the action. mermaid opens its click state on `"click"\s+`,
601
+ # so mmdc draws `click A "url"` where a single space refused it.
602
+ rule(:click_statement) do
603
+ str("click").as(:click_keyword) >> space.repeat(1) >>
604
+ click_target.as(:click_target) >>
605
+ (space >> click_action.as(:click_action)) >>
606
+ statement_end
607
+ end
608
+
609
+ # An action is required — mmdc rejects a bare `click A`. mermaid
610
+ # takes exactly four shapes here and anything else is an error, so
611
+ # the action is spelled out rather than scanned to the end of the
612
+ # line.
613
+ #
614
+ # Every gap between these tokens is ONE space or ONE tab. mermaid
615
+ # counts the characters: `click A href "u"`, `"u" "tip"` and
616
+ # `"u" _blank` are all errors. Only inside a `call` is whitespace
617
+ # free-form.
618
+ rule(:click_action) do
619
+ callback_action | href_action | link_action | callback_name_action
620
+ end
621
+
622
+ # `click A call cb(foo;bar)` — the semicolon belongs to the callback
623
+ # argument list, so the parens are only special after `call`.
624
+ #
625
+ # The name runs to the opening paren, dots and semicolons included:
626
+ # mmdc reads `call cb;B()` as one callback named `cb;B`, and takes
627
+ # `call ns.cb()` and any run of spaces after `call`.
628
+ #
629
+ # Only a quoted tooltip may trail the parens. mmdc refuses
630
+ # `call cb() _blank` and `call cb() nope`, which the old open-ended
631
+ # tail accepted.
632
+ rule(:callback_action) do
633
+ call_opener >> callback_name >>
634
+ callback_gap? >> lparen >>
635
+ (rparen.absent? >> any).repeat >> rparen >>
636
+ (space >> quoted_run).maybe
637
+ end
638
+
639
+ # Once `call` has opened a callback the parens are compulsory: mmdc
640
+ # exits 1 on `click A call cb`.
641
+ rule(:call_opener) { str("call") >> callback_gap }
642
+
643
+ # mermaid stops caring about line structure inside a callback, so
644
+ # whitespace and comments are ignorable after `call` and again
645
+ # before the `(`. mmdc renders all four of `call cb()`,
646
+ # `call` nl `cb()`, `call cb` nl `()` and `call cb` nl `%% c` nl
647
+ # `()`.
648
+ rule(:callback_gap) { (space | newline | comment).repeat(1) }
649
+ rule(:callback_gap?) { callback_gap.maybe }
650
+
651
+ # The name still stops at the end of its line. mermaid keeps reading
652
+ # past it — `call cb` nl `B --> C` nl `D()` swallows the whole edge
653
+ # and draws neither B nor C — and following it there would let a
654
+ # callback eat statements we can still draw.
655
+ rule(:callback_name) do
656
+ (lparen.absent? >> line_end.absent? >> any).repeat(1)
657
+ end
658
+
659
+ # `href` takes a quoted url, then at most a quoted tooltip and a
660
+ # link target, in that order. mmdc refuses `href` on its own,
661
+ # `href cb`, `href "u" nope` and a third quoted run.
662
+ rule(:href_action) { str("href") >> space >> quoted_run >> link_tail }
663
+
664
+ # The same shape without the keyword: `click A "u" "tip" _blank`.
665
+ rule(:link_action) { quoted_run >> link_tail }
666
+
667
+ # A quoted tooltip then a link target, both optional and in that
668
+ # order. mmdc refuses `"u" _blank "tip"` and a third quoted run.
669
+ rule(:link_tail) do
670
+ (space >> quoted_run).maybe >> (space >> link_target).maybe
671
+ end
672
+
673
+ # A bare token is a callback name, and only a quoted tooltip may
674
+ # follow it. mmdc draws `click A clickByFlow "Add a div"` and
675
+ # `click A http://x`, and refuses `click A cb _blank` and
676
+ # `click A my callback`.
677
+ rule(:callback_name_action) do
678
+ bare_token >> (space >> quoted_run).maybe
679
+ end
680
+
681
+ # A bare token runs to the first space, `;` or structural token,
682
+ # so `click A cb()` and `click A cb--x` are errors and
683
+ # `click A http://x;B` draws both nodes. A quote only opens a url
684
+ # when it comes first — mmdc draws `click A cb"x`. A keyword is
685
+ # not a callback name either: mmdc refuses `click A href`,
686
+ # `click A end` and `click A _blank`, and takes `click A callback`
687
+ # and `click A clickByFlow`.
688
+ rule(:bare_token) do
689
+ reserved_keyword.absent? >> str('"').absent? >>
690
+ token_char >> token_char.repeat
691
+ end
692
+
693
+ rule(:token_char) do
694
+ space.absent? >> line_end.absent? >> semicolon.absent? >>
695
+ structural_token.absent? >> any
696
+ end
697
+
698
+ # Never empty. mmdc refuses `click A ""` and every empty tooltip,
699
+ # and takes `click A " "`.
700
+ rule(:quoted_run) do
701
+ str('"') >> (str('"').absent? >> any).repeat(1) >> str('"')
702
+ end
703
+
704
+ # A statement keyword is not a node id. Without this a malformed
705
+ # directive falls through to the node rules: `click ;B` would
706
+ # produce nodes `click` and `B`, and mmdc rejects the whole source.
707
+ #
708
+ # The same words that end an id end a statement, so this asks
709
+ # `node_keyword` rather than keeping a second list to drift out of
710
+ # step. Every call site spells `reserved_keyword.absent? >> node_id`.
711
+ rule(:reserved_keyword) { node_keyword }
712
+
713
+ # `click`, `href` and `call` are directive words wherever a space,
714
+ # a newline or the end of input follows, and ordinary node ids
715
+ # before a `;` or a shape. mmdc refuses `href --- B`, `call --- B`
716
+ # and a bare `href`, and draws `href;B` and `href[x]` as nodes — so
717
+ # the reservation hangs off the boundary, not the word.
718
+ #
719
+ # NOT `line_end`: it swallows a trailing semicolon, and mmdc draws
720
+ # `graph TD;click;B` as two nodes.
721
+ #
722
+ # A comment is not one of the endings either. Behind a word it is
723
+ # not a comment at all, because mermaid only strips one at the
724
+ # start of a line: mmdc reads `href%%c` as a SINGLE node.
725
+ #
726
+ # The end of the source IS one. Mermaid appends a newline before it
727
+ # lexes, so a word at the very end is followed by one after all.
728
+ #
729
+ # These three words also guard an id, which is why `node_keyword`
730
+ # reaches for this rule rather than restating it.
731
+ rule(:spaced_keyword) do
732
+ (str("click") | str("href") | str("call")) >>
733
+ (space | newline | eof).present?
734
+ end
735
+
736
+ # mermaid's link targets, as they appear at the END of a click
737
+ # action (`click A "url" "_blank"`). They are reserved as node ids
738
+ # too, but that is `RESERVED_WORDS`' job now — this rule is only
739
+ # the click-action tail, which is why it survives on its own.
740
+ rule(:link_target) do
741
+ (str("_parent") | str("_blank") | str("_self") | str("_top")) >>
742
+ word_boundary
743
+ end
744
+
745
+ # A word ends where the next character cannot continue it, and
746
+ # mermaid counts an accent as the end: `1endé` is refused while
747
+ # `1end_`, `1end2` and `1endx` are all ids.
748
+ rule(:word_boundary) { match["a-zA-Z0-9_"].absent? }
749
+
750
+ # One `x` or `o` and then a link body, with nothing between them.
751
+ rule(:flush_link_marker) { match["xo"] >> link_body }
752
+
753
+ # Node with optional shape and edges
754
+ rule(:node_edge_statement) do
755
+ reserved_keyword.absent? >>
756
+ node_with_shape.as(:node) >> amp_group.as(:group) >>
757
+ edge_chain.maybe.as(:edges) >>
758
+ loose_statement_end
759
+ end
760
+
761
+ # Standalone node (just an identifier)
762
+ rule(:standalone_node) do
763
+ reserved_keyword.absent? >> node_id.as(:node_id) >>
764
+ loose_statement_end
765
+ end
766
+
767
+ # Node with its optional shape, inline class and metadata.
768
+ # Every optional part is captured whether or not it is present, so
769
+ # the tree has one shape instead of one per combination. Parslet
770
+ # omits a `.maybe` that wraps its own `.as`, which is why the
771
+ # transform previously needed a rule per combination.
772
+ # On its second branch the `:shape` slot comes back nil:
773
+ # `dot_absent` is a zero-width lookahead and captures nothing. It
774
+ # is there only to refuse the flush dot a bare name would
775
+ # otherwise swallow.
776
+ #
777
+ # The shape opening abuts the id, with no gap of any kind: mmdc
778
+ # refuses `A [B]`, `A\t[B]`, `A\n[B]` and `A %% c\n[B]` alike.
779
+ #
780
+ # `flush_link_marker.absent?` on the first line is the other
781
+ # refusal. A lone `x` or `o` sitting flush against a link body is
782
+ # mermaid's link-START marker, not a node called `x`: mmdc reads
783
+ # `x===B` as a link with nothing on its left and refuses it, while
784
+ # it draws `xx===B` and `x === B` — the marker has to be one
785
+ # character and flush. This rule is the edge TARGET as well as the
786
+ # statement's own node, so the guard governs both positions, and
787
+ # mmdc refuses `A --> x---B` for the same reason it refuses
788
+ # `x===B`.
789
+ rule(:node_with_shape) do
790
+ flush_link_marker.absent? >>
791
+ node_id.as(:node_id) >>
792
+ (node_shape.as(:shape) | dot_absent.as(:shape)) >>
793
+ (inline_class >> dot_absent).maybe.as(:inline_class) >>
794
+ node_metadata.maybe.as(:metadata)
795
+ end
796
+
797
+ # A shape and a `@{...}` block each close the node's name, and a
798
+ # link may then sit flush against it: mmdc draws `A[x].-B` and
799
+ # `A@{ shape: rect }.->B`. A bare name is still growing, and so is
800
+ # the class name of `A:::c` — each takes a `.` in mermaid, so mmdc
801
+ # reads the dot of `A.-B` and of `A[x]:::c.-B` as name text and
802
+ # refuses `A.->B`, `A:::c.->B` and `A[x]:::c.->B` outright.
803
+ #
804
+ # `node_id` carries a dot the same way a bare name does — see its
805
+ # own comment above — so `A.-B` reads exactly as mmdc reads it,
806
+ # as the single node `"A.-B"`, not a link: that is the no-shape
807
+ # fallback above, where `dot_absent` stands in for `:shape`
808
+ # itself. A real shape bypasses the guard outright — `A[x].-B`
809
+ # and `A@{ shape: rect }.->B` both draw as mmdc draws them —
810
+ # unless an inline class follows the shape, which is the second
811
+ # place the guard runs. That second use is where Etassia and mmdc
812
+ # part ways: the other five — `A:::c.-B`, `A[x]:::c.-B` and the
813
+ # three `.->` forms — are still refused rather than read as a
814
+ # link.
815
+ rule(:dot_absent) { str(".").absent? }
816
+
817
+ # `D@{ shape: rounded, label: "DD" }` — mermaid's newer way of
818
+ # giving a node a shape or a label, usable as a statement of its own
819
+ # or as a suffix inside an edge chain.
820
+ # The body is captured raw and handed to YAML, because that is what
821
+ # mermaid does with it. A single-line body is a flow mapping and a
822
+ # multiline one is block YAML, so commas are required on one line
823
+ # and rejected across several — a grammar rule cannot express that
824
+ # without reimplementing YAML badly.
825
+ #
826
+ # A metadata block cannot be the final source before an edge id:
827
+ # mermaid leaves the following `e1@` out of its LINK_ID state and
828
+ # refuses the statement, though an ordinary link may still follow.
829
+ rule(:node_metadata) do
830
+ str("@{") >> metadata_body.as(:body) >> str("}") >>
831
+ (line_space.repeat(1) >> edge_id_ahead).absent?
832
+ end
833
+
834
+ # An unmatched `"` is not body text. mermaid's lexer stays in its
835
+ # string state to the end of the block and refuses the source;
836
+ # falling through to the generic branch took `A@{ label: a"b }`,
837
+ # which mmdc rejects.
838
+ #
839
+ # A caret is not body text either. mermaid takes the run between
840
+ # the braces with `[^}^"]+`, so a `^` outside a quoted value ends
841
+ # the block early and mmdc refuses `A@{ label: a^b }`. Inside the
842
+ # quotes the rule is `[^"]+`, and `A@{ label: "a^b" }` draws.
843
+ rule(:metadata_body) do
844
+ (metadata_comment_line | metadata_quoted |
845
+ (metadata_stop.absent? >> any)).repeat
846
+ end
847
+
848
+ rule(:metadata_stop) { str('"') | str("}") | str("^") }
849
+
850
+ # Mermaid strips comments before lexing, so their stops are text.
851
+ # `line_space` rather than Ruby's `\s`: the indent has to be the set
852
+ # mmdc calls whitespace, or a comment indented with a no-break space
853
+ # keeps its `%%` in the label and its `}` closes the block.
854
+ #
855
+ # `%%{` opens a directive rather than a comment and is left alone,
856
+ # which is what makes etassia refuse `A@{ shape: rect` nl `%%{ x }`
857
+ # nl `}` the way mmdc does.
858
+ rule(:metadata_comment_line) do
859
+ newline >> line_space.repeat >> str("%%") >> str("{").absent? >>
860
+ (newline.absent? >> any).repeat(1)
861
+ end
862
+
863
+ # A double-quoted run is skipped whole so a brace inside it is
864
+ # text. Only the double quote does this: mermaid's metadata lexer
865
+ # has one string state and `"` opens it, so `label: 'a}b'` ends the
866
+ # block at that brace and mmdc rejects the line.
867
+ # A comment line inside the quotes is skipped first, because the
868
+ # quote that closes this run cannot be one mermaid already deleted:
869
+ # `label: "one` nl `%% has " quote` nl `two"` is one label to mmdc.
870
+ rule(:metadata_quoted) do
871
+ str('"') >>
872
+ (metadata_comment_line | (str('"').absent? >> any)).repeat >>
873
+ str('"')
874
+ end
875
+
876
+ # Inline class syntax: :::className
877
+ rule(:inline_class) do
878
+ str(":::") >> identifier
879
+ end
880
+
881
+ # Node shape with label
882
+ rule(:node_shape) do
883
+ # Order matters: try longer delimiters first
884
+ shape_triple_circle |
885
+ shape_stadium |
886
+ shape_subroutine |
887
+ shape_cylindrical |
888
+ shape_double_circle |
889
+ shape_hexagon |
890
+ shape_parallelogram |
891
+ shape_parallelogram_alt |
892
+ shape_trapezoid |
893
+ shape_trapezoid_alt |
894
+ shape_asymmetric |
895
+ shape_rectangle |
896
+ shape_rounded |
897
+ shape_rhombus
898
+ end
899
+
900
+ # Shape definitions (15+ shapes)
901
+ # Rectangle: [label]
902
+ # A bare `)` is still a round-node closer in Mermaid's text lexer,
903
+ # even while a rectangular label is open. Quoting shields it, so
904
+ # `A[hello ) world]` is invalid but `A["hello ) world"]` renders.
905
+ rule(:shape_rectangle) do
906
+ lbracket.as(:open) >>
907
+ (
908
+ quoted_label | ((rbracket | rparen).absent? >> any)
909
+ ).repeat.as(:label) >>
910
+ rbracket.as(:close)
911
+ end
912
+
913
+ # Rounded: (label)
914
+ # A double-quoted run is label text whole, so `("a (1)")` ends at
915
+ # the last paren and not the one inside the quotes.
916
+ # A quote that never closes is refused, not read as plain text,
917
+ # except on a comment line, which mermaid deletes before it looks.
918
+ rule(:shape_rounded) do
919
+ lparen.as(:open) >>
920
+ (quoted_label | inline_comment_line |
921
+ ((rparen | str('"')).absent? >> any)).repeat.as(:label) >>
922
+ rparen.as(:close)
923
+ end
924
+
925
+ # Stadium: ([label])
926
+ rule(:shape_stadium) do
927
+ str("([").as(:open) >>
928
+ (str("])").absent? >> any).repeat.as(:label) >>
929
+ str("])").as(:close)
930
+ end
931
+
932
+ # Subroutine: [[label]]
933
+ rule(:shape_subroutine) do
934
+ str("[[").as(:open) >>
935
+ (str("]]").absent? >> any).repeat.as(:label) >>
936
+ str("]]").as(:close)
937
+ end
938
+
939
+ # Cylindrical/Database: [(label)]
940
+ rule(:shape_cylindrical) do
941
+ str("[(").as(:open) >>
942
+ (str(")]").absent? >> any).repeat.as(:label) >>
943
+ str(")]").as(:close)
944
+ end
945
+
946
+ # Circle: ((label))
947
+ rule(:shape_double_circle) do
948
+ str("((").as(:open) >>
949
+ (str("))").absent? >> any).repeat.as(:label) >>
950
+ str("))").as(:close)
951
+ end
952
+
953
+ # Triple Circle: (((label)))
954
+ rule(:shape_triple_circle) do
955
+ str("(((").as(:open) >>
956
+ (str(")))").absent? >> any).repeat.as(:label) >>
957
+ str(")))").as(:close)
958
+ end
959
+
960
+ # Asymmetric: >label]
961
+ rule(:shape_asymmetric) do
962
+ str(">").as(:open) >>
963
+ (rbracket.absent? >> any).repeat.as(:label) >>
964
+ rbracket.as(:close)
965
+ end
966
+
967
+ # Rhombus/Diamond: {label}
968
+ rule(:shape_rhombus) do
969
+ lbrace.as(:open) >>
970
+ (rbrace.absent? >> any).repeat.as(:label) >>
971
+ rbrace.as(:close)
972
+ end
973
+
974
+ # Hexagon: {{label}}
975
+ rule(:shape_hexagon) do
976
+ str("{{").as(:open) >>
977
+ (str("}}").absent? >> any).repeat.as(:label) >>
978
+ str("}}").as(:close)
979
+ end
980
+
981
+ # Parallelogram: [/label/]
982
+ rule(:shape_parallelogram) do
983
+ str("[/").as(:open) >>
984
+ (str("/]").absent? >> any).repeat.as(:label) >>
985
+ str("/]").as(:close)
986
+ end
987
+
988
+ # Parallelogram Alt: [\label\]
989
+ rule(:shape_parallelogram_alt) do
990
+ str("[\\").as(:open) >>
991
+ (str('\\]').absent? >> any).repeat.as(:label) >>
992
+ str('\\]').as(:close)
993
+ end
994
+
995
+ # Trapezoid: [/label\]
996
+ rule(:shape_trapezoid) do
997
+ str("[/").as(:open) >>
998
+ (str('\\]').absent? >> any).repeat.as(:label) >>
999
+ str('\\]').as(:close)
1000
+ end
1001
+
1002
+ # Trapezoid Alt: [\label/]
1003
+ rule(:shape_trapezoid_alt) do
1004
+ str("[\\").as(:open) >>
1005
+ (str("/]").absent? >> any).repeat.as(:label) >>
1006
+ str("/]").as(:close)
1007
+ end
1008
+
1009
+ # Edge chain: can have multiple edges from one node
1010
+ rule(:edge_chain) do
1011
+ edge >> edge.repeat
1012
+ end
1013
+
1014
+ # Single edge. A symbol-only link is tried first, as mermaid's
1015
+ # lexer does: `A --x B` is a link with a cross head, and only a
1016
+ # link that cannot end where it starts opens `A -- text --x B`.
1017
+ #
1018
+ # The rule owns the gap before it, because an edge id has to start
1019
+ # on the line the source ends on, after a space: `A` newline
1020
+ # `e1@--> B` and `A[x]e1@--> B` are refused.
1021
+ rule(:edge) do
1022
+ ((line_space.repeat(1) >> edge_id_preempted.absent? >>
1023
+ edge_id_ahead >> edge_id_body) | ws?) >>
1024
+ (piped_edge | inline_label_edge) >>
1025
+ ws? >>
1026
+ reserved_keyword.absent? >> node_with_shape.as(:target) >>
1027
+ amp_group.as(:group)
1028
+ end
1029
+
1030
+ # `A & B`: further nodes joined to the one before, which then
1031
+ # share whatever link comes next. Mermaid needs at least one space
1032
+ # character on each side of the `&`. Which characters count is the
1033
+ # shared `space` rule's business, and it is narrower than mermaid's
1034
+ # `\s`: a tab passes here, an NBSP does not.
1035
+ rule(:amp_group) do
1036
+ (
1037
+ space.repeat(1) >> edge_id_ahead.absent? >> str("&") >>
1038
+ space.repeat(1) >> reserved_keyword.absent? >> node_with_shape
1039
+ ).repeat
1040
+ end
1041
+
1042
+ # `A e1@--> B` names the edge, so a later `e1@{ animate: true }`
1043
+ # can address it. The `@` must be followed by the start of a link,
1044
+ # and the id must not itself start like one: `A --me@--> B` or a
1045
+ # quoted `@` is not an id, but `A {x@--> B` has one, and an id may
1046
+ # start with `;`. Trying a rule and failing would send every `A B`
1047
+ # typo to the Deepest reporter in `Base`, so the source is read first.
1048
+ #
1049
+ # Keep every repeat possessive, each branch on its own character
1050
+ # class: two branches that can match the same text make a failed
1051
+ # match exponential in the blank lines after the `@`.
1052
+ EDGE_ID_END_AHEAD = /
1053
+ @
1054
+ [#{LINE_SPACE_CHARS}]*+
1055
+ (?:
1056
+ \r?\n
1057
+ (?:
1058
+ [#{LINE_SPACE_CHARS}] |
1059
+ \r?\n |
1060
+ %%[^\r\n]*+
1061
+ )*+
1062
+ )?+
1063
+ [ox<]?(?:-|=|\.|~~~)
1064
+ /x
1065
+ private_constant :EDGE_ID_END_AHEAD
1066
+
1067
+ EDGE_ID_AHEAD = /
1068
+ (?!@\{)
1069
+ (?![ox<]?(?:--|==|-\.|\.+-|~~~))
1070
+ [^#{LINE_SPACE_CHARS}\r\n"]+#{EDGE_ID_END_AHEAD}
1071
+ /x
1072
+ private_constant :EDGE_ID_AHEAD
1073
+
1074
+ # An edge id can contain `@`. When one of those internal characters
1075
+ # happens to precede something link-like, the id runs on to the last
1076
+ # `@` that can delimit it. The match ends right before that `@`, or
1077
+ # is empty when the `@` here is the last one.
1078
+ EDGE_ID_LAST_END = /
1079
+ (?:@[^#{LINE_SPACE_CHARS}\r\n"]+)?
1080
+ (?=#{EDGE_ID_END_AHEAD})
1081
+ /x
1082
+ private_constant :EDGE_ID_LAST_END
1083
+
1084
+ # These lexer tokens precede LINK_ID in mermaid, so a matching opener
1085
+ # wins before the otherwise broad `[^\s"]+@` rule can name an edge.
1086
+ # `click`, `href` and `call` are intentionally absent: their tokens
1087
+ # require whitespace after the word, and `@` does not provide it.
1088
+ rule(:edge_id_preempted) do
1089
+ reserved_word |
1090
+ (str("default") >> word_boundary) |
1091
+ (str("accTitle") >> str(":")) |
1092
+ (str("accDescr") >> (str(":") | str("{")))
1093
+ end
1094
+
1095
+ # True where an edge id starts, so a caller can leave it alone.
1096
+ #
1097
+ # These are the only raw-`Regexp` lookaheads in the parser tree;
1098
+ # everywhere else builds lookahead from Parslet atoms with
1099
+ # `.absent?`/`.present?`. Do not copy this as the house style. It is
1100
+ # here because deciding whether an `@` ends an edge id needs an
1101
+ # unbounded forward scan past comment and blank lines to the link
1102
+ # opener, which a combinator cannot express without trying the rule
1103
+ # and failing — the Deepest-reporter problem described above.
1104
+ rule(:edge_id_ahead) do
1105
+ dynamic do |source, _|
1106
+ source.matches?(EDGE_ID_AHEAD) ? str("") : str("").absent?
1107
+ end
1108
+ end
1109
+
1110
+ rule(:edge_id_end_ahead) do
1111
+ dynamic do |source, _|
1112
+ edge_id_end?(source) ? str("") : str("").absent?
1113
+ end
1114
+ end
1115
+
1116
+ # Every `@` between an id's first delimiter-like `@` and its last one
1117
+ # shares that last one, so it is found once and remembered. Asking
1118
+ # afresh at each `@` rescans the rest of the id every time.
1119
+ def edge_id_end?(source)
1120
+ pos = source.bytepos
1121
+ cached, first, last = @edge_id_end
1122
+ cached_position = cached.equal?(source) && pos.between?(first, last)
1123
+ return pos == last if cached_position
1124
+ return false unless source.matches?(EDGE_ID_END_AHEAD)
1125
+
1126
+ last = pos + source.matches?(EDGE_ID_LAST_END)
1127
+ @edge_id_end = [source, pos, last]
1128
+ pos == last
1129
+ end
1130
+ private :edge_id_end?
1131
+
1132
+ rule(:edge_id_body) do
1133
+ ((edge_id_end_ahead | line_space | newline).absent? >> any)
1134
+ .repeat(1).as(:edge_id) >> str("@") >>
1135
+ line_space.repeat >>
1136
+ (newline >> (line_space | newline | comment).repeat).repeat
1137
+ end
1138
+
1139
+ rule(:piped_edge) do
1140
+ arrow.as(:arrow) >> ws? >> edge_label.maybe.as(:label)
1141
+ end
1142
+
1143
+ # `A -- text --> B`: the label sits between the two halves of the
1144
+ # link, and the transform joins the halves back into one token, so
1145
+ # `<-- t -->` is a double arrow and `-. t .->` a dotted one.
1146
+ rule(:inline_label_edge) do
1147
+ inline_solid | inline_thick | inline_dotted
1148
+ end
1149
+
1150
+ rule(:inline_solid) do
1151
+ inline_halves(str("--"), solid_close, str("--"))
1152
+ end
1153
+
1154
+ rule(:inline_thick) do
1155
+ inline_halves(str("=="), thick_close, str("="))
1156
+ end
1157
+
1158
+ # A thick or dotted label cannot carry even one of its own body
1159
+ # characters, `=` or `.`: mmdc refuses `A == a=b ==> B` and
1160
+ # `A -. a.b .-> B`. Only the solid body may hold a single hyphen.
1161
+ rule(:inline_dotted) do
1162
+ inline_halves(str("-."), dotted_close, str("."))
1163
+ end
1164
+
1165
+ # A closing link carries at least one character more than the
1166
+ # opening half, a head or another body character, so `--` alone
1167
+ # never closes.
1168
+ rule(:solid_close) do
1169
+ str("--") >>
1170
+ (match[">xo"] | (str("-").repeat(1) >> match[">xo"].maybe))
1171
+ end
1172
+
1173
+ rule(:thick_close) do
1174
+ str("==") >>
1175
+ (match[">xo"] | (str("=").repeat(1) >> match[">xo"].maybe))
1176
+ end
1177
+
1178
+ rule(:dotted_close) do
1179
+ str("-").maybe >> str(".").repeat(1) >> str("-") >> match[">xo"].maybe
1180
+ end
1181
+
1182
+ # The label runs to the closing link, across lines as mermaid reads
1183
+ # it, and starts on a character that is not whitespace: mermaid
1184
+ # refuses `A -- --> B`. `forbidden` is the body text mermaid cannot
1185
+ # lex inside the label. Whitespace is mermaid's own set, and a whole
1186
+ # comment line inside a multiline label is deleted before it reads.
1187
+ # `%%{` opens a directive, never a comment, and no label holds one.
1188
+ #
1189
+ # A whitespace run is consumed whole and checked once for what
1190
+ # follows it, so a long run costs one pass, not one per character.
1191
+ def inline_halves(open, close, forbidden)
1192
+ blank = inline_blank
1193
+ closing = inline_closing(close)
1194
+ closing_ahead = inline_closing_ahead(forbidden)
1195
+ label = inline_label_body(closing, blank, forbidden)
1196
+ inline_opening(open) >> blank.repeat >> closing_ahead >> label >>
1197
+ blank.repeat >> closing.as(:close)
1198
+ end
1199
+ private :inline_halves
1200
+
1201
+ def inline_blank
1202
+ inline_comment_line | line_space | match["\r\n"]
1203
+ end
1204
+ private :inline_blank
1205
+
1206
+ def inline_closing(close)
1207
+ link_start.maybe >> close
1208
+ end
1209
+ private :inline_closing
1210
+
1211
+ def inline_opening(open)
1212
+ (link_start.maybe >> open).as(:open)
1213
+ end
1214
+ private :inline_opening
1215
+
1216
+ # Avoid caching every failed per-character lookahead when the
1217
+ # closing delimiter is absent altogether.
1218
+ def inline_closing_ahead(forbidden)
1219
+ dynamic do |source|
1220
+ if source.chars_until(forbidden.str) < source.chars_left
1221
+ str("")
1222
+ else
1223
+ forbidden
1224
+ end
1225
+ end
1226
+ end
1227
+ private :inline_closing_ahead
1228
+
1229
+ def inline_label_char(closing, blank, forbidden)
1230
+ char = closing.absent? >> blank.absent? >> str('"').absent? >>
1231
+ str("%%{").absent? >> any
1232
+ forbidden ? forbidden.absent? >> char : char
1233
+ end
1234
+ private :inline_label_char
1235
+
1236
+ def inline_label_content(char, gap)
1237
+ text = char >> (char | gap).repeat
1238
+ (quoted_label >> (char | gap).repeat) | text
1239
+ end
1240
+ private :inline_label_content
1241
+
1242
+ def inline_label_body(closing, blank, forbidden)
1243
+ char = inline_label_char(closing, blank, forbidden)
1244
+ gap = blank.repeat(1) >> closing.absent?
1245
+ inline_label_content(char, gap).as(:label)
1246
+ end
1247
+ private :inline_label_body
1248
+
1249
+ # A quoted run is text whole, whatever it holds: `A -- "a--b" --> B`.
1250
+ # The transform drops the two quotes, as mermaid does. `%%{` still
1251
+ # opens a directive even inside quotes, so it is refused here too.
1252
+ # A whole comment line is skipped first, quote and all: mermaid
1253
+ # deletes it before the quote inside it can end the label early.
1254
+ rule(:quoted_label) do
1255
+ str('"') >>
1256
+ (inline_comment_line | (str("%%{").absent? >> match['^"']))
1257
+ .repeat >> str('"')
1258
+ end
1259
+
1260
+ # A comment line inside a multiline inline label: mermaid deletes
1261
+ # it whole before reading the label. `%%{` opens a directive
1262
+ # rather than a comment and is left alone.
1263
+ rule(:inline_comment_line) do
1264
+ newline >> line_space.repeat >> str("%%") >> str("{").absent? >>
1265
+ (newline.absent? >> any).repeat
1266
+ end
1267
+
1268
+ # Link forms
1269
+ # Every symbol-only link mermaid draws, probed one at a time
1270
+ # against mmdc rather than counted from the docs. The form that
1271
+ # carries its label in the middle — `A -- text --> B` — is a
1272
+ # different shape, read by `inline_label_edge` above.
1273
+ #
1274
+ # `->` and `==` are deliberately absent: etassia accepted both and
1275
+ # mermaid rejects them.
1276
+ # `~` never opens a visible link, so this alternation is
1277
+ # mutually exclusive and its order is free.
1278
+ rule(:arrow) do
1279
+ (invisible_link | visible_link).as(:token)
1280
+ end
1281
+
1282
+ # `~~~` takes no markers at all. mmdc rejects `~~~>` outright, and
1283
+ # it never reads an `o` or an `x` beside a tilde run as a marker
1284
+ # either — `A o~~~o B` is refused, and written flush `Ao~~~oB` is
1285
+ # DRAWN, as the two nodes `Ao` and `oB` with an invisible link
1286
+ # between them. Etassia gives both the same answers.
1287
+ rule(:invisible_link) { str("~~~") >> str("~").repeat }
1288
+
1289
+ # The vocabulary is generated, not listed. Enumerating it missed
1290
+ # forms mmdc renders — `====`, `-.-x`, `<--x`, `o----o` among them
1291
+ # — and got `o--x` wrong on top of that.
1292
+ #
1293
+ # A leading marker is taken here whatever it is; whether mermaid
1294
+ # honours it depends on the marker at the other end, which the
1295
+ # transform decides.
1296
+ #
1297
+ # Headed first: parslet's alternation takes the first branch that
1298
+ # matches, and `long_link` would swallow the `---` of `--->` and
1299
+ # leave the `>` for the target to start with, which nothing can
1300
+ # parse — the whole diagram would be thrown away.
1301
+ rule(:visible_link) { link_start.maybe >> (headed_link | long_link) }
1302
+
1303
+ rule(:link_start) { match["ox<"] }
1304
+ rule(:link_end) { match[">xo"] }
1305
+
1306
+ rule(:headed_link) { link_body >> link_end }
1307
+
1308
+ # The three bodies part on a `-`, an `=`, or a dot no other body
1309
+ # carries — a dotted one may open with the dot itself — so this
1310
+ # alternation is mutually exclusive and its order is free. Only
1311
+ # the one above is load-bearing.
1312
+ rule(:link_body) { solid_body | thick_body | dotted_body }
1313
+
1314
+ rule(:solid_body) { str("--") >> str("-").repeat }
1315
+ rule(:thick_body) { str("==") >> str("=").repeat }
1316
+
1317
+ # The opening hyphen is optional. mmdc draws `.-`, `..->` and
1318
+ # `<.-x` exactly as it draws `-.-`, `-..->` and `<-.-x`, and this
1319
+ # rule refused the whole leading-dot half of the family.
1320
+ rule(:dotted_body) { str("-").maybe >> str(".").repeat(1) >> str("-") }
1321
+
1322
+ # Without a marker the body has to be longer than its minimum:
1323
+ # mmdc draws `---` and `===` and refuses `--` and `==`. A dotted
1324
+ # body carries a dot already, so its own minimum — `.-` — is a
1325
+ # link on its own and it stands here unchanged.
1326
+ rule(:long_link) { long_solid | long_thick | dotted_body }
1327
+ rule(:long_solid) { str("---") >> str("-").repeat }
1328
+ rule(:long_thick) { str("===") >> str("=").repeat }
1329
+
1330
+ # Edge label: can be in pipes |label|
1331
+ rule(:edge_label) do
1332
+ pipe_label
1333
+ end
1334
+
1335
+ # Pipe label: |label|. The pipes are delimiters, not text, so only
1336
+ # the run between them is captured.
1337
+ rule(:pipe_label) do
1338
+ pipe >> (pipe.absent? >> any).repeat(1).as(:pipe_text) >> pipe
1339
+ end
1340
+
1341
+ # A subgraph is NAMED, not built. It takes a quoted string and the
1342
+ # keyword `end`, but not the other reserved words — mmdc draws
1343
+ # `subgraph end [Title]` and `subgraph "AB" [Title]` and refuses
1344
+ # `subgraph default [Title]` and `subgraph _self [Title]`.
1345
+ #
1346
+ # `quoted_run`, not `quoted_string`: the shared rule takes an EMPTY
1347
+ # body and wraps it in its own `.as(:string)`, while mmdc refuses
1348
+ # `subgraph ""` and draws `subgraph " "`.
1349
+ #
1350
+ # An empty pair of quotes names nothing ON ITS OWN but may stand in
1351
+ # FRONT of a name, so `subgraph "" A` and `subgraph ""A` both draw.
1352
+ # Whitespace between the pair and the name is optional, so the name
1353
+ # is taken here rather than left to `subgraph_trailing_name`, which
1354
+ # only starts at a space.
1355
+ rule(:subgraph_id) do
1356
+ subgraph_quoted_name |
1357
+ (str('""') >> space.repeat >> subgraph_name_word) |
1358
+ subgraph_name_word
1359
+ end
1360
+
1361
+ # `quoted_run`'s body, captured. The quotes are not part of the
1362
+ # name: the transform compares ids and builds the title from this
1363
+ # text, so `subgraph "a b"` has to arrive as `a b`. Never empty,
1364
+ # for the same reason `quoted_run` is not.
1365
+ rule(:subgraph_quoted_name) do
1366
+ str('"') >> (str('"').absent? >> any).repeat(1).as(:string) >>
1367
+ str('"')
1368
+ end
1369
+
1370
+ # A name that hunts up an `end` and then ends the line closes the
1371
+ # subgraph on the spot. mermaid lexes `end\b\s*` as its END token,
1372
+ # so `subgraph end` opens nothing and the body's own `end` is left
1373
+ # over. mmdc refuses it, and refuses `subgraph ""end`,
1374
+ # `subgraph 1end` and `subgraph éend` the same way, while
1375
+ # `subgraph Zend`, `subgraph endx` and `subgraph Z#end` draw.
1376
+ #
1377
+ # Anything but whitespace after the name calls it off — a title,
1378
+ # another word, or a `;` — so `subgraph end [T]`, `subgraph end A`
1379
+ # and `subgraph end;` all draw. Whitespace does not save it in any
1380
+ # mixture, which is mermaid's own `end\b\s*` eating the run. The
1381
+ # run is `line_space`, so a no-break space goes with the keyword
1382
+ # the way a plain one does.
1383
+ #
1384
+ # The whole condition goes INSIDE the walk. Hunting the word alone
1385
+ # and testing the line end behind it commits to the FIRST `end` and
1386
+ # PEG never resumes at a later one, so `subgraph A endéend` would be
1387
+ # taken as a name where mmdc refuses it. `subgraph A endéend [T]`
1388
+ # still draws: with a title behind it no `end` ends the line.
1389
+ rule(:bare_subgraph_end) do
1390
+ token_at_lexer_restart(
1391
+ subgraph_end_word >> line_space.repeat >> (newline | eof),
1392
+ )
1393
+ end
1394
+
1395
+ # The `end` guard belongs here because every subgraph word funnels
1396
+ # through this rule — both arms of `subgraph_id` and every
1397
+ # trailing word — so `subgraph end`, `subgraph ""end` and
1398
+ # `subgraph A end` are refused by the same line.
1399
+ #
1400
+ # A subgraph keyword and a link opening end a name the same way
1401
+ # they end a node id, and mermaid looks for both behind every
1402
+ # lexer restart. So `subgraph 1default [T]`, `subgraph #Zédefault
1403
+ # [T]` and `subgraph Zé.- [T]` are refused, while `subgraph #end
1404
+ # [T]` and `subgraph a.- [T]` draw. Without the link guard all
1405
+ # sixteen measured refusals parsed here.
1406
+ rule(:subgraph_name_word) do
1407
+ bare_subgraph_end.absent? >>
1408
+ token_at_lexer_restart(subgraph_keyword).absent? >>
1409
+ dot_run_before_link.absent? >>
1410
+ token_at_lexer_restart(arrowhead_open).absent? >>
1411
+ id_run
1412
+ end
1413
+
1414
+ rule(:subgraph_reserved) do
1415
+ SUBGRAPH_RESERVED.map { |word| str(word) }.reduce(:|) >>
1416
+ word_boundary
1417
+ end
1418
+
1419
+ # All three directive words are reserved here too, and they end a
1420
+ # word the same way they do in a node id — `subgraph click- [T]`
1421
+ # and `subgraph clickx [T]` are ordinary names.
1422
+ #
1423
+ # `click` looked like the exception and is not. mmdc draws
1424
+ # `subgraph click [T]` only while the body holds no link: mermaid
1425
+ # opens its click state on the word and swallows the title, so the
1426
+ # subgraph is anonymous and the next link is a parse error. Probing
1427
+ # with a bare `X` inside hid that; `X --> Y` shows it.
1428
+ rule(:subgraph_keyword) { subgraph_reserved | spaced_keyword }
1429
+
1430
+ # A click target is named, not built, and mermaid is at its most
1431
+ # permissive here: `click default`, `click _self` and `click end`
1432
+ # all render, and so does `click --> "url"`. The target is simply
1433
+ # everything up to the next space, tab or newline. Only a leading
1434
+ # quote is out — mmdc refuses `click "AB" "https://example.com"`
1435
+ # because the quote opens a string instead.
1436
+ #
1437
+ # The terminator is this grammar's own whitespace, not Ruby's `\s`.
1438
+ # `\s` holds a vertical tab and a form feed, and ending the target
1439
+ # on either refused `click A\vB "url"` and `click A\fB "url"`,
1440
+ # which mmdc draws. It stays aligned with the single `space` in
1441
+ # `click_statement` that holds the action off the target.
1442
+ rule(:click_target) do
1443
+ str('"').absent? >> ((space | newline).absent? >> any).repeat(1)
1444
+ end
1445
+
1446
+ # A node id is a bare word, never a quoted run. It is BUILT here,
1447
+ # so every guard applies and malformed ids cannot bypass them.
1448
+ #
1449
+ # Mermaid's node ids are far wider than a programming identifier:
1450
+ # they may lead with a digit (`1-->2`), and carry dots, slashes and
1451
+ # hyphens (`9e122290`, `a.b`, `a/b`, `a-b`).
1452
+ #
1453
+ # A hyphen is only part of the id when an arrow cannot start there,
1454
+ # so `a-b` is one node while `a-->b` stays two.
1455
+ #
1456
+ # Mermaid does not stop hunting for a keyword at the start of an
1457
+ # id, so a reserved word buried in one is still a keyword there,
1458
+ # and the places it looks again are positions, not characters:
1459
+ # `#end` `1end` `éend` are refused, `Z#end` `Z1end` `$end` draw.
1460
+ rule(:node_id) do
1461
+ id_before_xo_link |
1462
+ (token_at_lexer_restart(node_keyword).absent? >>
1463
+ dot_run_before_link.absent? >>
1464
+ arrowhead_dot_dash.absent? >> id_run)
1465
+ end
1466
+
1467
+ # A node id ends before one `x` or `o` when `--`, `==` or `-.`
1468
+ # follows. A doubled `x`/`o`, as in `1xx`, keeps the marker in the
1469
+ # id, and so does a settled character in front of it — `Zx-->B` and
1470
+ # `A1x-->B` are one node and a plain link.
1471
+ #
1472
+ # Where the marker splits the id is a lexer restart, so the walk is
1473
+ # the same one every other guard here runs. mmdc splits `#x-->B`
1474
+ # into `#` and `B`, `&x---B` into `&` and `B`, and `éx-->B`,
1475
+ # `中o===B` and `Zéo==>B` the same way.
1476
+ #
1477
+ # `repeat(1)` rather than `token_at_lexer_restart`'s `repeat`: at
1478
+ # zero restarts the marker stands at the id START, where mermaid
1479
+ # has no node in front of the link at all. `x-->B` is that shape
1480
+ # and it is left exactly where it was.
1481
+ #
1482
+ # The split link now parses too: `#x-->B` draws `#` and `B`
1483
+ # joined by a plain `arrow`, same as mmdc. `Builders::Flowchart
1484
+ # .link_type` is what reads the marker — it honours a leading
1485
+ # `x`/`o` only when the trailing one matches it, so a lone
1486
+ # leading marker like this one falls back to the head it finds
1487
+ # at the other end rather than failing the statement.
1488
+ rule(:id_before_xo_link) do
1489
+ (xo_link_open.absent? >> restart_step).repeat(1) >>
1490
+ xo_link_open.present?
1491
+ end
1492
+
1493
+ # The opening of a link that carries a crossed or circled head.
1494
+ # Mermaid spells the head on both ends of all three links —
1495
+ # `[xo<]?--+[-xo>]`, `[xo<]?==+[=xo>]`, `[xo<]?-?\.+-[xo>]?` — so
1496
+ # the marker leads a dash, an equals or a dotted run alike.
1497
+ #
1498
+ # The dotted one puts its dash BEFORE the dots and makes it
1499
+ # optional, so it opens as `x-.` and as `x.-` both.
1500
+ # `arrowhead_open` already spells the whole opening, markers and
1501
+ # leading dash included, so it is reused rather than respelt.
1502
+ #
1503
+ # A settled character in front kills the opening, which is what
1504
+ # mmdc does too: `Zéax.-B`, `x1.-` and `xx.-` keep their whole id
1505
+ # while `#x.-B` and `éx.-B` are refused.
1506
+ rule(:xo_link_open) do
1507
+ arrowhead_open |
1508
+ (match["xo"] >> (str("--") | str("==") | str("-.")))
1509
+ end
1510
+
1511
+ # A hyphen joins the id unless another dash or a dot follows, which
1512
+ # is where a link starts. `x`, `o` and `>` are NOT excluded,
1513
+ # because mermaid renders `a-o-->B` and `a-x-->B`. `A->B` fails
1514
+ # because the hyphen joins the id here and `>B` cannot continue a
1515
+ # statement — not because `->` is unknown; `A -> B` is refused by
1516
+ # `visible_link`, which spells its shortest solid body `--`.
1517
+ rule(:id_hyphen) { str("-") >> match["-."].absent? }
1518
+
1519
+ rule(:reserved_word) do
1520
+ RESERVED_WORDS.map { |word| str(word) }.reduce(:|) >> word_boundary
1521
+ end
1522
+
1523
+ rule(:node_keyword) { reserved_word | spaced_keyword }
1524
+
1525
+ # An arrowhead opening standing AT one of these places is a link,
1526
+ # and mermaid starts a fresh token behind it — so the walk has to
1527
+ # carry on past it, or it stalls on the `x.-` and never sees the
1528
+ # word behind it. mmdc refuses `#x.-end --- Z` and `1x.-end --- Z`.
1529
+ #
1530
+ # Only at one of the places, though. A settled character in front
1531
+ # still kills it, so `Zéax.-end` and `Zx.-end` stay whole ids,
1532
+ # which is what mmdc draws.
1533
+ rule(:restart_step) do
1534
+ arrowhead_open | (settled_run.maybe >> restart_run)
1535
+ end
1536
+
1537
+ rule(:restart_run) { (id_carry_char | id_restart_char).repeat(1) }
1538
+
1539
+ rule(:settled_run) do
1540
+ settled_char >> (settled_char | id_carry_char).repeat
1541
+ end
1542
+
1543
+ rule(:settled_char) { id_dot | id_hyphen | id_ascii_char }
1544
+
1545
+ # A dot run against a dash is the opening of a dotted link, not a
1546
+ # node: mmdc refuses `.-->B` while `. --> B` is a node called `.`.
1547
+ # An equals sign is a different story — `.==>B` renders as `.` and
1548
+ # `B`, so excluding it here refused a diagram mermaid draws.
1549
+ #
1550
+ # Mermaid finds that opening wherever its lexer restarts, not only
1551
+ # at the id start, so it is hunted the way a keyword is. `1.-->B`
1552
+ # `#.-->B` `Zé.-->B` and `xé.-->B` are all refused, while
1553
+ # `Z1.-->B` `Z#.-->B` and `Zéa.-->B` draw as a node and a link.
1554
+ #
1555
+ # Nothing about what follows changes it. mmdc draws `1.-a --- Z`
1556
+ # as THREE nodes — `1`, `a` and the target — because `.-` opened a
1557
+ # dotted link between the first two. `dotted_body` does now spell
1558
+ # that opening, so the gap is no longer in the vocabulary: it is
1559
+ # this rule, which stops the id at the dot run rather than let it
1560
+ # start a link, and `1.-a --- Z` is still refused where the
1561
+ # spaced `1 .- a --- Z` draws all three. The id stopping short is
1562
+ # better than drawing a node that is not on mermaid's page, so the
1563
+ # refusal stands until the restart itself is modelled.
1564
+ #
1565
+ # `arrowhead_dot_dash` below keeps the id in the same spot but is
1566
+ # the weaker of the two: it treats the opening as a link only when
1567
+ # nothing follows it. What actually stops `#x.-B` is
1568
+ # `id_before_xo_link`.
1569
+ rule(:dot_run_before_link) { token_at_lexer_restart(dotted_link_open) }
1570
+
1571
+ # `arrowhead_open` without its leading marker. Mermaid's dotted
1572
+ # link also carries a trailing `[xo>]`, and it is deliberately NOT
1573
+ # spelled here: the tail is optional, so a rule holding it succeeds
1574
+ # exactly where one without it does, and the only reader
1575
+ # (`dot_run_before_link`) is consumed under `.absent?`, where the
1576
+ # length never matters either.
1577
+ rule(:dotted_link_open) { str(".").repeat(1) >> str("-") }
1578
+
1579
+ # Everywhere else a dot just joins, dash or no dash: mmdc draws
1580
+ # `A.-->B` as `A.` and `B`, and draws `A.-` `A.-B` `A..-->B` and
1581
+ # `y.- --> Z`.
1582
+ rule(:id_dot) { str(".") }
1583
+
1584
+ # Reads mermaid's dotted left-arrowhead opening — the whole of it,
1585
+ # as `arrowhead_open` spells it out below — as a link instead of an
1586
+ # id. Where that opening is legal decides the guard.
1587
+ #
1588
+ # At the id start nothing sits in front of the link, so mermaid
1589
+ # always refuses: `x.-`, `x..-`, `x.-z` and `x.-B` all fail. Behind
1590
+ # a lexer restart a node does sit in front, so mermaid opens a real
1591
+ # link there and reads THREE nodes — `#x.-B --- Z` is `#`, `B` and
1592
+ # `Z`. This rule alone would let the id carry on past the opening;
1593
+ # `id_before_xo_link` is what stops it at the restart.
1594
+ #
1595
+ # A settled character in front kills it either way, so `X.-`,
1596
+ # `x1.-`, `xx.-` and `xo.-` stay ordinary ids.
1597
+ #
1598
+ # The second arm needs no "at least one restart" of its own: at
1599
+ # zero restarts the arm in front already matches everything
1600
+ # `arrowhead_ends_id` could.
1601
+ rule(:arrowhead_dot_dash) do
1602
+ arrowhead_open | token_at_lexer_restart(arrowhead_ends_id)
1603
+ end
1604
+
1605
+ # Copied from the shape of mermaid's own dotted-link rule. In
1606
+ # mmdc 11.12.0's flow lexer that rule is
1607
+ # `/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/`, so the opening carries a
1608
+ # marker on BOTH ends and may put a dash in front of the dots.
1609
+ # Spelling only the middle of it reads `x.-x` as `x.-` plus a stray
1610
+ # `x` and restarts every guard downstream one character early.
1611
+ #
1612
+ # `<` is in mermaid's leading set too and is left out on purpose:
1613
+ # it is not an id character here, so it can never be reached
1614
+ # inside one.
1615
+ rule(:arrowhead_open) do
1616
+ (str("x") | str("o")) >> str("-").maybe >> str(".").repeat(1) >>
1617
+ str("-") >> match["xo>"].maybe
1618
+ end
1619
+
1620
+ # An arrowhead opening with nothing after it that could continue an
1621
+ # id. The crossed and circled links this opens ARE drawn — `x--x`,
1622
+ # `x-.-x` and the rest parse into real edges, see
1623
+ # `Builders::Flowchart#link_type` — so this rule refuses nothing
1624
+ # on its own; it only decides where the id ends. `id_before_xo_link`
1625
+ # above is what actually stops the id at the restart.
1626
+ rule(:arrowhead_ends_id) { arrowhead_open >> id_body.absent? }
1627
+
1628
+ # Mermaid's lexer spells its id charset out, so an id is not
1629
+ # "anything that is not punctuation". It is printable ASCII, or a
1630
+ # letter in the basic plane. Category and plane both matter: `é`,
1631
+ # `中`, `ª` and `A` draw, while `Ⅰ` (category Nl, not a letter),
1632
+ # astral `𝐀`, control characters, combining marks, non-ASCII
1633
+ # digits, braille and box drawing are all refused.
1634
+ #
1635
+ # `\p{L}` is close but not exact, so the letters come from
1636
+ # mermaid's own table instead (`MERMAID_UNICODE_TEXT`). How the
1637
+ # two differ, and why the table is copied rather than approximated,
1638
+ # is written down once beside the table in
1639
+ # `grammars/mermaid_unicode_text.rb`.
1640
+ #
1641
+ # `:` `,` `"` and `%` are printable ASCII and mermaid joins them —
1642
+ # it draws `A:B`, `A,B`, `A"B` and `A%B` as single nodes — but they
1643
+ # are held back on purpose, because each also opens something else
1644
+ # here: an inline class, a declaration list, a quoted label and a
1645
+ # comment. Letting them in needs those boundaries worked out first,
1646
+ # so it is left for its own change. A `;` is NOT one of them:
1647
+ # mermaid separates on it, and `A;B --- Z` draws `A` and then
1648
+ # `B --- Z`. mmdc refuses `( ) < = > @ [ ] ^ { | } ~` outright.
1649
+ #
1650
+ # `-` and `.` are excluded here and handled by their own rules: a
1651
+ # hyphen only joins when a link cannot start there.
1652
+ #
1653
+ # The class is written as three pieces because `restart_step` has
1654
+ # to tell them apart. An id itself takes any of the three.
1655
+ rule(:id_char) { id_ascii_char | id_carry_char | id_restart_char }
1656
+
1657
+ rule(:id_ascii_char) do
1658
+ match['[\\u0021-\\u007E]&&[^;:,"%()<=>@\\[\\]^{|}~.\\-#&*0-9]']
1659
+ end
1660
+
1661
+ # These carry mermaid's keyword hunt along rather than ending it.
1662
+ rule(:id_carry_char) { match["#&*0-9"] }
1663
+
1664
+ # And a letter outside ASCII starts it over.
1665
+ rule(:id_restart_char) { match[MERMAID_UNICODE_TEXT] }
1666
+
1667
+ # An id stops in front of an entity escape, and the guard belongs
1668
+ # to the RUN rather than to the character. `arrowhead_ends_id` also
1669
+ # reads `id_body`, but it asks a different question — whether
1670
+ # anything could continue an id behind a link opening — and there a
1671
+ # `#` still could. Guarding the character would change that rule
1672
+ # too, so the guard sits here.
1673
+ rule(:id_run) { (entity_escape.absent? >> id_body).repeat(1) }
1674
+
1675
+ rule(:id_body) { id_dot | id_hyphen | id_char }
1676
+
1677
+ # Before mermaid lexes anything it rewrites the whole source. The
1678
+ # rewrite that matters here is the last of `encodeEntities`' three,
1679
+ # `r.replace(/#\w+;/g, …)`, which turns `#\w+;` into a placeholder
1680
+ # standing for `&…;`
1681
+ # (`mermaid/dist/chunks/mermaid.esm.min/chunk-7CWYLC5S.mjs`). The
1682
+ # placeholder is spelt in characters no id may hold, so an id that
1683
+ # swallowed the sequence is one mermaid cannot lex.
1684
+ #
1685
+ # NOT every `#\w+;`, though, and this rule does not model the
1686
+ # difference. Two rewrites run FIRST — `/style.*:\S*#.*;/` and
1687
+ # `/classDef.*:\S*#.*;/` — and each strips the final `;` from what
1688
+ # it matches, so mmdc draws `stylex[foo:#bar]-->A#a;` where this
1689
+ # refuses it. Modelling that needs a source pre-pass, which this
1690
+ # grammar has no place for yet.
1691
+ #
1692
+ # The shape is exact: the run must be `[A-Za-z0-9_]` and the `;`
1693
+ # must abut it. mmdc refuses `#a;B`, `#35;B` and `Z#a;B`, and draws
1694
+ # `#;B`, `#a ;B`, `#é;B` and `#a.b;B` — none of which the rewrite
1695
+ # matches.
1696
+ #
1697
+ # Only the id stops here. mermaid applies the rewrite everywhere,
1698
+ # so `A[#a;]` draws a node labelled `&a;`, and rendering that text
1699
+ # belongs to a change that models the escape rather than refuses it.
1700
+ rule(:entity_escape) do
1701
+ str("#") >> match["a-zA-Z0-9_"].repeat(1) >> semicolon
1702
+ end
1703
+
1704
+ # Line terminator. A statement ends at the newline, and NOT at a
1705
+ # `%%` on the way to it: mermaid strips a comment with
1706
+ # `/^\s*%%(?!{)[^\n]+\n?/gm`, anchored to the line start, so a `%%`
1707
+ # with a statement in front of it is never a comment. It is content,
1708
+ # and mmdc reads `A%%c` as one node called `A%%c` and `A;%%c` as the
1709
+ # two nodes `A` and `%%c`.
1710
+ #
1711
+ # So no trailing comment is taken here. mmdc refuses `A %% c`,
1712
+ # `A --> B %% c` and `A[T] %% c` outright.
1713
+ #
1714
+ # `A%%c` is the one under-acceptance left, and closing it means
1715
+ # letting `%` into a node id — a widening this PR does not make.
1716
+ # `A;%% c` is refused by `separator`, which is where the `%%` guard
1717
+ # lives.
1718
+ rule(:line_end) { semicolon.maybe >> space? >> (newline | eof) }
1719
+ end
1720
+ end
1721
+ end
1722
+ end