-
-
Notifications
You must be signed in to change notification settings - Fork 54
/
mermaid.rb
177 lines (145 loc) · 4.42 KB
/
mermaid.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# frozen_string_literal: true
require "cgi"
require "stringio"
module SyntaxTree
# This module is responsible for rendering mermaid (https://mermaid.js.org/)
# flow charts.
module Mermaid
# This is the main class that handles rendering a flowchart. It keeps track
# of its nodes and links and renders them according to the mermaid syntax.
class FlowChart
attr_reader :output, :prefix, :nodes, :links
def initialize
@output = StringIO.new
@output.puts("flowchart TD")
@prefix = " "
@nodes = {}
@links = []
end
# Retrieve a node that has already been added to the flowchart by its id.
def fetch(id)
nodes.fetch(id)
end
# Add a link to the flowchart between two nodes with an optional label.
def link(from, to, label = nil, type: :directed, color: nil)
link = Link.new(from, to, label, type, color)
links << link
output.puts("#{prefix}#{link.render}")
link
end
# Add a node to the flowchart with an optional label.
def node(id, label = " ", shape: :rectangle)
node = Node.new(id, label, shape)
nodes[id] = node
output.puts("#{prefix}#{nodes[id].render}")
node
end
# Add a subgraph to the flowchart. Within the given block, all of the
# nodes will be rendered within the subgraph.
def subgraph(label)
output.puts("#{prefix}subgraph #{Mermaid.escape(label)}")
previous = prefix
@prefix = "#{prefix} "
begin
yield
ensure
@prefix = previous
output.puts("#{prefix}end")
end
end
# Return the rendered flowchart.
def render
links.each_with_index do |link, index|
if link.color
output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}")
end
end
output.string
end
end
# This class represents a link between two nodes in a flowchart. It is not
# meant to be interacted with directly, but rather used as a data structure
# by the FlowChart class.
class Link
TYPES = %i[directed dotted].freeze
COLORS = %i[green red].freeze
attr_reader :from, :to, :label, :type, :color
def initialize(from, to, label, type, color)
raise unless TYPES.include?(type)
raise if color && !COLORS.include?(color)
@from = from
@to = to
@label = label
@type = type
@color = color
end
def render
left_side, right_side, full_side = sides
if label
escaped = Mermaid.escape(label)
"#{from.id} #{left_side} #{escaped} #{right_side} #{to.id}"
else
"#{from.id} #{full_side} #{to.id}"
end
end
private
def sides
case type
when :directed
%w[-- --> -->]
when :dotted
%w[-. .-> -.->]
end
end
end
# This class represents a node in a flowchart. Unlike the Link class, it can
# be used directly. It is the return value of the #node method, and is meant
# to be passed around to #link methods to create links between nodes.
class Node
SHAPES = %i[circle rectangle rounded stadium].freeze
attr_reader :id, :label, :shape
def initialize(id, label, shape)
raise unless SHAPES.include?(shape)
@id = id
@label = label
@shape = shape
end
def render
left_bound, right_bound = bounds
"#{id}#{left_bound}#{Mermaid.escape(label)}#{right_bound}"
end
private
def bounds
case shape
when :circle
%w[(( ))]
when :rectangle
["[", "]"]
when :rounded
%w[( )]
when :stadium
["([", "])"]
end
end
end
class << self
# Escape a label to be used in the mermaid syntax. This is used to escape
# HTML entities such that they render properly within the quotes.
def escape(label)
"\"#{CGI.escapeHTML(label)}\""
end
# Create a new flowchart. If a block is given, it will be yielded to and
# the flowchart will be rendered. Otherwise, the flowchart will be
# returned.
def flowchart
flowchart = FlowChart.new
if block_given?
yield flowchart
flowchart.render
else
flowchart
end
end
end
end
end