railroad_diagrams 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class MultipleChoice < DiagramMultiContainer
5
+ def initialize(default, type, *items)
6
+ super('g', items)
7
+ raise ArgumentError, "default must be between 0 and #{items.length - 1}" unless (0...items.length).cover?(default)
8
+ raise ArgumentError, "type must be 'any' or 'all'" unless %w[any all].include?(type)
9
+
10
+ @default = default
11
+ @type = type
12
+ @needs_space = true
13
+ @inner_width = @items.map(&:width).max
14
+ @width = 30 + AR + @inner_width + AR + 20
15
+ @up = @items[0].up
16
+ @down = @items[-1].down
17
+ @height = @items[default].height
18
+
19
+ @items.each_with_index do |item, i|
20
+ minimum =
21
+ if [default - 1, default + 1].include?(i)
22
+ 10 + AR
23
+ else
24
+ AR
25
+ end
26
+
27
+ if i < default
28
+ @up += [minimum, item.height + item.down + VS + @items[i + 1].up].max
29
+ elsif i > default
30
+ @down += [minimum, item.up + VS + @items[i - 1].down + @items[i - 1].height].max
31
+ end
32
+ end
33
+
34
+ @down -= @items[default].height # already counted in @height
35
+ end
36
+
37
+ def to_s
38
+ items = @items.map(&:to_s).join(', ')
39
+ "MultipleChoice(#{@default}, #{@type}, #{items})"
40
+ end
41
+
42
+ def format(x, y, width)
43
+ left_gap, right_gap = determine_gaps(width, @width)
44
+
45
+ # Hook up the two sides if self is narrower than its stated width.
46
+ Path.new(x, y).h(left_gap).add(self)
47
+ Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
48
+ x += left_gap
49
+
50
+ default = @items[@default]
51
+
52
+ # Do the elements that curve above
53
+ above = @items[0...@default].reverse
54
+ distance_from_y = 0
55
+ distance_from_y = [10 + AR, default.up + VS + above.first.down + above.first.height].max if above.any?
56
+
57
+ double_enumerate(above).each do |i, ni, item|
58
+ Path.new(x + 30, y).up(distance_from_y - AR).arc('wn').add(self)
59
+ item.format(x + 30 + AR, y - distance_from_y, @inner_width).add(self)
60
+ Path.new(x + 30 + AR + @inner_width, y - distance_from_y + item.height)
61
+ .arc('ne')
62
+ .down(distance_from_y - item.height + default.height - AR - 10)
63
+ .add(self)
64
+ distance_from_y += [AR, item.up + VS + above[i + 1].down + above[i + 1].height].max if ni < -1
65
+ end
66
+
67
+ # Do the straight-line path.
68
+ Path.new(x + 30, y).right(AR).add(self)
69
+ @items[@default].format(x + 30 + AR, y, @inner_width).add(self)
70
+ Path.new(x + 30 + AR + @inner_width, y + @height).right(AR).add(self)
71
+
72
+ # Do the elements that curve below
73
+ below = @items[(@default + 1)..] || []
74
+ distance_from_y = [10 + AR, default.height + default.down + VS + below.first.up].max if below.any?
75
+
76
+ below.each_with_index do |item, i|
77
+ Path.new(x + 30, y).down(distance_from_y - AR).arc('ws').add(self)
78
+ item.format(x + 30 + AR, y + distance_from_y, @inner_width).add(self)
79
+ Path.new(x + 30 + AR + @inner_width, y + distance_from_y + item.height)
80
+ .arc('se')
81
+ .up(distance_from_y - AR + item.height - default.height - 10)
82
+ .add(self)
83
+
84
+ distance_from_y += [AR, item.height + item.down + VS + (below[i + 1]&.up || 0)].max
85
+ end
86
+
87
+ text = DiagramItem.new('g', attrs: { 'class' => 'diagram-text' }).add(self)
88
+ DiagramItem.new(
89
+ 'title',
90
+ text: @type == 'any' ? 'take one or more branches, once each, in any order' : 'take all branches, once each, in any order'
91
+ ).add(text)
92
+
93
+ DiagramItem.new(
94
+ 'path',
95
+ attrs: {
96
+ 'd' => "M #{x + 30} #{y - 10} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z",
97
+ 'class' => 'diagram-text'
98
+ }
99
+ ).add(text)
100
+
101
+ DiagramItem.new(
102
+ 'text',
103
+ text: @type == 'any' ? '1+' : 'all',
104
+ attrs: { 'x' => x + 15, 'y' => y + 4, 'class' => 'diagram-text' }
105
+ ).add(text)
106
+
107
+ DiagramItem.new(
108
+ 'path',
109
+ attrs: {
110
+ 'd' => "M #{x + @width - 20} #{y - 10} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z",
111
+ 'class' => 'diagram-text'
112
+ }
113
+ ).add(text)
114
+
115
+ DiagramItem.new(
116
+ 'text',
117
+ text: '↺',
118
+ attrs: { 'x' => x + @width - 10, 'y' => y + 4, 'class' => 'diagram-arrow' }
119
+ ).add(text)
120
+
121
+ self
122
+ end
123
+
124
+ def text_diagram
125
+ multi_repeat = TextDiagram.get_parts(['multi_repeat']).first
126
+ any_all = TextDiagram.rect(@type == 'any' ? '1+' : 'all')
127
+ diagram_td = Choice.text_diagram(self)
128
+ repeat_td = TextDiagram.rect(multi_repeat)
129
+ diagram_td = any_all.append_right(diagram_td, '')
130
+ diagram_td.append_right(repeat_td, '')
131
+ end
132
+
133
+ private
134
+
135
+ def double_enumerate(seq)
136
+ length = seq.length
137
+ seq.each_with_index.map { |item, i| [i, i - length, item] }
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class NonTerminal < DiagramItem
5
+ def initialize(text, href = nil, title = nil, cls: '')
6
+ super('g', attrs: { 'class' => "non-terminal #{cls}" })
7
+ @text = text
8
+ @href = href
9
+ @title = title
10
+ @cls = cls
11
+ @width = (text.length * CHAR_WIDTH) + 20
12
+ @up = 11
13
+ @down = 11
14
+ @needs_space = true
15
+ end
16
+
17
+ def to_s
18
+ "NonTerminal(#{@text}, href=#{@href}, title=#{@title}, cls=#{@cls})"
19
+ end
20
+
21
+ def format(x, y, width)
22
+ left_gap, right_gap = determine_gaps(width, @width)
23
+
24
+ # Hook up the two sides if self is narrower than its stated width.
25
+ Path.new(x, y).h(left_gap).add(self)
26
+ Path.new(x + left_gap + @width, y).h(right_gap).add(self)
27
+
28
+ DiagramItem.new(
29
+ 'rect',
30
+ attrs: {
31
+ 'x' => x + left_gap,
32
+ 'y' => y - 11,
33
+ 'width' => @width,
34
+ 'height' => @up + @down
35
+ }
36
+ ).add(self)
37
+
38
+ text = DiagramItem.new(
39
+ 'text',
40
+ attrs: {
41
+ 'x' => x + left_gap + (@width / 2),
42
+ 'y' => y + 4
43
+ },
44
+ text: @text
45
+ )
46
+ if @href
47
+ a = DiagramItem.new(
48
+ 'a',
49
+ attrs: {
50
+ 'xlink:href' => @href
51
+ },
52
+ text:
53
+ ).add(self)
54
+ text.add(a)
55
+ else
56
+ text.add(self)
57
+ end
58
+ DiagramItem.new('title', attrs: {}, text: @title).add(self) if @title
59
+ self
60
+ end
61
+
62
+ def text_diagram
63
+ # NOTE: href, title, and cls are ignored for text diagrams.
64
+ TextDiagram.rect(@text)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class OneOrMore < DiagramItem
5
+ def initialize(item, repeat = nil)
6
+ super('g')
7
+ @item = wrap_string(item)
8
+ repeat ||= Skip.new
9
+ @rep = wrap_string(repeat)
10
+ @width = [@item.width, @rep.width].max + (AR * 2)
11
+ @height = @item.height
12
+ @up = @item.up
13
+ @down = [AR * 2, @item.down + VS + @rep.up + @rep.height + @rep.down].max
14
+ @needs_space = true
15
+ end
16
+
17
+ def to_s
18
+ "OneOrMore(#{@item}, repeat=#{@rep})"
19
+ end
20
+
21
+ def format(x, y, width)
22
+ left_gap, right_gap = determine_gaps(width, @width)
23
+
24
+ # Hook up the two sides if self is narrower than its stated width.
25
+ Path.new(x, y).h(left_gap).add(self)
26
+ Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
27
+ x += left_gap
28
+
29
+ # Draw item
30
+ Path.new(x, y).right(AR).add(self)
31
+ @item.format(x + AR, y, @width - (AR * 2)).add(self)
32
+ Path.new(x + @width - AR, y + @height).right(AR).add(self)
33
+
34
+ # Draw repeat arc
35
+ distance_from_y = [AR * 2, @item.height + @item.down + VS + @rep.up].max
36
+ Path.new(x + AR, y).arc('nw').down(distance_from_y - (AR * 2)).arc('ws').add(self)
37
+ @rep.format(x + AR, y + distance_from_y, @width - (AR * 2)).add(self)
38
+ Path.new(x + @width - AR, y + distance_from_y + @rep.height)
39
+ .arc('se')
40
+ .up(distance_from_y - (AR * 2) + @rep.height - @item.height)
41
+ .arc('en')
42
+ .add(self)
43
+
44
+ self
45
+ end
46
+
47
+ def text_diagram
48
+ parts = TextDiagram.get_parts(
49
+ %w[
50
+ line repeat_top_left repeat_left repeat_bot_left repeat_top_right repeat_right repeat_bot_right
51
+ ]
52
+ )
53
+ line, repeat_top_left, repeat_left, repeat_bot_left, repeat_top_right, repeat_right, repeat_bot_right = parts
54
+
55
+ # Format the item and then format the repeat append it to the bottom, after a spacer.
56
+ item_td = @item.text_diagram
57
+ repeat_td = @rep.text_diagram
58
+ fir_width = TextDiagram._max_width(item_td, repeat_td)
59
+ repeat_td = repeat_td.expand(0, fir_width - repeat_td.width, 0, 0)
60
+ item_td = item_td.expand(0, fir_width - item_td.width, 0, 0)
61
+ item_and_repeat_td = item_td.append_below(repeat_td, [])
62
+
63
+ # Build the left side of the repeat line and append the combined item and repeat to its right.
64
+ left_lines = []
65
+ left_lines << (repeat_top_left + line)
66
+ left_lines += [repeat_left + ' '] * ((item_td.height - item_td.entry) + repeat_td.entry - 1)
67
+ left_lines << (repeat_bot_left + line)
68
+ left_td = TextDiagram.new(0, 0, left_lines)
69
+ left_td = left_td.append_right(item_and_repeat_td, '')
70
+
71
+ # Build the right side of the repeat line and append it to the combined left side, item, and repeat's right.
72
+ right_lines = []
73
+ right_lines << (line + repeat_top_right)
74
+ right_lines += [' ' + repeat_right] * ((item_td.height - item_td.exit) + repeat_td.exit - 1)
75
+ right_lines << (line + repeat_bot_right)
76
+ right_td = TextDiagram.new(0, 0, right_lines)
77
+ left_td.append_right(right_td, '')
78
+ end
79
+
80
+ def walk(callback)
81
+ callback.call(self)
82
+ @item.walk(callback)
83
+ @rep.walk(callback)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class Optional < DiagramMultiContainer
5
+ def self.new(item, skip = false)
6
+ Choice.new(skip ? 0 : 1, Skip.new, item)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class OptionalSequence < DiagramMultiContainer
5
+ def self.new(*items)
6
+ return Sequence.new(*items) if items.size <= 1
7
+
8
+ super
9
+ end
10
+
11
+ def initialize(*items)
12
+ super('g', items)
13
+ @needs_space = false
14
+ @width = 0
15
+ @up = 0
16
+ @height = @items.sum(&:height)
17
+ @down = @items.first.down
18
+
19
+ height_so_far = 0.0
20
+
21
+ @items.each_with_index do |item, i|
22
+ @up = [@up, [AR * 2, item.up + VS].max - height_so_far].max
23
+ height_so_far += item.height
24
+
25
+ if i.positive?
26
+ @down = [
27
+ @height + @down,
28
+ height_so_far + [AR * 2, item.down + VS].max
29
+ ].max - @height
30
+ end
31
+
32
+ item_width = item.width + (item.needs_space ? 10 : 0)
33
+ @width += if i.zero?
34
+ AR + [item_width, AR].max
35
+ else
36
+ (AR * 2) + [item_width, AR].max + AR
37
+ end
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ items = @items.map(&:to_s).join(', ')
43
+ "OptionalSequence(#{items})"
44
+ end
45
+
46
+ def format(x, y, width)
47
+ left_gap, right_gap = determine_gaps(width, @width)
48
+ Path.new(x, y).right(left_gap).add(self)
49
+ Path.new(x + left_gap + @width, y + @height).right(right_gap).add(self)
50
+ x += left_gap
51
+ upper_line_y = y - @up
52
+ last = @items.size - 1
53
+
54
+ @items.each_with_index do |item, i|
55
+ item_space = item.needs_space ? 10 : 0
56
+ item_width = item.width + item_space
57
+
58
+ if i.zero?
59
+ # Upper skip
60
+ Path.new(x, y)
61
+ .arc('se')
62
+ .up(y - upper_line_y - (AR * 2))
63
+ .arc('wn')
64
+ .right(item_width - AR)
65
+ .arc('ne')
66
+ .down(y + item.height - upper_line_y - (AR * 2))
67
+ .arc('ws')
68
+ .add(self)
69
+
70
+ # Straight line
71
+ Path.new(x, y).right(item_space + AR).add(self)
72
+ item.format(x + item_space + AR, y, item.width).add(self)
73
+ x += item_width + AR
74
+ y += item.height
75
+ elsif i < last
76
+ # Upper skip
77
+ Path.new(x, upper_line_y)
78
+ .right((AR * 2) + [item_width, AR].max + AR)
79
+ .arc('ne')
80
+ .down(y - upper_line_y + item.height - (AR * 2))
81
+ .arc('ws')
82
+ .add(self)
83
+
84
+ # Straight line
85
+ Path.new(x, y).right(AR * 2).add(self)
86
+ item.format(x + (AR * 2), y, item.width).add(self)
87
+ Path.new(x + item.width + (AR * 2), y + item.height)
88
+ .right(item_space + AR)
89
+ .add(self)
90
+
91
+ # Lower skip
92
+ Path.new(x, y)
93
+ .arc('ne')
94
+ .down(item.height + [item.down + VS, AR * 2].max - (AR * 2))
95
+ .arc('ws')
96
+ .right(item_width - AR)
97
+ .arc('se')
98
+ .up(item.down + VS - (AR * 2))
99
+ .arc('wn')
100
+ .add(self)
101
+
102
+ x += (AR * 2) + [item_width, AR].max + AR
103
+ y += item.height
104
+ else
105
+ # Straight line
106
+ Path.new(x, y).right(AR * 2).add(self)
107
+ item.format(x + (AR * 2), y, item.width).add(self)
108
+ Path.new(x + (AR * 2) + item.width, y + item.height)
109
+ .right(item_space + AR)
110
+ .add(self)
111
+
112
+ # Lower skip
113
+ Path.new(x, y)
114
+ .arc('ne')
115
+ .down(item.height + [item.down + VS, AR * 2].max - (AR * 2))
116
+ .arc('ws')
117
+ .right(item_width - AR)
118
+ .arc('se')
119
+ .up(item.down + VS - (AR * 2))
120
+ .arc('wn')
121
+ .add(self)
122
+ end
123
+ end
124
+ self
125
+ end
126
+
127
+ def text_diagram
128
+ line, line_vertical, roundcorner_bot_left, roundcorner_bot_right,
129
+ roundcorner_top_left, roundcorner_top_right = TextDiagram.get_parts(
130
+ %w[line line_vertical roundcorner_bot_left roundcorner_bot_right roundcorner_top_left roundcorner_top_right]
131
+ )
132
+
133
+ # Format all the child items, so we can know the maximum entry.
134
+ item_tds = @items.map(&:text_diagram)
135
+
136
+ # diagramEntry: distance from top to lowest entry, aka distance from top to diagram entry, aka final diagram entry and exit.
137
+ diagram_entry = item_tds.map(&:entry).max
138
+ # SOILHeight: distance from top to lowest entry before rightmost item, aka distance from skip-over-items line to rightmost entry, aka SOIL height.
139
+ soil_height = item_tds[0...-1].map(&:entry).max
140
+ # topToSOIL: distance from top to skip-over-items line.
141
+ top_to_soil = diagram_entry - soil_height
142
+
143
+ # The diagram starts with a line from its entry up to the skip-over-items line:
144
+ lines = [' '] * top_to_soil
145
+ lines += [roundcorner_top_left + line]
146
+ lines += ["#{line_vertical} "] * soil_height
147
+ lines += [roundcorner_bot_right + line]
148
+ diagram_td = TextDiagram.new(lines.size - 1, lines.size - 1, lines)
149
+
150
+ @items.each_with_index do |item_td, i|
151
+ if i.positive?
152
+ # All items except the leftmost start with a line from their entry down to their skip-under-item line,
153
+ # with a joining-line across at the skip-over-items line:
154
+ lines = ([' '] * top_to_soil) + [line * 2] +
155
+ ([' '] * (diagram_td.exit - top_to_soil - 1)) +
156
+ [line + roundcorner_top_right] +
157
+ ([" #{line_vertical}"] * (item_td.height - item_td.entry - 1)) +
158
+ [" #{roundcorner_bot_left}"]
159
+
160
+ skip_down_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
161
+ diagram_td = diagram_td.append_right(skip_down_td, '')
162
+
163
+ # All items except the leftmost next have a line from skip-over-items line down to their entry,
164
+ # with joining-lines at their entry and at their skip-under-item line:
165
+ lines = ([' '] * top_to_soil) + [line + roundcorner_top_right +
166
+ # All such items except the rightmost also have a continuation of the skip-over-items line:
167
+ (i < item_tds.size - 1 ? line : ' ')] +
168
+ ([" #{line_vertical} "] * (diagram_td.exit - top_to_soil - 1)) +
169
+ [line + roundcorner_bot_left + line] +
170
+ ([' ' * 3] * (item_td.height - item_td.entry - 1)) +
171
+ [line * 3]
172
+
173
+ entry_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
174
+ diagram_td = diagram_td.append_right(entry_td, '')
175
+ end
176
+
177
+ part_td = TextDiagram.new(0, 0, [])
178
+ if i < item_tds.size - 1
179
+ # All items except the rightmost have a segment of the skip-over-items line at the top,
180
+ # followed by enough blank lines to push their entry down to the previous item's exit:
181
+ lines = [line * item_td.width] + ([' ' * item_td.width] * (soil_height - item_td.entry))
182
+ soil_segment = TextDiagram.new(0, 0, lines)
183
+ part_td = part_td.append_below(soil_segment, [])
184
+ end
185
+
186
+ part_td = part_td.append_below(item_td, [], move_entry: true, move_exit: true)
187
+
188
+ if i.positive?
189
+ # All items except the leftmost have their skip-under-item line at the bottom.
190
+ soil_segment = TextDiagram.new(0, 0, [line * item_td.width])
191
+ part_td = part_td.append_below(soil_segment, [])
192
+ end
193
+
194
+ diagram_td = diagram_td.append_right(part_td, '')
195
+
196
+ next unless i.positive?
197
+
198
+ # All items except the leftmost have a line from their skip-under-item line to their exit:
199
+ lines = ([' '] * top_to_soil) +
200
+ # All such items except the rightmost also have a joining-line across at the skip-over-items line:
201
+ [(i < item_tds.size - 1 ? line * 2 : ' ')] +
202
+ ([' '] * (diagram_td.exit - top_to_soil - 1)) +
203
+ [line + roundcorner_top_left] +
204
+ ([" #{line_vertical}"] * (part_td.height - part_td.exit - 2)) +
205
+ [line + roundcorner_bot_right]
206
+
207
+ skip_up_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
208
+ diagram_td = diagram_td.append_right(skip_up_td, '')
209
+ end
210
+
211
+ diagram_td
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class Path
5
+ attr_reader :x, :y, :attrs
6
+
7
+ def initialize(x, y)
8
+ @x = x
9
+ @y = y
10
+ @attrs = { 'd' => "M#{x} #{y}" }
11
+ end
12
+
13
+ def m(x, y)
14
+ @attrs['d'] += "m#{x} #{y}"
15
+ self
16
+ end
17
+
18
+ def l(x, y)
19
+ @attrs['d'] += "l#{x} #{y}"
20
+ self
21
+ end
22
+
23
+ def h(val)
24
+ @attrs['d'] += "h#{val}"
25
+ self
26
+ end
27
+
28
+ def right(val)
29
+ h([0, val].max)
30
+ end
31
+
32
+ def left(val)
33
+ h(-[0, val].max)
34
+ end
35
+
36
+ def v(val)
37
+ @attrs['d'] += "v#{val}"
38
+ self
39
+ end
40
+
41
+ def down(val)
42
+ v([0, val].max)
43
+ end
44
+
45
+ def up(val)
46
+ v(-[0, val].max)
47
+ end
48
+
49
+ def arc_8(start, dir)
50
+ arc = AR
51
+ s2 = 1 / Math.sqrt(2) * arc
52
+ s2inv = arc - s2
53
+ sweep = dir == 'cw' ? '1' : '0'
54
+ path = "a #{arc} #{arc} 0 0 #{sweep} "
55
+
56
+ sd = start + dir
57
+ offset = case sd
58
+ when 'ncw' then [s2, s2inv]
59
+ when 'necw' then [s2inv, s2]
60
+ when 'ecw' then [-s2inv, s2]
61
+ when 'secw' then [-s2, s2inv]
62
+ when 'scw' then [-s2, -s2inv]
63
+ when 'swcw' then [-s2inv, -s2]
64
+ when 'wcw' then [s2inv, -s2]
65
+ when 'nwcw' then [s2, -s2inv]
66
+ when 'nccw' then [-s2, s2inv]
67
+ when 'nwccw' then [-s2inv, s2]
68
+ when 'wccw' then [s2inv, s2]
69
+ when 'swccw' then [s2, s2inv]
70
+ when 'sccw' then [s2, -s2inv]
71
+ when 'seccw' then [s2inv, -s2]
72
+ when 'eccw' then [-s2inv, -s2]
73
+ when 'neccw' then [-s2, -s2inv]
74
+ end
75
+
76
+ path += offset.map(&:to_s).join(' ')
77
+ @attrs['d'] += path
78
+ self
79
+ end
80
+
81
+ def arc(sweep)
82
+ x = AR
83
+ y = AR
84
+ x *= -1 if sweep[0] == 'e' || sweep[1] == 'w'
85
+ y *= -1 if sweep[0] == 's' || sweep[1] == 'n'
86
+ cw = %w[ne es sw wn].include?(sweep) ? 1 : 0
87
+ @attrs['d'] += "a#{AR} #{AR} 0 0 #{cw} #{x} #{y}"
88
+ self
89
+ end
90
+
91
+ def add(parent)
92
+ parent.children << self
93
+ self
94
+ end
95
+
96
+ def write_svg(write)
97
+ write.call('<path')
98
+ @attrs.sort.each do |name, value|
99
+ write.call(" #{name}=\"#{RailroadDiagrams.escape_attr(value)}\"")
100
+ end
101
+ write.call(' />')
102
+ end
103
+
104
+ def format
105
+ @attrs['d'] += 'h.5'
106
+ self
107
+ end
108
+
109
+ def text_diagram
110
+ TextDiagram.new(0, 0, [])
111
+ end
112
+
113
+ def to_s
114
+ "Path(#{@x.inspect}, #{@y.inspect})"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailroadDiagrams
4
+ class Sequence < DiagramMultiContainer
5
+ def initialize(*items)
6
+ super('g', items)
7
+ @needs_space = false
8
+ @up = 0
9
+ @down = 0
10
+ @height = 0
11
+ @width = 0
12
+ @items.each do |item|
13
+ @width += item.width + (item.needs_space ? 20 : 0)
14
+ @up = [@up, item.up - @height].max
15
+ @height += item.height
16
+ @down = [@down - item.height, item.down].max
17
+ end
18
+ @width -= 10 if @items[0].needs_space
19
+ @width -= 10 if @items[-1].needs_space
20
+ end
21
+
22
+ def to_s
23
+ items = @items.map(&:to_s).join(', ')
24
+ "Sequence(#{items})"
25
+ end
26
+
27
+ def format(x, y, width)
28
+ left_gap, right_gap = determine_gaps(width, @width)
29
+ Path.new(x, y).h(left_gap).add(self)
30
+ Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
31
+ x += left_gap
32
+ @items.each_with_index do |item, i|
33
+ if item.needs_space && i.positive?
34
+ Path.new(x, y).h(10).add(self)
35
+ x += 10
36
+ end
37
+ item.format(x, y, item.width).add(self)
38
+ x += item.width
39
+ y += item.height
40
+ if item.needs_space && i < @items.length - 1
41
+ Path.new(x, y).h(10).add(self)
42
+ x += 10
43
+ end
44
+ end
45
+ self
46
+ end
47
+
48
+ def text_diagram
49
+ separator, = TextDiagram.get_parts(['separator'])
50
+ diagram_td = TextDiagram.new(0, 0, [''])
51
+ @items.each do |item|
52
+ item_td = item.text_diagram
53
+ item_td = item_td.expand(1, 1, 0, 0) if item.needs_space
54
+ diagram_td = diagram_td.append_right(item_td, separator)
55
+ end
56
+ diagram_td
57
+ end
58
+ end
59
+ end