railroad_diagrams 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.
- 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
|