railroad_diagrams 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2025-02-01
6
+
7
+ - Initial release
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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: %i[]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.join(__dir__, '../lib')
5
+ require 'railroad_diagrams'
6
+
7
+ RailroadDiagrams::Command.new.run(ARGV.dup)
@@ -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