railroad_diagrams 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/MIT +21 -0
- data/README.md +30 -0
- data/Rakefile +4 -0
- data/exe/railroad_diagrams +7 -0
- data/lib/railroad_diagrams/alternating_sequence.rb +169 -0
- data/lib/railroad_diagrams/choice.rb +209 -0
- data/lib/railroad_diagrams/command.rb +84 -0
- data/lib/railroad_diagrams/comment.rb +48 -0
- data/lib/railroad_diagrams/diagram.rb +107 -0
- data/lib/railroad_diagrams/diagram_item.rb +77 -0
- data/lib/railroad_diagrams/diagram_multi_container.rb +23 -0
- data/lib/railroad_diagrams/end.rb +39 -0
- data/lib/railroad_diagrams/group.rb +75 -0
- data/lib/railroad_diagrams/horizontal_choice.rb +247 -0
- data/lib/railroad_diagrams/multiple_choice.rb +140 -0
- data/lib/railroad_diagrams/non_terminal.rb +67 -0
- data/lib/railroad_diagrams/one_or_more.rb +86 -0
- data/lib/railroad_diagrams/optional.rb +9 -0
- data/lib/railroad_diagrams/optional_sequence.rb +214 -0
- data/lib/railroad_diagrams/path.rb +117 -0
- data/lib/railroad_diagrams/sequence.rb +59 -0
- data/lib/railroad_diagrams/skip.rb +26 -0
- data/lib/railroad_diagrams/stack.rb +120 -0
- data/lib/railroad_diagrams/start.rb +62 -0
- data/lib/railroad_diagrams/style.rb +67 -0
- data/lib/railroad_diagrams/terminal.rb +63 -0
- data/lib/railroad_diagrams/text_diagram.rb +341 -0
- data/lib/railroad_diagrams/version.rb +5 -0
- data/lib/railroad_diagrams/zero_or_more.rb +9 -0
- data/lib/railroad_diagrams.rb +50 -0
- data/sample/sample.html +215 -0
- data/test.rb +570 -0
- metadata +81 -0
@@ -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,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
|