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,107 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Diagram < DiagramMultiContainer
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(*items, **kwargs)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('svg', items.to_a, { 'class' => DIAGRAM_CLASS })
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @type = kwargs.fetch(:type, 'simple')
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  if @items.any?
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @items.unshift(Start.new(@type)) unless @items.first.is_a?(Start)
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @items.push(End.new(@type)) unless @items.last.is_a?(End)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  @up = 0
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @down = 0
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @height = 0
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @width = 0
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  @items.each do |item|
         
     | 
| 
      
 20 
     | 
    
         
            +
                    next if item.is_a?(Style)
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    @width += item.width + (item.needs_space ? 20 : 0)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @up = [@up, item.up - @height].max
         
     | 
| 
      
 24 
     | 
    
         
            +
                    @height += item.height
         
     | 
| 
      
 25 
     | 
    
         
            +
                    @down = [@down - item.height, item.down].max
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  @width -= 10 if @items[0].needs_space
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @width -= 10 if @items[-1].needs_space
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @formatted = false
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 34 
     | 
    
         
            +
                  items = items.map(&:to_s).join(', ')
         
     | 
| 
      
 35 
     | 
    
         
            +
                  pieces = items ? [items] : []
         
     | 
| 
      
 36 
     | 
    
         
            +
                  pieces.push("type=#{@type}") if @type != 'simple'
         
     | 
| 
      
 37 
     | 
    
         
            +
                  "Diagram(#{pieces.join(', ')})"
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                def format(padding_top = 20, padding_right = nil, padding_bottom = nil, padding_left = nil)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  padding_right = padding_top if padding_right.nil?
         
     | 
| 
      
 42 
     | 
    
         
            +
                  padding_bottom = padding_top if padding_bottom.nil?
         
     | 
| 
      
 43 
     | 
    
         
            +
                  padding_left = padding_right if padding_left.nil?
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  x = padding_left
         
     | 
| 
      
 46 
     | 
    
         
            +
                  y = padding_top + @up
         
     | 
| 
      
 47 
     | 
    
         
            +
                  g = DiagramItem.new('g')
         
     | 
| 
      
 48 
     | 
    
         
            +
                  g.attrs['transform'] = 'translate(.5 .5)' if STROKE_ODD_PIXEL_LENGTH
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  @items.each do |item|
         
     | 
| 
      
 51 
     | 
    
         
            +
                    if item.needs_space
         
     | 
| 
      
 52 
     | 
    
         
            +
                      Path.new(x, y).h(10).add(g)
         
     | 
| 
      
 53 
     | 
    
         
            +
                      x += 10
         
     | 
| 
      
 54 
     | 
    
         
            +
                    end
         
     | 
| 
      
 55 
     | 
    
         
            +
                    item.format(x, y, item.width).add(g)
         
     | 
| 
      
 56 
     | 
    
         
            +
                    x += item.width
         
     | 
| 
      
 57 
     | 
    
         
            +
                    y += item.height
         
     | 
| 
      
 58 
     | 
    
         
            +
                    if item.needs_space
         
     | 
| 
      
 59 
     | 
    
         
            +
                      Path.new(x, y).h(10).add(g)
         
     | 
| 
      
 60 
     | 
    
         
            +
                      x += 10
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  @attrs['width'] = (@width + padding_left + padding_right).to_s
         
     | 
| 
      
 65 
     | 
    
         
            +
                  @attrs['height'] = (@up + @height + @down + padding_top + padding_bottom).to_s
         
     | 
| 
      
 66 
     | 
    
         
            +
                  @attrs['viewBox'] = "0 0 #{@attrs['width']} #{@attrs['height']}"
         
     | 
| 
      
 67 
     | 
    
         
            +
                  g.add(self)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  @formatted = true
         
     | 
| 
      
 69 
     | 
    
         
            +
                  self
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 73 
     | 
    
         
            +
                  separator, = TextDiagram.get_parts(['separator'])
         
     | 
