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,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Skip < DiagramItem
|
5
|
+
def initialize
|
6
|
+
super('g')
|
7
|
+
@width = 0
|
8
|
+
@up = 0
|
9
|
+
@down = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
'Skip()'
|
14
|
+
end
|
15
|
+
|
16
|
+
def format(x, y, width)
|
17
|
+
Path.new(x, y).right(width).add(self)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def text_diagram
|
22
|
+
line, = TextDiagram.get_parts(['line'])
|
23
|
+
TextDiagram.new(0, 0, [line])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Stack < DiagramMultiContainer
|
5
|
+
def initialize(*items)
|
6
|
+
super('g', items)
|
7
|
+
@need_space = false
|
8
|
+
@width = @items.map { |item| item.width + (item.needs_space ? 20 : 0) }.max
|
9
|
+
|
10
|
+
# pretty sure that space calc is totes wrong
|
11
|
+
@width += AR * 2 if @items.size > 1
|
12
|
+
|
13
|
+
@up = @items.first.up
|
14
|
+
@down = @items.last.down
|
15
|
+
@height = 0
|
16
|
+
last = @items.size - 1
|
17
|
+
|
18
|
+
@items.each_with_index do |item, i|
|
19
|
+
@height += item.height
|
20
|
+
@height += [AR * 2, item.up + VS].max if i.positive?
|
21
|
+
@height += [AR * 2, item.down + VS].max if i < last
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
items = @items.map(&:to_s).join(', ')
|
27
|
+
"Stack(#{items})"
|
28
|
+
end
|
29
|
+
|
30
|
+
def format(x, y, width)
|
31
|
+
left_gap, right_gap = determine_gaps(width, @width)
|
32
|
+
Path.new(x, y).h(left_gap).add(self)
|
33
|
+
x += left_gap
|
34
|
+
x_initial = x
|
35
|
+
if @items.size > 1
|
36
|
+
Path.new(x, y).h(AR).add(self)
|
37
|
+
x += AR
|
38
|
+
inner_width = @width - (AR * 2)
|
39
|
+
else
|
40
|
+
inner_width = @width
|
41
|
+
end
|
42
|
+
|
43
|
+
@items.each_with_index do |item, i|
|
44
|
+
item.format(x, y, inner_width).add(self)
|
45
|
+
x += inner_width
|
46
|
+
y += item.height
|
47
|
+
next unless i != @items.size - 1
|
48
|
+
|
49
|
+
Path.new(x, y)
|
50
|
+
.arc('ne')
|
51
|
+
.down([0, item.down + VS - (AR * 2)].max)
|
52
|
+
.arc('es')
|
53
|
+
.left(inner_width)
|
54
|
+
.arc('nw')
|
55
|
+
.down([0, @items[i + 1].up + VS - (AR * 2)].max)
|
56
|
+
.arc('ws')
|
57
|
+
.add(self)
|
58
|
+
y += [item.down + VS, AR * 2].max + [@items[i + 1].up + VS, AR * 2].max
|
59
|
+
x = x_initial + AR
|
60
|
+
end
|
61
|
+
if @items.size > 1
|
62
|
+
Path.new(x, y).h(AR).add(self)
|
63
|
+
x += AR
|
64
|
+
end
|
65
|
+
Path.new(x, y).h(right_gap).add(self)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def text_diagram
|
70
|
+
corner_bot_left, corner_bot_right, corner_top_left, corner_top_right, line, line_vertical = TextDiagram.get_parts(
|
71
|
+
%w[corner_bot_left corner_bot_right corner_top_left corner_top_right line line_vertical]
|
72
|
+
)
|
73
|
+
|
74
|
+
# Format all the child items, so we can know the maximum width.
|
75
|
+
item_tds = @items.map(&:text_diagram)
|
76
|
+
max_width = item_tds.map(&:width).max
|
77
|
+
left_lines = []
|
78
|
+
right_lines = []
|
79
|
+
separator_td = TextDiagram.new(0, 0, [line * max_width])
|
80
|
+
diagram_td = nil # Top item will replace it.
|
81
|
+
item_tds.each_with_index do |item_td, item_num|
|
82
|
+
if item_num.zero?
|
83
|
+
# The top item enters directly from its left.
|
84
|
+
left_lines += [line * 2]
|
85
|
+
left_lines += [' ' * 2] * (item_td.height - item_td.entry - 1)
|
86
|
+
else
|
87
|
+
# All items below the top enter from a snake-line from the previous item's exit.
|
88
|
+
# Here, we resume that line, already having descended from above on the right.
|
89
|
+
diagram_td = diagram_td.append_below(separator_td, [])
|
90
|
+
left_lines += [corner_top_left + line]
|
91
|
+
left_lines += ["#{line_vertical} "] * item_td.entry
|
92
|
+
left_lines += [corner_bot_left + line]
|
93
|
+
left_lines += [' ' * 2] * (item_td.height - item_td.entry - 1)
|
94
|
+
right_lines += [' ' * 2] * item_td.exit
|
95
|
+
end
|
96
|
+
if item_num < item_tds.size - 1
|
97
|
+
# All items above the bottom exit via a snake-line to the next item's entry.
|
98
|
+
# Here, we start that line on the right.
|
99
|
+
right_lines += [line + corner_top_right]
|
100
|
+
right_lines += [" #{line_vertical}"] * (item_td.height - item_td.exit - 1)
|
101
|
+
right_lines += [line + corner_bot_right]
|
102
|
+
else
|
103
|
+
# The bottom item exits directly to its right.
|
104
|
+
right_lines += [line * 2]
|
105
|
+
end
|
106
|
+
left_pad, right_pad = TextDiagram._gaps(max_width, item_td.width)
|
107
|
+
item_td = item_td.expand(left_pad, right_pad, 0, 0)
|
108
|
+
diagram_td = if item_num.zero?
|
109
|
+
item_td
|
110
|
+
else
|
111
|
+
diagram_td.append_below(item_td, [])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
left_td = TextDiagram.new(0, 0, left_lines)
|
115
|
+
diagram_td = left_td.append_right(diagram_td, '')
|
116
|
+
right_td = TextDiagram.new(0, right_lines.size - 1, right_lines)
|
117
|
+
diagram_td.append_right(right_td, '')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Start < DiagramItem
|
5
|
+
def initialize(type = 'simple', label: nil)
|
6
|
+
super('g')
|
7
|
+
@width =
|
8
|
+
if label
|
9
|
+
[20, (label.length * CHAR_WIDTH) + 10].max
|
10
|
+
else
|
11
|
+
20
|
12
|
+
end
|
13
|
+
@up = 10
|
14
|
+
@down = 10
|
15
|
+
@type = type
|
16
|
+
@label = label
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"Start(#{@type}, label=#{@label})"
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(x, y, _width)
|
24
|
+
path = Path.new(x, y - 10)
|
25
|
+
if @type == 'complex'
|
26
|
+
path.down(20).m(0, -10).right(@width).add(self)
|
27
|
+
else
|
28
|
+
path.down(20).m(10, -20).down(20).m(-10, -10).right(@width).add(self)
|
29
|
+
end
|
30
|
+
if @label
|
31
|
+
DiagramItem.new(
|
32
|
+
'text',
|
33
|
+
attrs: {
|
34
|
+
'x' => x,
|
35
|
+
'y' => y - 15,
|
36
|
+
'style' => 'text-anchor:start'
|
37
|
+
},
|
38
|
+
text: @label
|
39
|
+
).add(self)
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def text_diagram
|
45
|
+
cross, line, tee_right = TextDiagram.get_parts(%w[cross line tee_right])
|
46
|
+
start =
|
47
|
+
if @type == 'simple'
|
48
|
+
tee_right + cross + line
|
49
|
+
else
|
50
|
+
tee_right + line
|
51
|
+
end
|
52
|
+
|
53
|
+
label_td = TextDiagram.new(0, 0, [])
|
54
|
+
if @label
|
55
|
+
label_td = TextDiagram.new(0, 0, [@label])
|
56
|
+
start = TextDiagram.pad_r(start, label_td.width, line)
|
57
|
+
end
|
58
|
+
start_td = TextDiagram.new(0, 0, [start])
|
59
|
+
label_td.append_below(start_td, [], move_entry: true, move_exit: true)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Style
|
5
|
+
def initialize(css)
|
6
|
+
@css = css
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def default_style
|
11
|
+
<<~CSS
|
12
|
+
svg.railroad-diagram {
|
13
|
+
background-color:hsl(30,20%,95%);
|
14
|
+
}
|
15
|
+
svg.railroad-diagram path {
|
16
|
+
stroke-width:3;
|
17
|
+
stroke:black;
|
18
|
+
fill:rgba(0,0,0,0);
|
19
|
+
}
|
20
|
+
svg.railroad-diagram text {
|
21
|
+
font:bold 14px monospace;
|
22
|
+
text-anchor:middle;
|
23
|
+
}
|
24
|
+
svg.railroad-diagram text.label{
|
25
|
+
text-anchor:start;
|
26
|
+
}
|
27
|
+
svg.railroad-diagram text.comment{
|
28
|
+
font:italic 12px monospace;
|
29
|
+
}
|
30
|
+
svg.railroad-diagram rect{
|
31
|
+
stroke-width:3;
|
32
|
+
stroke:black;
|
33
|
+
fill:hsl(120,100%,90%);
|
34
|
+
}
|
35
|
+
svg.railroad-diagram rect.group-box {
|
36
|
+
stroke: gray;
|
37
|
+
stroke-dasharray: 10 5;
|
38
|
+
fill: none;
|
39
|
+
}
|
40
|
+
CSS
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"Style(#{@css})"
|
46
|
+
end
|
47
|
+
|
48
|
+
def add(parent)
|
49
|
+
parent.children.push(self)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def format
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def text_diagram
|
58
|
+
TextDiagram.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def write_svg(write)
|
62
|
+
# Write included stylesheet as CDATA. See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/style
|
63
|
+
cdata = "/* <![CDATA[ */\n#{@css}\n/* ]]> */\n"
|
64
|
+
write.call("<style>#{cdata}</style>")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Terminal < DiagramItem
|
5
|
+
def initialize(text, href = nil, title = nil, cls: '')
|
6
|
+
super('g', attrs: { 'class' => "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
|
+
"Terminal(#{@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
|
+
'rx' => 10,
|
36
|
+
'ry' => 10
|
37
|
+
}
|
38
|
+
).add(self)
|
39
|
+
|
40
|
+
text = DiagramItem.new(
|
41
|
+
'text',
|
42
|
+
attrs: {
|
43
|
+
'x' => x + left_gap + (@width / 2),
|
44
|
+
'y' => y + 4
|
45
|
+
},
|
46
|
+
text: @text
|
47
|
+
)
|
48
|
+
if @href
|
49
|
+
a = DiagramItem.new('a', attrs: { 'xlink:href' => @href }, text:).add(self)
|
50
|
+
text.add(a)
|
51
|
+
else
|
52
|
+
text.add(self)
|
53
|
+
end
|
54
|
+
DiagramItem.new('title', attrs: {}, text: @title).add(self) if @title
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def text_diagram
|
59
|
+
# NOTE: href, title, and cls are ignored for text diagrams.
|
60
|
+
TextDiagram.round_rect(@text)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,341 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class TextDiagram
|
5
|
+
PARTS_UNICODE = {
|
6
|
+
'cross_diag' => '╳',
|
7
|
+
'corner_bot_left' => '└',
|
8
|
+
'corner_bot_right' => '┘',
|
9
|
+
'corner_top_left' => '┌',
|
10
|
+
'corner_top_right' => '┐',
|
11
|
+
'cross' => '┼',
|
12
|
+
'left' => '│',
|
13
|
+
'line' => '─',
|
14
|
+
'line_vertical' => '│',
|
15
|
+
'multi_repeat' => '↺',
|
16
|
+
'rect_bot' => '─',
|
17
|
+
'rect_bot_dashed' => '┄',
|
18
|
+
'rect_bot_left' => '└',
|
19
|
+
'rect_bot_right' => '┘',
|
20
|
+
'rect_left' => '│',
|
21
|
+
'rect_left_dashed' => '┆',
|
22
|
+
'rect_right' => '│',
|
23
|
+
'rect_right_dashed' => '┆',
|
24
|
+
'rect_top' => '─',
|
25
|
+
'rect_top_dashed' => '┄',
|
26
|
+
'rect_top_left' => '┌',
|
27
|
+
'rect_top_right' => '┐',
|
28
|
+
'repeat_bot_left' => '╰',
|
29
|
+
'repeat_bot_right' => '╯',
|
30
|
+
'repeat_left' => '│',
|
31
|
+
'repeat_right' => '│',
|
32
|
+
'repeat_top_left' => '╭',
|
33
|
+
'repeat_top_right' => '╮',
|
34
|
+
'right' => '│',
|
35
|
+
'roundcorner_bot_left' => '╰',
|
36
|
+
'roundcorner_bot_right' => '╯',
|
37
|
+
'roundcorner_top_left' => '╭',
|
38
|
+
'roundcorner_top_right' => '╮',
|
39
|
+
'roundrect_bot' => '─',
|
40
|
+
'roundrect_bot_dashed' => '┄',
|
41
|
+
'roundrect_bot_left' => '╰',
|
42
|
+
'roundrect_bot_right' => '╯',
|
43
|
+
'roundrect_left' => '│',
|
44
|
+
'roundrect_left_dashed' => '┆',
|
45
|
+
'roundrect_right' => '│',
|
46
|
+
'roundrect_right_dashed' => '┆',
|
47
|
+
'roundrect_top' => '─',
|
48
|
+
'roundrect_top_dashed' => '┄',
|
49
|
+
'roundrect_top_left' => '╭',
|
50
|
+
'roundrect_top_right' => '╮',
|
51
|
+
'separator' => '─',
|
52
|
+
'tee_left' => '┤',
|
53
|
+
'tee_right' => '├'
|
54
|
+
}.freeze
|
55
|
+
|
56
|
+
PARTS_ASCII = {
|
57
|
+
'cross_diag' => 'X',
|
58
|
+
'corner_bot_left' => '\\',
|
59
|
+
'corner_bot_right' => '/',
|
60
|
+
'corner_top_left' => '/',
|
61
|
+
'corner_top_right' => '\\',
|
62
|
+
'cross' => '+',
|
63
|
+
'left' => '|',
|
64
|
+
'line' => '-',
|
65
|
+
'line_vertical' => '|',
|
66
|
+
'multi_repeat' => '&',
|
67
|
+
'rect_bot' => '-',
|
68
|
+
'rect_bot_dashed' => '-',
|
69
|
+
'rect_bot_left' => '+',
|
70
|
+
'rect_bot_right' => '+',
|
71
|
+
'rect_left' => '|',
|
72
|
+
'rect_left_dashed' => '|',
|
73
|
+
'rect_right' => '|',
|
74
|
+
'rect_right_dashed' => '|',
|
75
|
+
'rect_top' => '-',
|
76
|
+
'rect_top_dashed' => '-',
|
77
|
+
'rect_top_left' => '+',
|
78
|
+
'rect_top_right' => '+',
|
79
|
+
'repeat_bot_left' => '\\',
|
80
|
+
'repeat_bot_right' => '/',
|
81
|
+
'repeat_left' => '|',
|
82
|
+
'repeat_right' => '|',
|
83
|
+
'repeat_top_left' => '/',
|
84
|
+
'repeat_top_right' => '\\',
|
85
|
+
'right' => '|',
|
86
|
+
'roundcorner_bot_left' => '\\',
|
87
|
+
'roundcorner_bot_right' => '/',
|
88
|
+
'roundcorner_top_left' => '/',
|
89
|
+
'roundcorner_top_right' => '\\',
|
90
|
+
'roundrect_bot' => '-',
|
91
|
+
'roundrect_bot_dashed' => '-',
|
92
|
+
'roundrect_bot_left' => '\\',
|
93
|
+
'roundrect_bot_right' => '/',
|
94
|
+
'roundrect_left' => '|',
|
95
|
+
'roundrect_left_dashed' => '|',
|
96
|
+
'roundrect_right' => '|',
|
97
|
+
'roundrect_right_dashed' => '|',
|
98
|
+
'roundrect_top' => '-',
|
99
|
+
'roundrect_top_dashed' => '-',
|
100
|
+
'roundrect_top_left' => '/',
|
101
|
+
'roundrect_top_right' => '\\',
|
102
|
+
'separator' => '-',
|
103
|
+
'tee_left' => '|',
|
104
|
+
'tee_right' => '|'
|
105
|
+
}.freeze
|
106
|
+
|
107
|
+
class << self
|
108
|
+
attr_accessor :parts
|
109
|
+
|
110
|
+
def set_formatting(characters = nil, defaults = nil)
|
111
|
+
return unless characters
|
112
|
+
|
113
|
+
@parts = defaults ? defaults.dup : {}
|
114
|
+
@parts.merge!(characters)
|
115
|
+
@parts.each do |name, value|
|
116
|
+
raise ArgumentError, "Text part #{name} is more than 1 character: #{value}" if value.size != 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def rect(item, dashed = false)
|
121
|
+
rectish('rect', item, dashed)
|
122
|
+
end
|
123
|
+
|
124
|
+
def round_rect(item, dashed = false)
|
125
|
+
rectish('roundrect', item, dashed)
|
126
|
+
end
|
127
|
+
|
128
|
+
def pad_l(string, width, pad)
|
129
|
+
gap = width - string.length
|
130
|
+
raise "Gap #{gap} must be a multiple of pad string '#{pad}'" unless gap % pad.length == 0
|
131
|
+
|
132
|
+
(pad * (gap / pad.length)) + string
|
133
|
+
end
|
134
|
+
|
135
|
+
def pad_r(string, width, pad)
|
136
|
+
gap = width - string.length
|
137
|
+
raise "Gap #{gap} must be a multiple of pad string '#{pad}'" unless gap % pad.length == 0
|
138
|
+
|
139
|
+
string + (pad * (gap / pad.length))
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def rectish(rect_type, data, dashed)
|
145
|
+
line_type = dashed ? '_dashed' : ''
|
146
|
+
parts = get_parts([
|
147
|
+
"#{rect_type}_top_left",
|
148
|
+
"#{rect_type}_left#{line_type}",
|
149
|
+
"#{rect_type}_bot_left",
|
150
|
+
"#{rect_type}_top_right",
|
151
|
+
"#{rect_type}_right#{line_type}",
|
152
|
+
"#{rect_type}_bot_right",
|
153
|
+
"#{rect_type}_top#{line_type}",
|
154
|
+
"#{rect_type}_bot#{line_type}",
|
155
|
+
'line',
|
156
|
+
'cross'
|
157
|
+
])
|
158
|
+
|
159
|
+
item_td = data.is_a?(TextDiagram) ? data : new(0, 0, [data.to_s])
|
160
|
+
|
161
|
+
lines = [parts[6] * (item_td.width + 2)]
|
162
|
+
lines += item_td.expand(1, 1, 0, 0).lines.map { |line| " #{line} " }
|
163
|
+
lines << (parts[7] * (item_td.width + 2))
|
164
|
+
|
165
|
+
entry = item_td.entry + 1
|
166
|
+
exit = item_td.exit + 1
|
167
|
+
|
168
|
+
left_max = [parts[0], parts[1], parts[2]].map(&:size).max
|
169
|
+
lefts = Array.new(lines.size, parts[1].ljust(left_max))
|
170
|
+
lefts[0] = parts[0].ljust(left_max, parts[6])
|
171
|
+
lefts[-1] = parts[2].ljust(left_max, parts[7])
|
172
|
+
lefts[entry] = parts[9].ljust(left_max) if data.is_a?(TextDiagram)
|
173
|
+
|
174
|
+
right_max = [parts[3], parts[4], parts[5]].map(&:size).max
|
175
|
+
rights = Array.new(lines.size, parts[4].rjust(right_max))
|
176
|
+
rights[0] = parts[3].rjust(right_max, parts[6])
|
177
|
+
rights[-1] = parts[5].rjust(right_max, parts[7])
|
178
|
+
rights[exit] = parts[9].rjust(right_max) if data.is_a?(TextDiagram)
|
179
|
+
|
180
|
+
new_lines = lines.each_with_index.map do |line, i|
|
181
|
+
lefts[i] + line + rights[i]
|
182
|
+
end
|
183
|
+
|
184
|
+
lefts = Array.new(lines.size, ' ')
|
185
|
+
lefts[entry] = parts[8]
|
186
|
+
rights = Array.new(lines.size, ' ')
|
187
|
+
rights[exit] = parts[8]
|
188
|
+
|
189
|
+
new_lines = new_lines.each_with_index.map do |line, i|
|
190
|
+
lefts[i] + line + rights[i]
|
191
|
+
end
|
192
|
+
|
193
|
+
new(entry, exit, new_lines)
|
194
|
+
end
|
195
|
+
|
196
|
+
def enclose_lines(lines, lefts, rights)
|
197
|
+
unless lines.length == lefts.length && lines.length == rights.length
|
198
|
+
raise 'All arguments must be the same length'
|
199
|
+
end
|
200
|
+
|
201
|
+
lines.each_with_index.map { |line, i| lefts[i] + line + rights[i] }
|
202
|
+
end
|
203
|
+
|
204
|
+
def gaps(outer_width, inner_width)
|
205
|
+
diff = outer_width - inner_width
|
206
|
+
case INTERNAL_ALIGNMENT
|
207
|
+
when 'left'
|
208
|
+
[0, diff]
|
209
|
+
when 'right'
|
210
|
+
[diff, 0]
|
211
|
+
else
|
212
|
+
left = diff / 2
|
213
|
+
right = diff - left
|
214
|
+
[left, right]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def get_parts(part_names)
|
219
|
+
part_names.map { |name| @parts[name] }
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
attr_reader :entry, :exit, :height, :lines, :width
|
224
|
+
|
225
|
+
def initialize(entry, exit, lines)
|
226
|
+
@entry = entry
|
227
|
+
@exit = exit
|
228
|
+
@lines = lines.dup
|
229
|
+
@height = lines.size
|
230
|
+
@width = lines.empty? ? 0 : lines.first.size
|
231
|
+
|
232
|
+
validate
|
233
|
+
end
|
234
|
+
|
235
|
+
def alter(new_entry = nil, new_exit = nil, new_lines = nil)
|
236
|
+
self.class.new(
|
237
|
+
new_entry || @entry,
|
238
|
+
new_exit || @exit,
|
239
|
+
new_lines || @lines.dup
|
240
|
+
)
|
241
|
+
end
|
242
|
+
|
243
|
+
def append_below(item, lines_between, move_entry: false, move_exit: false)
|
244
|
+
new_width = [@width, item.width].max
|
245
|
+
new_lines = center(new_width).lines
|
246
|
+
lines_between.each { |line| new_lines << TextDiagram.pad_r(line, new_width, ' ') }
|
247
|
+
new_lines += item.center(new_width).lines
|
248
|
+
|
249
|
+
new_entry = move_entry ? @height + lines_between.size + item.entry : @entry
|
250
|
+
new_exit = move_exit ? @height + lines_between.size + item.exit : @exit
|
251
|
+
|
252
|
+
self.class.new(new_entry, new_exit, new_lines)
|
253
|
+
end
|
254
|
+
|
255
|
+
def append_right(item, chars_between)
|
256
|
+
join_line = [@exit, item.entry].max
|
257
|
+
new_height = [@height - @exit, item.height - item.entry].max + join_line
|
258
|
+
|
259
|
+
left = expand(0, 0, join_line - @exit, new_height - @height - (join_line - @exit))
|
260
|
+
right = item.expand(0, 0, join_line - item.entry, new_height - item.height - (join_line - item.entry))
|
261
|
+
|
262
|
+
new_lines = (0...new_height).map do |i|
|
263
|
+
sep = i == join_line ? chars_between : ' ' * chars_between.size
|
264
|
+
left_line = i < left.lines.size ? left.lines[i] : ' ' * left.width
|
265
|
+
right_line = i < right.lines.size ? right.lines[i] : ' ' * right.width
|
266
|
+
"#{left_line}#{sep}#{right_line}"
|
267
|
+
end
|
268
|
+
|
269
|
+
self.class.new(
|
270
|
+
@entry + (join_line - @exit),
|
271
|
+
item.exit + (join_line - item.entry),
|
272
|
+
new_lines
|
273
|
+
)
|
274
|
+
end
|
275
|
+
|
276
|
+
def center(new_width, pad = ' ')
|
277
|
+
raise 'Cannot center into smaller width' if width < @width
|
278
|
+
return copy if new_width == @width
|
279
|
+
|
280
|
+
total_padding = new_width - @width
|
281
|
+
left_width = total_padding / 2
|
282
|
+
left = [pad * left_width] * @height
|
283
|
+
right = [pad * (total_padding - left_width)] * @height
|
284
|
+
|
285
|
+
self.class.new(@entry, @exit, enclose_lines(@lines, left, right))
|
286
|
+
end
|
287
|
+
|
288
|
+
def copy
|
289
|
+
self.class.new(@entry, @exit, @lines.dup)
|
290
|
+
end
|
291
|
+
|
292
|
+
def expand(left, right, top, bottom)
|
293
|
+
return copy if [left, right, top, bottom].all?(&:zero?)
|
294
|
+
|
295
|
+
new_lines = []
|
296
|
+
top.times { new_lines << (' ' * (@width + left + right)) }
|
297
|
+
|
298
|
+
@lines.each do |line|
|
299
|
+
left_part = (line == @lines[@entry] ? self.class.parts['line'] : ' ') * left
|
300
|
+
right_part = (line == @lines[@exit] ? self.class.parts['line'] : ' ') * right
|
301
|
+
new_lines << "#{left_part}#{line}#{right_part}"
|
302
|
+
end
|
303
|
+
|
304
|
+
bottom.times { new_lines << (' ' * (@width + left + right)) }
|
305
|
+
|
306
|
+
self.class.new(
|
307
|
+
@entry + top,
|
308
|
+
@exit + top,
|
309
|
+
new_lines
|
310
|
+
)
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def validate
|
316
|
+
return if @lines.empty?
|
317
|
+
|
318
|
+
line_length = @lines.first.size
|
319
|
+
@lines.each do |line|
|
320
|
+
raise ArgumentError, "Diagram is not rectangular:\n#{inspect}" unless line.size == line_length
|
321
|
+
end
|
322
|
+
|
323
|
+
raise ArgumentError, "Entry point out of bounds:\n#{inspect}" if @entry >= @height
|
324
|
+
|
325
|
+
return unless @exit >= @height
|
326
|
+
|
327
|
+
raise ArgumentError, "Exit point out of bounds:\n#{inspect}"
|
328
|
+
end
|
329
|
+
|
330
|
+
def inspect
|
331
|
+
output = ["TextDiagram(entry=#{@entry}, exit=#{@exit}, height=#{@height})"]
|
332
|
+
@lines.each_with_index do |line, i|
|
333
|
+
marker = []
|
334
|
+
marker << 'entry' if i == @entry
|
335
|
+
marker << 'exit' if i == @exit
|
336
|
+
output << (format('%3d: %-20s %s', i, line.inspect, marker.join(', ')))
|
337
|
+
end
|
338
|
+
output.join("\n")
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|