33require "cgi"
44
55module SyntaxTree
6- # This module is responsible for rendering mermaid flow charts.
6+ # This module is responsible for rendering mermaid (https://mermaid.js.org/)
7+ # flow charts.
78 module Mermaid
8- def self . escape ( label )
9- "\" #{ CGI . escapeHTML ( label ) } \" "
9+ # This is the main class that handles rendering a flowchart. It keeps track
10+ # of its nodes and links and renders them according to the mermaid syntax.
11+ class FlowChart
12+ attr_reader :output , :prefix , :nodes , :links
13+
14+ def initialize
15+ @output = StringIO . new
16+ @output . puts ( "flowchart TD" )
17+ @prefix = " "
18+
19+ @nodes = { }
20+ @links = [ ]
21+ end
22+
23+ # Retrieve a node that has already been added to the flowchart by its id.
24+ def fetch ( id )
25+ nodes . fetch ( id )
26+ end
27+
28+ # Add a link to the flowchart between two nodes with an optional label.
29+ def link ( from , to , label = nil , type : :directed , color : nil )
30+ link = Link . new ( from , to , label , type , color )
31+ links << link
32+
33+ output . puts ( "#{ prefix } #{ link . render } " )
34+ link
35+ end
36+
37+ # Add a node to the flowchart with an optional label.
38+ def node ( id , label = " " , shape : :rectangle )
39+ node = Node . new ( id , label , shape )
40+ nodes [ id ] = node
41+
42+ output . puts ( "#{ prefix } #{ nodes [ id ] . render } " )
43+ node
44+ end
45+
46+ # Add a subgraph to the flowchart. Within the given block, all of the
47+ # nodes will be rendered within the subgraph.
48+ def subgraph ( label )
49+ output . puts ( "#{ prefix } subgraph #{ Mermaid . escape ( label ) } " )
50+
51+ previous = prefix
52+ @prefix = "#{ prefix } "
53+
54+ begin
55+ yield
56+ ensure
57+ @prefix = previous
58+ output . puts ( "#{ prefix } end" )
59+ end
60+ end
61+
62+ # Return the rendered flowchart.
63+ def render
64+ links . each_with_index do |link , index |
65+ if link . color
66+ output . puts ( "#{ prefix } linkStyle #{ index } stroke:#{ link . color } " )
67+ end
68+ end
69+
70+ output . string
71+ end
1072 end
1173
74+ # This class represents a link between two nodes in a flowchart. It is not
75+ # meant to be interacted with directly, but rather used as a data structure
76+ # by the FlowChart class.
1277 class Link
13- TYPES = %i[ directed ] . freeze
78+ TYPES = %i[ directed dotted ] . freeze
1479 COLORS = %i[ green red ] . freeze
1580
1681 attr_reader :from , :to , :label , :type , :color
1782
1883 def initialize ( from , to , label , type , color )
19- raise if ! TYPES . include? ( type )
84+ raise unless TYPES . include? ( type )
2085 raise if color && !COLORS . include? ( color )
2186
2287 @from = from
@@ -27,17 +92,31 @@ def initialize(from, to, label, type, color)
2792 end
2893
2994 def render
95+ left_side , right_side , full_side = sides
96+
97+ if label
98+ escaped = Mermaid . escape ( label )
99+ "#{ from . id } #{ left_side } #{ escaped } #{ right_side } #{ to . id } "
100+ else
101+ "#{ from . id } #{ full_side } #{ to . id } "
102+ end
103+ end
104+
105+ private
106+
107+ def sides
30108 case type
31109 when :directed
32- if label
33- "#{ from . id } -- #{ Mermaid . escape ( label ) } --> #{ to . id } "
34- else
35- "#{ from . id } --> #{ to . id } "
36- end
110+ %w[ -- --> --> ]
111+ when :dotted
112+ %w[ -. .-> -.-> ]
37113 end
38114 end
39115 end
40116
117+ # This class represents a node in a flowchart. Unlike the Link class, it can
118+ # be used directly. It is the return value of the #node method, and is meant
119+ # to be passed around to #link methods to create links between nodes.
41120 class Node
42121 SHAPES = %i[ circle rectangle rounded stadium ] . freeze
43122
@@ -61,72 +140,37 @@ def render
61140 def bounds
62141 case shape
63142 when :circle
64- [ "((" , "))" ]
143+ %w[ (( )) ]
65144 when :rectangle
66145 [ "[" , "]" ]
67146 when :rounded
68- [ "(" , ")" ]
147+ %w[ ( ) ]
69148 when :stadium
70149 [ "([" , "])" ]
71150 end
72151 end
73152 end
74153
75- class FlowChart
76- attr_reader :output , :prefix , :nodes , :links
77-
78- def initialize
79- @output = StringIO . new
80- @output . puts ( "flowchart TD" )
81- @prefix = " "
82-
83- @nodes = { }
84- @links = [ ]
85- end
86-
87- def fetch ( id )
88- nodes . fetch ( id )
89- end
90-
91- def link ( from , to , label = nil , type : :directed , color : nil )
92- link = Link . new ( from , to , label , type , color )
93- links << link
94-
95- output . puts ( "#{ prefix } #{ link . render } " )
96- link
154+ class << self
155+ # Escape a label to be used in the mermaid syntax. This is used to escape
156+ # HTML entities such that they render properly within the quotes.
157+ def escape ( label )
158+ "\" #{ CGI . escapeHTML ( label ) } \" "
97159 end
98160
99- def node ( id , label , shape : :rectangle )
100- node = Node . new ( id , label , shape )
101- nodes [ id ] = node
102-
103- output . puts ( "#{ prefix } #{ nodes [ id ] . render } " )
104- node
105- end
106-
107- def subgraph ( label )
108- output . puts ( "#{ prefix } subgraph #{ Mermaid . escape ( label ) } " )
109-
110- previous = prefix
111- @prefix = "#{ prefix } "
112-
113- begin
114- yield
115- ensure
116- @prefix = previous
117- output . puts ( "#{ prefix } end" )
161+ # Create a new flowchart. If a block is given, it will be yielded to and
162+ # the flowchart will be rendered. Otherwise, the flowchart will be
163+ # returned.
164+ def flowchart
165+ flowchart = FlowChart . new
166+
167+ if block_given?
168+ yield flowchart
169+ flowchart . render
170+ else
171+ flowchart
118172 end
119173 end
120-
121- def render
122- links . each_with_index do |link , index |
123- if link . color
124- output . puts ( "#{ prefix } linkStyle #{ index } stroke:#{ link . color } " )
125- end
126- end
127-
128- output . string
129- end
130174 end
131175 end
132176end
0 commit comments