| 
      
 74 
     | 
    
         
            +
                  diagram_td = items[0].text_diagram
         
     | 
| 
      
 75 
     | 
    
         
            +
                  items[1..].each do |item|
         
     | 
| 
      
 76 
     | 
    
         
            +
                    item_td = item.text_diagram
         
     | 
| 
      
 77 
     | 
    
         
            +
                    item_td.expand(1, 1, 0, 0) if item.needs_space
         
     | 
| 
      
 78 
     | 
    
         
            +
                    diagram_td = diagram_td.append_right(separator)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  end
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                def write_svg(write)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  format unless @formatted
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                  super
         
     | 
| 
      
 86 
     | 
    
         
            +
                end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                def write_text(_write)
         
     | 
| 
      
 89 
     | 
    
         
            +
                  output = text_diagram
         
     | 
| 
      
 90 
     | 
    
         
            +
                  output = "#{output.lines.join("\n")}\n"
         
     | 
| 
      
 91 
     | 
    
         
            +
                  output = output.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"') if ESCAPE_HTML
         
     | 
| 
      
 92 
     | 
    
         
            +
                  write(output)
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                def write_standalone(write, css = nil)
         
     | 
| 
      
 96 
     | 
    
         
            +
                  format unless @formatted
         
     | 
| 
      
 97 
     | 
    
         
            +
                  css = Style.default_style if css
         
     | 
| 
      
 98 
     | 
    
         
            +
                  Style.new(css).add(self)
         
     | 
| 
      
 99 
     | 
    
         
            +
                  @attrs['xmlns'] = 'http://www.w3.org/2000/svg'
         
     | 
