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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 992d560c768d0ad4991ea5c774a60083b4a2f8c407b88c91abfe6c92ab73fea5
|
4
|
+
data.tar.gz: 108ce587863fb33a368d2e5278002ff6ee6bd6885576f6f7b9a7cd4e7c8eec3a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6795906adf826df7a16207cea05cc54a3724cd66ab5dd0a9120643d5b2bf24f08ab7ac2b66d517478cd826c1baccc6faadfe1365b40dc6636bcd102e2ab393cf
|
7
|
+
data.tar.gz: e041c348cea71435c688dedb451eb368b00e71dac41cce239492766e7dd2991c61576c0087774064a948d9248538f74b233dfde439117b6587ed46df310a4ba3
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Yudai Takada
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/MIT
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Yudai Takada
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# RailroadDiagrams
|
2
|
+
|
3
|
+
A tiny Ruby+SVG library for drawing railroad syntax diagrams.
|
4
|
+
|
5
|
+
# Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
```ruby
|
9
|
+
gem 'railroad_diagrams'
|
10
|
+
```
|
11
|
+
|
12
|
+
Add then execute:
|
13
|
+
```bash
|
14
|
+
bundle install
|
15
|
+
```
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
```bash
|
19
|
+
gem install railroad_diagrams
|
20
|
+
```
|
21
|
+
|
22
|
+
## Development
|
23
|
+
|
24
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
25
|
+
|
26
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/railroad_diagrams.
|
data/Rakefile
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class AlternatingSequence < DiagramMultiContainer
|
5
|
+
def self.new(*items)
|
6
|
+
raise "AlternatingSequence takes exactly two arguments, but got #{items.size} arguments." unless items.size == 2
|
7
|
+
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(*items)
|
12
|
+
super('g', items)
|
13
|
+
@needs_space = false
|
14
|
+
|
15
|
+
arc = AR
|
16
|
+
vert = VS
|
17
|
+
first, second = @items
|
18
|
+
|
19
|
+
arc_x = 1 / Math.sqrt(2) * arc * 2
|
20
|
+
arc_y = (1 - (1 / Math.sqrt(2))) * arc * 2
|
21
|
+
cross_y = [arc, vert].max
|
22
|
+
cross_x = (cross_y - arc_y) + arc_x
|
23
|
+
|
24
|
+
first_out = [
|
25
|
+
arc + arc, (cross_y / 2) + arc + arc, (cross_y / 2) + vert + first.down
|
26
|
+
].max
|
27
|
+
@up = first_out + first.height + first.up
|
28
|
+
|
29
|
+
second_in = [
|
30
|
+
arc + arc, (cross_y / 2) + arc + arc, (cross_y / 2) + vert + second.up
|
31
|
+
].max
|
32
|
+
@down = second_in + second.height + second.down
|
33
|
+
|
34
|
+
@height = 0
|
35
|
+
|
36
|
+
first_width = (first.needs_space ? 20 : 0) + first.width
|
37
|
+
second_width = (second.needs_space ? 20 : 0) + second.width
|
38
|
+
@width = (2 * arc) + [first_width, cross_x, second_width].max + (2 * arc)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
items = @items.map(&:to_s).join(', ')
|
43
|
+
"AlternatingSequence(#{items})"
|
44
|
+
end
|
45
|
+
|
46
|
+
def format(x, y, width)
|
47
|
+
arc = AR
|
48
|
+
gaps = determine_gaps(width, @width)
|
49
|
+
Path.new(x, y).right(gaps[0]).add(self)
|
50
|
+
x += gaps[0]
|
51
|
+
Path.new(x + @width, y + @height).right(gaps[1]).add(self)
|
52
|
+
# bounding box
|
53
|
+
# Path(x+gaps[0], y).up(@up).right(@width).down(@up+@down).left(@width).up(@up).add(self)
|
54
|
+
first, second = @items
|
55
|
+
|
56
|
+
# top
|
57
|
+
first_in = @up - first.up
|
58
|
+
first_out = @up - first.up - first.height
|
59
|
+
Path.new(x, y).arc('se').up(first_in - (2 * arc)).arc('wn').add(self)
|
60
|
+
first.format(x + (2 * arc), y - first_in, @width - (4 * arc)).add(self)
|
61
|
+
Path.new(x + @width - (2 * arc), y - first_out)
|
62
|
+
.arc('ne').down(first_out - (2 * arc)).arc('ws').add(self)
|
63
|
+
|
64
|
+
# bottom
|
65
|
+
second_in = @down - second.down - second.height
|
66
|
+
second_out = @down - second.down
|
67
|
+
Path.new(x, y)
|
68
|
+
.arc('ne')
|
69
|
+
.down(second_in - (2 * arc))
|
70
|
+
.arc('ws')
|
71
|
+
.add(self)
|
72
|
+
second.format(x + (2 * arc), y + second_in, @width - (4 * arc)).add(self)
|
73
|
+
Path.new(x + @width - (2 * arc), y + second_out)
|
74
|
+
.arc('se').up(second_out - (2 * arc)).arc('wn').add(self)
|
75
|
+
|
76
|
+
# crossover
|
77
|
+
arc_x = 1 / Math.sqrt(2) * arc * 2
|
78
|
+
arc_y = (1 - (1 / Math.sqrt(2))) * arc * 2
|
79
|
+
cross_y = [arc, VS].max
|
80
|
+
cross_x = (cross_y - arc_y) + arc_x
|
81
|
+
cross_bar = (@width - (4 * arc) - cross_x) / 2
|
82
|
+
|
83
|
+
Path.new(x + arc, y - (cross_y / 2) - arc)
|
84
|
+
.arc('ws')
|
85
|
+
.right(cross_bar)
|
86
|
+
.arc_8('n', 'cw')
|
87
|
+
.l(cross_x - arc_x, cross_y - arc_y)
|
88
|
+
.arc_8('sw', 'ccw')
|
89
|
+
.right(cross_bar)
|
90
|
+
.arc('ne')
|
91
|
+
.add(self)
|
92
|
+
|
93
|
+
Path.new(x + arc, y + (cross_y / 2) + arc)
|
94
|
+
.arc('wn')
|
95
|
+
.right(cross_bar)
|
96
|
+
.arc_8('s', 'ccw')
|
97
|
+
.l(cross_x - arc_x, -(cross_y - arc_y))
|
98
|
+
.arc_8('nw', 'cw')
|
99
|
+
.right(cross_bar)
|
100
|
+
.arc('se')
|
101
|
+
.add(self)
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def text_diagram
|
107
|
+
cross_diag, corner_bot_left, corner_bot_right, corner_top_left, corner_top_right,
|
108
|
+
line, line_vertical, tee_left, tee_right = TextDiagram.get_parts(
|
109
|
+
%w[
|
110
|
+
cross_diag roundcorner_bot_left roundcorner_bot_right
|
111
|
+
roundcorner_top_left roundcorner_top_right line
|
112
|
+
line_vertical tee_left tee_right
|
113
|
+
]
|
114
|
+
)
|
115
|
+
|
116
|
+
first_td = @items[0].text_diagram
|
117
|
+
second_td = @items[1].text_diagram
|
118
|
+
max_width = TextDiagram._max_width(first_td, second_td)
|
119
|
+
left_width, right_width = TextDiagram._gaps(max_width, 0)
|
120
|
+
|
121
|
+
left_lines = []
|
122
|
+
right_lines = []
|
123
|
+
separator = []
|
124
|
+
|
125
|
+
left_size, right_size = TextDiagram._gaps(first_td.width, 0)
|
126
|
+
diagram_td = first_td.expand(left_width - left_size, right_width - right_size, 0, 0)
|
127
|
+
|
128
|
+
left_lines += [' ' * 2] * diagram_td.entry
|
129
|
+
left_lines << (corner_top_left + line)
|
130
|
+
left_lines += ["#{line_vertical} "] * (diagram_td.height - diagram_td.entry - 1)
|
131
|
+
left_lines << (corner_bot_left + line)
|
132
|
+
|
133
|
+
right_lines += [' ' * 2] * diagram_td.entry
|
134
|
+
right_lines << (line + corner_top_right)
|
135
|
+
right_lines += [" #{line_vertical}"] * (diagram_td.height - diagram_td.entry - 1)
|
136
|
+
right_lines << (line + corner_bot_right)
|
137
|
+
|
138
|
+
separator << ("#{line * (left_width - 1)}#{corner_top_right} #{corner_top_left}#{line * (right_width - 2)}")
|
139
|
+
separator << ("#{' ' * (left_width - 1)} #{cross_diag} #{' ' * (right_width - 2)}")
|
140
|
+
separator << ("#{line * (left_width - 1)}#{corner_bot_right} #{corner_bot_left}#{line * (right_width - 2)}")
|
141
|
+
|
142
|
+
left_lines << (' ' * 2)
|
143
|
+
right_lines << (' ' * 2)
|
144
|
+
|
145
|
+
left_size, right_size = TextDiagram._gaps(second_td.width, 0)
|
146
|
+
second_td = second_td.expand(left_width - left_size, right_width - right_size, 0, 0)
|
147
|
+
diagram_td = diagram_td.append_below(second_td, separator, move_entry: true, move_exit: true)
|
148
|
+
|
149
|
+
left_lines << (corner_top_left + line)
|
150
|
+
left_lines += ["#{line_vertical} "] * second_td.entry
|
151
|
+
left_lines << (corner_bot_left + line)
|
152
|
+
|
153
|
+
right_lines << (line + corner_top_right)
|
154
|
+
right_lines += [" #{line_vertical}"] * second_td.entry
|
155
|
+
right_lines << (line + corner_bot_right)
|
156
|
+
|
157
|
+
mid_point = first_td.height + (separator.size / 2)
|
158
|
+
diagram_td = diagram_td.alter(entry: mid_point, exit: mid_point)
|
159
|
+
|
160
|
+
left_td = TextDiagram.new(mid_point, mid_point, left_lines)
|
161
|
+
right_td = TextDiagram.new(mid_point, mid_point, right_lines)
|
162
|
+
|
163
|
+
diagram_td = left_td.append_right(diagram_td, '').append_right(right_td, '')
|
164
|
+
TextDiagram.new(1, 1, [corner_top_left, tee_left, corner_bot_left])
|
165
|
+
.append_right(diagram_td, '')
|
166
|
+
.append_right(TextDiagram.new(1, 1, [corner_top_right, tee_right, corner_bot_right]), '')
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Choice < DiagramMultiContainer
|
5
|
+
def initialize(default, *items)
|
6
|
+
super('g', items)
|
7
|
+
raise ArgumentError, 'default index out of range' if default >= items.size
|
8
|
+
|
9
|
+
@default = default
|
10
|
+
@width = (AR * 4) + @items.map(&:width).max
|
11
|
+
|
12
|
+
# The calcs are non-trivial and need to be done both here
|
13
|
+
# and in .format(), so no reason to do it twice.
|
14
|
+
@separators = Array.new(items.size - 1, VS)
|
15
|
+
|
16
|
+
# If the entry or exit lines would be too close together
|
17
|
+
# to accommodate the arcs,
|
18
|
+
# bump up the vertical separation to compensate.
|
19
|
+
@up = 0
|
20
|
+
(default - 1).downto(0) do |i|
|
21
|
+
arcs =
|
22
|
+
if i == default - 1
|
23
|
+
AR * 2
|
24
|
+
else
|
25
|
+
AR
|
26
|
+
end
|
27
|
+
|
28
|
+
item = @items[i]
|
29
|
+
lower_item = @items[i + 1]
|
30
|
+
|
31
|
+
entry_delta = lower_item.up + VS + item.down + item.height
|
32
|
+
exit_delta = lower_item.height + lower_item.up + VS + item.down
|
33
|
+
|
34
|
+
separator = VS
|
35
|
+
separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
|
36
|
+
@separators[i] = separator
|
37
|
+
|
38
|
+
@up += lower_item.up + separator + item.down + item.height
|
39
|
+
end
|
40
|
+
@up += @items[0].up
|
41
|
+
|
42
|
+
@height = @items[default].height
|
43
|
+
@down = 0
|
44
|
+
|
45
|
+
(default + 1...@items.size).each do |i|
|
46
|
+
arcs =
|
47
|
+
if i == default + 1
|
48
|
+
AR * 2
|
49
|
+
else
|
50
|
+
AR
|
51
|
+
end
|
52
|
+
|
53
|
+
item = @items[i]
|
54
|
+
upper_item = @items[i - 1]
|
55
|
+
|
56
|
+
entry_delta = upper_item.height + upper_item.down + VS + item.up
|
57
|
+
exit_delta = upper_item.down + VS + item.up + item.height
|
58
|
+
|
59
|
+
separator = VS
|
60
|
+
separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
|
61
|
+
@separators[i - 1] = separator
|
62
|
+
|
63
|
+
@down += upper_item.down + separator + item.up + item.height
|
64
|
+
end
|
65
|
+
@down += @items[-1].down
|
66
|
+
@needs_space = false
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
items_str = @items.map(&:inspect).join(', ')
|
71
|
+
"Choice(#{@default}, #{items_str})"
|
72
|
+
end
|
73
|
+
|
74
|
+
def format(x, y, width)
|
75
|
+
left_gap, right_gap = determine_gaps(width, @width)
|
76
|
+
|
77
|
+
# Hook up the two sides if self is narrower than its stated width.
|
78
|
+
Path.new(x, y).h(left_gap).add(self)
|
79
|
+
Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
|
80
|
+
x += left_gap
|
81
|
+
|
82
|
+
inner_width = @width - (AR * 4)
|
83
|
+
default = @items[@default]
|
84
|
+
|
85
|
+
# Do the elements that curve above
|
86
|
+
distance_from_y = 0
|
87
|
+
(@default - 1).downto(0) do |i|
|
88
|
+
item = @items[i]
|
89
|
+
lower_item = @items[i + 1]
|
90
|
+
distance_from_y += lower_item.up + @separators[i] + item.down + item.height
|
91
|
+
Path.new(x, y)
|
92
|
+
.arc('se')
|
93
|
+
.up(distance_from_y - (AR * 2))
|
94
|
+
.arc('wn')
|
95
|
+
.add(self)
|
96
|
+
item.format(x + (AR * 2), y - distance_from_y, inner_width).add(self)
|
97
|
+
Path.new(x + (AR * 2) + inner_width, y - distance_from_y + item.height)
|
98
|
+
.arc('ne')
|
99
|
+
.down(distance_from_y - item.height + default.height - (AR * 2))
|
100
|
+
.arc('ws')
|
101
|
+
.add(self)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Do the straight-line path.
|
105
|
+
Path.new(x, y).right(AR * 2).add(self)
|
106
|
+
@items[@default].format(x + (AR * 2), y, inner_width).add(self)
|
107
|
+
Path.new(x + (AR * 2) + inner_width, y + @height).right(AR * 2).add(self)
|
108
|
+
|
109
|
+
# Do the elements that curve below
|
110
|
+
distance_from_y = 0
|
111
|
+
(@default + 1...@items.size).each do |i|
|
112
|
+
item = @items[i]
|
113
|
+
upper_item = @items[i - 1]
|
114
|
+
distance_from_y += upper_item.height + upper_item.down + @separators[i - 1] + item.up
|
115
|
+
Path.new(x, y)
|
116
|
+
.arc('ne')
|
117
|
+
.down(distance_from_y - (AR * 2))
|
118
|
+
.arc('ws')
|
119
|
+
.add(self)
|
120
|
+
item.format(x + (AR * 2), y + distance_from_y, inner_width).add(self)
|
121
|
+
Path.new(x + (AR * 2) + inner_width, y + distance_from_y + item.height)
|
122
|
+
.arc('se')
|
123
|
+
.up(distance_from_y - (AR * 2) + item.height - default.height)
|
124
|
+
.arc('wn')
|
125
|
+
.add(self)
|
126
|
+
end
|
127
|
+
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def text_diagram
|
132
|
+
cross, line, line_vertical, roundcorner_bot_left, roundcorner_bot_right, roundcorner_top_left, roundcorner_top_right =
|
133
|
+
TextDiagram.get_parts(
|
134
|
+
%w[
|
135
|
+
cross line line_vertical roundcorner_bot_left roundcorner_bot_right roundcorner_top_left roundcorner_top_right
|
136
|
+
]
|
137
|
+
)
|
138
|
+
|
139
|
+
# Format all the child items, so we can know the maximum width.
|
140
|
+
item_tds = @items.map { |item| item.text_diagram.expand(1, 1, 0, 0) }
|
141
|
+
max_item_width = item_tds.map(&:width).max
|
142
|
+
diagram_td = TextDiagram.new(0, 0, [])
|
143
|
+
# Format the choice collection.
|
144
|
+
item_tds.each_with_index do |item_td, i|
|
145
|
+
left_pad, right_pad = TextDiagram.gaps(max_item_width, item_td.width)
|
146
|
+
item_td = item_td.expand(left_pad, right_pad, 0, 0)
|
147
|
+
has_separator = true
|
148
|
+
left_lines = [line_vertical] * item_td.height
|
149
|
+
right_lines = [line_vertical] * item_td.height
|
150
|
+
move_entry = false
|
151
|
+
move_exit = false
|
152
|
+
if i <= @default
|
153
|
+
# First item and above the line: also remove ascenders above the item's entry and exit, suppress the separator above it.
|
154
|
+
has_separator = false
|
155
|
+
[0..item_td.entry].each { |j| left_lines[j] = ' ' }
|
156
|
+
[0..item_td.exit].each { |j| right_lines[j] = ' ' }
|
157
|
+
end
|
158
|
+
if i >= @default
|
159
|
+
# Item below the line: round off the entry/exit lines downwards.
|
160
|
+
left_lines[item_td.entry] = roundcorner_bot_left
|
161
|
+
right_lines[item_td.entry] = roundcorner_bot_right
|
162
|
+
if i == 0
|
163
|
+
# First item and below the line: also suppress the separator above it.
|
164
|
+
has_separator = false
|
165
|
+
end
|
166
|
+
if i == @items.size - 1
|
167
|
+
# Last item and below the line: also remove descenders below the item's entry and exit
|
168
|
+
[item_td.entry + 1..item_td.height].each { |j| left_lines[j] = ' ' }
|
169
|
+
[item_td.exit + 1..item_td.height].each { |j| right_lines[j] = ' ' }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
if i == @default
|
173
|
+
# Item on the line: entry/exit are horizontal, and sets the outer entry/exit.
|
174
|
+
left_lines[item_td.entry] = cross
|
175
|
+
right_lines[item_td.entry] = cross
|
176
|
+
move_entry = true
|
177
|
+
move_exit = true
|
178
|
+
if i == 0 && i == @items.size - 1
|
179
|
+
# Only item and on the line: set entry/exit for straight through.
|
180
|
+
left_lines[item_td.entry] = line
|
181
|
+
right_lines[item_td.entry] = line
|
182
|
+
elsif i == 0
|
183
|
+
# First item and on the line: set entry/exit for no ascenders.
|
184
|
+
left_lines[item_td.entry] = roundcorner_top_left
|
185
|
+
left_lines[item_td.exit] = roundcorner_bot_left
|
186
|
+
elsif i == @items.size - 1
|
187
|
+
# Last item and on the line: set entry/exit for no descenders.
|
188
|
+
left_lines[item_td.entry] = roundcorner_bot_left
|
189
|
+
right_lines[item_td.entry] = roundcorner_bot_right
|
190
|
+
end
|
191
|
+
end
|
192
|
+
left_join_td = TextDiagram.new(item_td.entry, item_td.entry, left_lines)
|
193
|
+
right_join_td = TextDiagram.new(item_td.exit, item_td.exit, right_lines)
|
194
|
+
item_td = left_join_td.append_right(item_td, '').append_right(right_join_td, '')
|
195
|
+
separator = if has_separator
|
196
|
+
[
|
197
|
+
line_vertical +
|
198
|
+
(' ' * (TextDiagram.max_width(diagram_td, item_td) - 2)) +
|
199
|
+
line_vertical
|
200
|
+
]
|
201
|
+
else
|
202
|
+
[]
|
203
|
+
end
|
204
|
+
diagram_td = diagram_td.append_below(item_td, separator, move_entry:, move_exit:)
|
205
|
+
end
|
206
|
+
diagram_td
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module RailroadDiagrams
|
6
|
+
class Command
|
7
|
+
def initialize
|
8
|
+
@format = 'svg'
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(argv)
|
12
|
+
OptionParser.new do |opts|
|
13
|
+
opts.banner = <<~BANNER
|
14
|
+
This is a test runner for railroad_diagrams:
|
15
|
+
Usage: railroad_diagrams [options] [files]
|
16
|
+
BANNER
|
17
|
+
|
18
|
+
opts.on('-f', '--format FORMAT', 'Output format (svg, ascii, unicode, standalone)') do |format|
|
19
|
+
@format = format
|
20
|
+
end
|
21
|
+
opts.on('-h', '--help', 'Print this help') do
|
22
|
+
puts opts
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
opts.on('-v', '--version', 'Print version') do
|
26
|
+
puts "railroad_diagrams #{RailroadDiagrams::VERSION}"
|
27
|
+
exit 0
|
28
|
+
end
|
29
|
+
opts.parse!(argv)
|
30
|
+
end
|
31
|
+
|
32
|
+
@test_list = argv
|
33
|
+
|
34
|
+
puts <<~HTML
|
35
|
+
<!doctype html>
|
36
|
+
<html>
|
37
|
+
<head>
|
38
|
+
<title>Test</title>
|
39
|
+
HTML
|
40
|
+
|
41
|
+
case @format
|
42
|
+
when 'ascii'
|
43
|
+
TextDiagram.set_formatting(TextDiagram::PARTS_ASCII)
|
44
|
+
when 'unicode'
|
45
|
+
TextDiagram.set_formatting(TextDiagram::PARTS_UNICODE)
|
46
|
+
when 'svg', 'standalone'
|
47
|
+
TextDiagram.set_formatting(TextDiagram::PARTS_UNICODE)
|
48
|
+
puts <<~CSS
|
49
|
+
<style>
|
50
|
+
#{Style.default_style}
|
51
|
+
.blue text { fill: blue; }
|
52
|
+
</style>
|
53
|
+
CSS
|
54
|
+
end
|
55
|
+
|
56
|
+
puts '</head><body>'
|
57
|
+
|
58
|
+
File.open('test.rb', 'r:utf-8') do |fh|
|
59
|
+
eval(fh.read, binding, 'test.rb')
|
60
|
+
end
|
61
|
+
|
62
|
+
puts '</body></html>'
|
63
|
+
end
|
64
|
+
|
65
|
+
def add(name, diagram)
|
66
|
+
return unless @test_list.empty? || @test_list.include?(name)
|
67
|
+
|
68
|
+
puts "\n<h1>#{RailroadDiagrams.escape_html(name)}</h1>"
|
69
|
+
|
70
|
+
case @format
|
71
|
+
when 'svg'
|
72
|
+
diagram.write_svg(STDOUT.method(:write))
|
73
|
+
when 'standalone'
|
74
|
+
diagram.write_standalone(STDOUT.method(:write))
|
75
|
+
when 'ascii', 'unicode'
|
76
|
+
puts "\n<pre>"
|
77
|
+
diagram.write_text(STDOUT.method(:write))
|
78
|
+
puts "\n</pre>"
|
79
|
+
end
|
80
|
+
|
81
|
+
puts "\n"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailroadDiagrams
|
4
|
+
class Comment < 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 * COMMENT_CHAR_WIDTH) + 10
|
12
|
+
@up = 8
|
13
|
+
@down = 8
|
14
|
+
@needs_space = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"Comment(#{@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
|
+
text = DiagramItem.new(
|
29
|
+
'text',
|
30
|
+
attrs: { 'x' => x + left_gap + (@width / 2), 'y' => y + 4, 'class' => 'comment' },
|
31
|
+
text: @text
|
32
|
+
)
|
33
|
+
if @href
|
34
|
+
a = DiagramItem.new('a', attrs: { 'xlink:href' => @href }, text:).add(self)
|
35
|
+
text.add(a)
|
36
|
+
else
|
37
|
+
text.add(self)
|
38
|
+
end
|
39
|
+
DiagramItem.new('title', attrs: {}, text: @title).add(self) if @title
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def text_diagram
|
44
|
+
# NOTE: href, title, and cls are ignored for text diagrams.
|
45
|
+
TextDiagram.new(0, 0, @text)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|