| 
      
 100 
     | 
    
         
            +
                  @attrs['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'
         
     | 
| 
      
 101 
     | 
    
         
            +
                  DiagramItem.write_svg(write)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  @children.pop
         
     | 
| 
      
 103 
     | 
    
         
            +
                  @attrs.delete('xmlns')
         
     | 
| 
      
 104 
     | 
    
         
            +
                  @attrs.delete('xmlns:xlink')
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
              end
         
     | 
| 
      
 107 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,77 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                attr_reader :up, :down, :height, :width, :needs_space, :attrs, :children
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(name, attrs: {}, text: nil)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @name = name
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @up = 0
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @height = 0
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @down = 0
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @width = 0
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @needs_space = false
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @attrs = attrs || {}
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @children = text ? [text] : []
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 23 
     | 
    
         
            +
                  raise NotImplementedError 'Virtual'
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def add(parent)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  parent.children.push self
         
     | 
| 
      
 28 
     | 
    
         
            +
                  self
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def write_svg(write)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  write.call("<#{@name}")
         
     | 
| 
      
 33 
     | 
    
         
            +
                  @attrs.sort.each do |name, value|
         
     | 
| 
      
 34 
     | 
    
         
            +
                    write.call(" #{name}=\"#{RailroadDiagrams.escape_attr(value)}\"")
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                  write.call('>')
         
     | 
| 
      
 37 
     | 
    
         
            +
                  write.call("\n") if @name in %w[g svg]
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @children.each do |child|
         
     | 
| 
      
 39 
     | 
    
         
            +
                    if child.is_a?(DiagramItem) || child.is_a?(Path) || child.is_a?(Style)
         
     | 
| 
      
 40 
     | 
    
         
            +
                      child.write_svg(write)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    else
         
     | 
| 
      
 42 
     | 
    
         
            +
                      write.call(RailroadDiagrams.escape_html(child))
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                  write.call("</#{@name}>")
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                def walk(_callback)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  callback(self)
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                def to_str
         
     | 
| 
      
 53 
     | 
    
         
            +
                  "DiagramItem(#{@name}, #{@attrs}, #{@children})"
         
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                private
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def wrap_string(value)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  if value.class <= DiagramItem
         
     | 
| 
      
 60 
     | 
    
         
            +
                    value
         
     | 
| 
      
 61 
     | 
    
         
            +
                  else
         
     | 
| 
      
 62 
     | 
    
         
            +
                    Terminal.new(value)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                def determine_gaps(outer, inner)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  diff = outer - inner
         
     | 
| 
      
 68 
     | 
    
         
            +
                  if INTERNAL_ALIGNMENT == 'left'
         
     | 
| 
      
 69 
     | 
    
         
            +
                    [0, diff]
         
     | 
| 
      
 70 
     | 
    
         
            +
                  elsif INTERNAL_ALIGNMENT == 'right'
         
     | 
| 
      
 71 
     | 
    
         
            +
                    [diff, 0]
         
     | 
| 
      
 72 
     | 
    
         
            +
                  else
         
     | 
| 
      
 73 
     | 
    
         
            +
                    [diff / 2, diff / 2]
         
     | 
| 
      
 74 
     | 
    
         
            +
                  end
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class DiagramMultiContainer < DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(name, items, attrs = nil, text = nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super(name, attrs:, text:)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @items = items.map { |item| wrap_string(item) }
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def walk(callback)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  callback(self)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @items.each { |item| item.walk(callback) }
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def to_str
         
     | 
| 
      
 20 
     | 
    
         
            +
                  "DiagramMultiContainer(#{@name}, #{@items}, #{@attrs}, #{@children})"
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class End < DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(type = 'simple')
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('path')
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @width = 20
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @up = 10
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @down = 10
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @type = type
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 14 
     | 
    
         
            +
                  "End(type=#{@type})"
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def format(x, y, _width)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @attrs['d'] =
         
     | 
| 
      
 19 
     | 
    
         
            +
                    if @type == 'simple'
         
     | 
| 
      
 20 
     | 
    
         
            +
                      "M #{x} #{y} h 20 m -10 -10 v 20 m 10 -20 v 20"
         
     | 
| 
      
 21 
     | 
    
         
            +
                    else
         
     | 
| 
      
 22 
     | 
    
         
            +
                      "M #{x} #{y} h 20 m 0 -10 v 20"
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
                  self
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 28 
     | 
    
         
            +
                  cross, line, tee_left = TextDiagram.get_parts(%w[cross line tee_left])
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end_node =
         
     | 
| 
      
 30 
     | 
    
         
            +
                    if @type == 'simple'
         
     | 
| 
      
 31 
     | 
    
         
            +
                      line + cross + tee_left
         
     | 
| 
      
 32 
     | 
    
         
            +
                    else
         
     | 
| 
      
 33 
     | 
    
         
            +
                      line + tee_left
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  TextDiagram.new(0, 0, [end_node])
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,75 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Group < DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(item, label = nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('g')
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @item = wrap_string(item)
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  @label =
         
     | 
| 
      
 10 
     | 
    
         
            +
                    if label.is_a?(DiagramItem)
         
     | 
| 
      
 11 
     | 
    
         
            +
                      label
         
     | 
| 
      
 12 
     | 
    
         
            +
                    elsif label
         
     | 
| 
      
 13 
     | 
    
         
            +
                      Comment.new(label)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  item_width = @item.width + (@item.needs_space ? 20 : 0)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  label_width = @label ? @label.width : 0
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @width = [item_width, label_width, AR * 2].max
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  @height = @item.height
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  @box_up = [@item.up + VS, AR].max
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @up = @box_up
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @up += @label.up + @label.height + @label.down if @label
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  @down = [@item.down + VS, AR].max
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  @needs_space = true
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 32 
     | 
    
         
            +
                  "Group(#{@item}, label=#{@label})"
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 42 
     | 
    
         
            +
                    'rect',
         
     | 
| 
      
 43 
     | 
    
         
            +
                    attrs: {
         
     | 
| 
      
 44 
     | 
    
         
            +
                      'x' => x,
         
     | 
| 
      
 45 
     | 
    
         
            +
                      'y' => y - @box_up,
         
     | 
| 
      
 46 
     | 
    
         
            +
                      'width' => @width,
         
     | 
| 
      
 47 
     | 
    
         
            +
                      'height' => @height + @box_up + @down,
         
     | 
| 
      
 48 
     | 
    
         
            +
                      'rx' => AR,
         
     | 
| 
      
 49 
     | 
    
         
            +
                      'ry' => AR,
         
     | 
| 
      
 50 
     | 
    
         
            +
                      'class' => 'group-box'
         
     | 
| 
      
 51 
     | 
    
         
            +
                    }
         
     | 
| 
      
 52 
     | 
    
         
            +
                  ).add(self)
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  @item.format(x, y, @width).add(self)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  @label.format(x, y - (@box_up + @label.down + @label.height), @width).add(self) if @label
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  self
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                def walk(callback)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  callback.call(self)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  item.walk(callback)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  label&.walk(callback)
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 67 
     | 
    
         
            +
                  diagram_td = TextDiagram.round_rect(@item.text_diagram, dashed: true)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  if @label
         
     | 
| 
      
 69 
     | 
    
         
            +
                    label_td = @label.text_diagram
         
     | 
| 
      
 70 
     | 
    
         
            +
                    diagram_td = label_td.append_below(diagram_td, [], move_entry: true, move_exit: true).expand(0, 0, 1, 0)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
                  diagram_td
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,247 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class HorizontalChoice < 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 
     | 
    
         
            +
                  all_but_last = @items[0...-1]
         
     | 
| 
      
 14 
     | 
    
         
            +
                  middles = @items[1...-1]
         
     | 
| 
      
 15 
     | 
    
         
            +
                  first = @items.first
         
     | 
| 
      
 16 
     | 
    
         
            +
                  last = @items.last
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @needs_space = false
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  @width =
         
     | 
| 
      
 20 
     | 
    
         
            +
                    AR + # starting track
         
     | 
| 
      
 21 
     | 
    
         
            +
                    (AR * 2 * (@items.size - 1)) + # inbetween tracks
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @items.sum { |x| x.width + (x.needs_space ? 20 : 0) } + # items
         
     | 
| 
      
 23 
     | 
    
         
            +
                    (last.height > 0 ? AR : 0) + # needs space to curve up
         
     | 
| 
      
 24 
     | 
    
         
            +
                    AR # ending track
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  # Always exits at entrance height
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @height = 0
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  # All but the last have a track running above them
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @upper_track = [AR * 2, VS, all_but_last.map(&:up).max + VS].max
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @up = [@upper_track, last.up].max
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  # All but the first have a track running below them
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # Last either straight-lines or curves up, so has different calculation
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @lower_track = [
         
     | 
| 
      
 36 
     | 
    
         
            +
                    VS,
         
     | 
| 
      
 37 
     | 
    
         
            +
                    middles.any? ? middles.map { |x| x.height + [x.down + VS, AR * 2].max }.max : 0,
         
     | 
| 
      
 38 
     | 
    
         
            +
                    last.height + last.down + VS
         
     | 
| 
      
 39 
     | 
    
         
            +
                  ].max
         
     | 
| 
      
 40 
     | 
    
         
            +
                  if first.height < @lower_track
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # Make sure there's at least 2*AR room between first exit and lower track
         
     | 
| 
      
 42 
     | 
    
         
            +
                    @lower_track = [@lower_track, first.height + (AR * 2)].max
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  @down = [@lower_track, first.height + first.down].max
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 48 
     | 
    
         
            +
                  items = @items.map(&:to_s).join(', ')
         
     | 
| 
      
 49 
     | 
    
         
            +
                  "HorizontalChoice(#{items})"
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # Hook up the two sides if self is narrower than its stated width.
         
     | 
| 
      
 54 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  first = @items.first
         
     | 
| 
      
 60 
     | 
    
         
            +
                  last = @items.last
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  # upper track
         
     | 
| 
      
 63 
     | 
    
         
            +
                  upper_span =
         
     | 
| 
      
 64 
     | 
    
         
            +
                    @items[0...-1].sum { |item| item.width + (item.needs_space ? 20 : 0) } +
         
     | 
| 
      
 65 
     | 
    
         
            +
                    ((@items.size - 2) * AR * 2) -
         
     | 
| 
      
 66 
     | 
    
         
            +
                    AR
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  Path.new(x, y)
         
     | 
| 
      
 69 
     | 
    
         
            +
                      .arc('se')
         
     | 
| 
      
 70 
     | 
    
         
            +
                      .up(@upper_track - (AR * 2))
         
     | 
| 
      
 71 
     | 
    
         
            +
                      .arc('wn')
         
     | 
| 
      
 72 
     | 
    
         
            +
                      .h(upper_span)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      .add(self)
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  # lower track
         
     | 
| 
      
 76 
     | 
    
         
            +
                  lower_span =
         
     | 
| 
      
 77 
     | 
    
         
            +
                    @items[1..].sum { |item| item.width + (item.needs_space ? 20 : 0) } +
         
     | 
| 
      
 78 
     | 
    
         
            +
                    ((@items.size - 2) * AR * 2) +
         
     | 
| 
      
 79 
     | 
    
         
            +
                    (last.height.positive? ? AR : 0) -
         
     | 
| 
      
 80 
     | 
    
         
            +
                    AR
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                  lower_start = x + AR + first.width + (first.needs_space ? 20 : 0) + (AR * 2)
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                  Path.new(lower_start, y + @lower_track)
         
     | 
| 
      
 85 
     | 
    
         
            +
                      .h(lower_span)
         
     | 
| 
      
 86 
     | 
    
         
            +
                      .arc('se')
         
     | 
| 
      
 87 
     | 
    
         
            +
                      .up(@lower_track - (AR * 2))
         
     | 
| 
      
 88 
     | 
    
         
            +
                      .arc('wn')
         
     | 
| 
      
 89 
     | 
    
         
            +
                      .add(self)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  # Items
         
     | 
| 
      
 92 
     | 
    
         
            +
                  @items.each_with_index do |item, i|
         
     | 
| 
      
 93 
     | 
    
         
            +
                    # input track
         
     | 
| 
      
 94 
     | 
    
         
            +
                    if i.zero?
         
     | 
| 
      
 95 
     | 
    
         
            +
                      Path.new(x, y)
         
     | 
| 
      
 96 
     | 
    
         
            +
                          .h(AR)
         
     | 
| 
      
 97 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 98 
     | 
    
         
            +
                      x += AR
         
     | 
| 
      
 99 
     | 
    
         
            +
                    else
         
     | 
| 
      
 100 
     | 
    
         
            +
                      Path.new(x, y - @upper_track)
         
     | 
| 
      
 101 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 102 
     | 
    
         
            +
                          .v(@upper_track - (AR * 2))
         
     | 
| 
      
 103 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 104 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 105 
     | 
    
         
            +
                      x += AR * 2
         
     | 
| 
      
 106 
     | 
    
         
            +
                    end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                    # item
         
     | 
| 
      
 109 
     | 
    
         
            +
                    item_width = item.width + (item.needs_space ? 20 : 0)
         
     | 
| 
      
 110 
     | 
    
         
            +
                    item.format(x, y, item_width).add(self)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    x += item_width
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    # output track
         
     | 
| 
      
 114 
     | 
    
         
            +
                    if i == @items.size - 1
         
     | 
| 
      
 115 
     | 
    
         
            +
                      if item.height.zero?
         
     | 
| 
      
 116 
     | 
    
         
            +
                        Path.new(x, y).h(AR).add(self)
         
     | 
| 
      
 117 
     | 
    
         
            +
                      else
         
     | 
| 
      
 118 
     | 
    
         
            +
                        Path.new(x, y + item.height).arc('se').add(self)
         
     | 
| 
      
 119 
     | 
    
         
            +
                      end
         
     | 
| 
      
 120 
     | 
    
         
            +
                    elsif i.zero? && item.height > @lower_track
         
     | 
| 
      
 121 
     | 
    
         
            +
                      # Needs to arc up to meet the lower track, not down.
         
     | 
| 
      
 122 
     | 
    
         
            +
                      if item.height - @lower_track >= AR * 2
         
     | 
| 
      
 123 
     | 
    
         
            +
                        Path.new(x, y + item.height)
         
     | 
| 
      
 124 
     | 
    
         
            +
                            .arc('se')
         
     | 
| 
      
 125 
     | 
    
         
            +
                            .v(@lower_track - item.height + (AR * 2))
         
     | 
| 
      
 126 
     | 
    
         
            +
                            .arc('wn')
         
     | 
| 
      
 127 
     | 
    
         
            +
                            .add(self)
         
     | 
| 
      
 128 
     | 
    
         
            +
                      else
         
     | 
| 
      
 129 
     | 
    
         
            +
                        # Not enough space to fit two arcs
         
     | 
| 
      
 130 
     | 
    
         
            +
                        # so just bail and draw a straight line for now.
         
     | 
| 
      
 131 
     | 
    
         
            +
                        Path.new(x, y + item.height)
         
     | 
| 
      
 132 
     | 
    
         
            +
                            .l(AR * 2, @lower_track - item.height)
         
     | 
| 
      
 133 
     | 
    
         
            +
                            .add(self)
         
     | 
| 
      
 134 
     | 
    
         
            +
                      end
         
     | 
| 
      
 135 
     | 
    
         
            +
                    else
         
     | 
| 
      
 136 
     | 
    
         
            +
                      Path.new(x, y + item.height)
         
     | 
| 
      
 137 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 138 
     | 
    
         
            +
                          .v(@lower_track - item.height - (AR * 2))
         
     | 
| 
      
 139 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 140 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 141 
     | 
    
         
            +
                    end
         
     | 
| 
      
 142 
     | 
    
         
            +
                  end
         
     | 
| 
      
 143 
     | 
    
         
            +
                  self
         
     | 
| 
      
 144 
     | 
    
         
            +
                end
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 147 
     | 
    
         
            +
                  line, line_vertical, roundcorner_bot_left, roundcorner_bot_right,
         
     | 
| 
      
 148 
     | 
    
         
            +
                  roundcorner_top_left, roundcorner_top_right = TextDiagram.get_parts(
         
     | 
| 
      
 149 
     | 
    
         
            +
                    %w[line line_vertical roundcorner_bot_left roundcorner_bot_right roundcorner_top_left
         
     | 
| 
      
 150 
     | 
    
         
            +
                       roundcorner_top_right]
         
     | 
| 
      
 151 
     | 
    
         
            +
                  )
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                  # Format all the child items, so we can know the maximum entry, exit, and height.
         
     | 
| 
      
 154 
     | 
    
         
            +
                  item_tds = @items.map(&:text_diagram)
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                  # diagram_entry: distance from top to lowest entry, aka distance from top to diagram entry, aka final diagram entry and exit.
         
     | 
| 
      
 157 
     | 
    
         
            +
                  diagram_entry = item_tds.map(&:entry).max
         
     | 
| 
      
 158 
     | 
    
         
            +
                  # soil_to_baseline: distance from top to lowest entry before rightmost item, aka distance from skip-over-items line to rightmost entry, aka SOIL height.
         
     | 
| 
      
 159 
     | 
    
         
            +
                  soil_to_baseline = item_tds[0..-2].map(&:entry).max || 0
         
     | 
| 
      
 160 
     | 
    
         
            +
                  # top_to_soil: distance from top to skip-over-items line.
         
     | 
| 
      
 161 
     | 
    
         
            +
                  top_to_soil = diagram_entry - soil_to_baseline
         
     | 
| 
      
 162 
     | 
    
         
            +
                  # baseline_to_suil: distance from lowest entry or exit after leftmost item to bottom, aka distance from entry to skip-under-items line, aka SUIL height.
         
     | 
| 
      
 163 
     | 
    
         
            +
                  baseline_to_suil = item_tds[1..-1].map { |td| td.height - [td.entry, td.exit].min }.max.to_i - 1
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                  # The diagram starts with a line from its entry up to skip-over-items line:
         
     | 
| 
      
 166 
     | 
    
         
            +
                  lines = Array.new(top_to_soil, '  ')
         
     | 
| 
      
 167 
     | 
    
         
            +
                  lines << (roundcorner_top_left + line)
         
     | 
| 
      
 168 
     | 
    
         
            +
                  lines += Array.new(soil_to_baseline, line_vertical + ' ')
         
     | 
| 
      
 169 
     | 
    
         
            +
                  lines << (roundcorner_bot_right + line)
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                  diagram_td = TextDiagram.new(lines.size - 1, lines.size - 1, lines)
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                  item_tds.each_with_index do |item_td, item_num|
         
     | 
| 
      
 174 
     | 
    
         
            +
                    if item_num > 0
         
     | 
| 
      
 175 
     | 
    
         
            +
                      # All items except the leftmost start with a line from the skip-over-items line down to their entry,
         
     | 
| 
      
 176 
     | 
    
         
            +
                      # with a joining-line across at the skip-under-items line:
         
     | 
| 
      
 177 
     | 
    
         
            +
                      lines = Array.new(top_to_soil, '  ')
         
     | 
| 
      
 178 
     | 
    
         
            +
                      # All such items except the rightmost also have a continuation of the skip-over-items line:
         
     | 
| 
      
 179 
     | 
    
         
            +
                      line_to_next_item = item_num == item_tds.size - 1 ? ' ' : line
         
     | 
| 
      
 180 
     | 
    
         
            +
                      lines << (roundcorner_top_right + line_to_next_item)
         
     | 
| 
      
 181 
     | 
    
         
            +
                      lines += Array.new(soil_to_baseline, line_vertical + ' ')
         
     | 
| 
      
 182 
     | 
    
         
            +
                      lines << (roundcorner_bot_left + line)
         
     | 
| 
      
 183 
     | 
    
         
            +
                      lines += Array.new(baseline_to_suil, '  ')
         
     | 
| 
      
 184 
     | 
    
         
            +
                      lines << (line * 2)
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
                      entry_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
         
     | 
| 
      
 187 
     | 
    
         
            +
                      diagram_td = diagram_td.append_right(entry_td, '')
         
     | 
| 
      
 188 
     | 
    
         
            +
                    end
         
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
                    part_td = TextDiagram.new(0, 0, [])
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                    if item_num < item_tds.size - 1
         
     | 
| 
      
 193 
     | 
    
         
            +
                      # All items except the rightmost start with a segment of the skip-over-items line at the top.
         
     | 
| 
      
 194 
     | 
    
         
            +
                      # followed by enough blank lines to push their entry down to the previous item's exit:
         
     | 
| 
      
 195 
     | 
    
         
            +
                      lines = []
         
     | 
| 
      
 196 
     | 
    
         
            +
                      lines << (line * item_td.width)
         
     | 
| 
      
 197 
     | 
    
         
            +
                      lines += Array.new(soil_to_baseline - item_td.entry, ' ' * item_td.width)
         
     | 
| 
      
 198 
     | 
    
         
            +
                      soil_segment = TextDiagram.new(0, 0, lines)
         
     | 
| 
      
 199 
     | 
    
         
            +
                      part_td = part_td.append_below(soil_segment, [])
         
     | 
| 
      
 200 
     | 
    
         
            +
                    end
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
      
 202 
     | 
    
         
            +
                    part_td = part_td.append_below(item_td, [], move_entry: true, move_exit: true)
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                    if item_num > 0
         
     | 
| 
      
 205 
     | 
    
         
            +
                      # All items except the leftmost end with enough blank lines to pad down to the skip-under-items
         
     | 
| 
      
 206 
     | 
    
         
            +
                      # line, followed by a segment of the skip-under-items line:
         
     | 
| 
      
 207 
     | 
    
         
            +
                      lines = Array.new(baseline_to_suil - (item_td.height - item_td.entry) + 1, ' ' * item_td.width)
         
     | 
| 
      
 208 
     | 
    
         
            +
                      lines << (line * item_td.width)
         
     | 
| 
      
 209 
     | 
    
         
            +
                      suil_segment = TextDiagram.new(0, 0, lines)
         
     | 
| 
      
 210 
     | 
    
         
            +
                      part_td = part_td.append_below(suil_segment, [])
         
     | 
| 
      
 211 
     | 
    
         
            +
                    end
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
                    diagram_td = diagram_td.append_right(part_td, '')
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                    if item_num < item_tds.size - 1
         
     | 
| 
      
 216 
     | 
    
         
            +
                      # All items except the rightmost have a line from their exit down to the skip-under-items line,
         
     | 
| 
      
 217 
     | 
    
         
            +
                      # with a joining-line across at the skip-over-items line:
         
     | 
| 
      
 218 
     | 
    
         
            +
                      lines = Array.new(top_to_soil, '  ')
         
     | 
| 
      
 219 
     | 
    
         
            +
                      lines << (line * 2)
         
     | 
| 
      
 220 
     | 
    
         
            +
                      lines += Array.new(diagram_td.exit - top_to_soil - 1, '  ')
         
     | 
| 
      
 221 
     | 
    
         
            +
                      lines << (line + roundcorner_top_right)
         
     | 
| 
      
 222 
     | 
    
         
            +
                      lines += Array.new(baseline_to_suil - (diagram_td.exit - diagram_td.entry), ' ' + line_vertical)
         
     | 
| 
      
 223 
     | 
    
         
            +
                      line_from_prev_item = item_num > 0 ? line : ' '
         
     | 
| 
      
 224 
     | 
    
         
            +
                      lines << (line_from_prev_item + roundcorner_bot_left)
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                      entry = diagram_entry + 1 + (diagram_td.exit - diagram_td.entry)
         
     | 
| 
      
 227 
     | 
    
         
            +
                      exit_td = TextDiagram.new(entry, diagram_entry + 1, lines)
         
     | 
| 
      
 228 
     | 
    
         
            +
                      diagram_td = diagram_td.append_right(exit_td, '')
         
     | 
| 
      
 229 
     | 
    
         
            +
                    else
         
     | 
| 
      
 230 
     | 
    
         
            +
                      # The rightmost item has a line from the skip-under-items line and from its exit up to the diagram exit:
         
     | 
| 
      
 231 
     | 
    
         
            +
                      lines = []
         
     | 
| 
      
 232 
     | 
    
         
            +
                      line_from_exit = diagram_td.exit == diagram_td.entry ? line : ' '
         
     | 
| 
      
 233 
     | 
    
         
            +
                      lines << (line_from_exit + roundcorner_top_left)
         
     | 
| 
      
 234 
     | 
    
         
            +
                      lines += Array.new(diagram_td.exit - diagram_td.entry - 1, ' ' + line_vertical)
         
     | 
| 
      
 235 
     | 
    
         
            +
                      lines << (line + roundcorner_bot_right) if diagram_td.exit != diagram_td.entry
         
     | 
| 
      
 236 
     | 
    
         
            +
                      lines += Array.new(baseline_to_suil - (diagram_td.exit - diagram_td.entry), ' ' + line_vertical)
         
     | 
| 
      
 237 
     | 
    
         
            +
                      lines << (line + roundcorner_bot_right)
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
                      exit_td = TextDiagram.new(diagram_td.exit - diagram_td.entry, 0, lines)
         
     | 
| 
      
 240 
     | 
    
         
            +
                      diagram_td = diagram_td.append_right(exit_td, '')
         
     | 
| 
      
 241 
     | 
    
         
            +
                    end
         
     | 
| 
      
 242 
     | 
    
         
            +
                  end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
                  diagram_td
         
     | 
| 
      
 245 
     | 
    
         
            +
                end
         
     | 
| 
      
 246 
     | 
    
         
            +
              end
         
     | 
| 
      
 247 
     | 
    
         
            +
            end
         
     |