railroad_diagrams 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/CHANGELOG.md +7 -0
 - data/LICENSE.txt +21 -0
 - data/MIT +21 -0
 - data/README.md +30 -0
 - data/Rakefile +4 -0
 - data/exe/railroad_diagrams +7 -0
 - data/lib/railroad_diagrams/alternating_sequence.rb +169 -0
 - data/lib/railroad_diagrams/choice.rb +209 -0
 - data/lib/railroad_diagrams/command.rb +84 -0
 - data/lib/railroad_diagrams/comment.rb +48 -0
 - data/lib/railroad_diagrams/diagram.rb +107 -0
 - data/lib/railroad_diagrams/diagram_item.rb +77 -0
 - data/lib/railroad_diagrams/diagram_multi_container.rb +23 -0
 - data/lib/railroad_diagrams/end.rb +39 -0
 - data/lib/railroad_diagrams/group.rb +75 -0
 - data/lib/railroad_diagrams/horizontal_choice.rb +247 -0
 - data/lib/railroad_diagrams/multiple_choice.rb +140 -0
 - data/lib/railroad_diagrams/non_terminal.rb +67 -0
 - data/lib/railroad_diagrams/one_or_more.rb +86 -0
 - data/lib/railroad_diagrams/optional.rb +9 -0
 - data/lib/railroad_diagrams/optional_sequence.rb +214 -0
 - data/lib/railroad_diagrams/path.rb +117 -0
 - data/lib/railroad_diagrams/sequence.rb +59 -0
 - data/lib/railroad_diagrams/skip.rb +26 -0
 - data/lib/railroad_diagrams/stack.rb +120 -0
 - data/lib/railroad_diagrams/start.rb +62 -0
 - data/lib/railroad_diagrams/style.rb +67 -0
 - data/lib/railroad_diagrams/terminal.rb +63 -0
 - data/lib/railroad_diagrams/text_diagram.rb +341 -0
 - data/lib/railroad_diagrams/version.rb +5 -0
 - data/lib/railroad_diagrams/zero_or_more.rb +9 -0
 - data/lib/railroad_diagrams.rb +50 -0
 - data/sample/sample.html +215 -0
 - data/test.rb +570 -0
 - metadata +81 -0
 
| 
         @@ -0,0 +1,140 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class MultipleChoice < DiagramMultiContainer
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(default, type, *items)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('g', items)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  raise ArgumentError, "default must be between 0 and #{items.length - 1}" unless (0...items.length).cover?(default)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  raise ArgumentError, "type must be 'any' or 'all'" unless %w[any all].include?(type)
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  @default = default
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @type = type
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @needs_space = true
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @inner_width = @items.map(&:width).max
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @width = 30 + AR + @inner_width + AR + 20
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @up = @items[0].up
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @down = @items[-1].down
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @height = @items[default].height
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  @items.each_with_index do |item, i|
         
     | 
| 
      
 20 
     | 
    
         
            +
                    minimum =
         
     | 
| 
      
 21 
     | 
    
         
            +
                      if [default - 1, default + 1].include?(i)
         
     | 
| 
      
 22 
     | 
    
         
            +
                        10 + AR
         
     | 
| 
      
 23 
     | 
    
         
            +
                      else
         
     | 
| 
      
 24 
     | 
    
         
            +
                        AR
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    if i < default
         
     | 
| 
      
 28 
     | 
    
         
            +
                      @up += [minimum, item.height + item.down + VS + @items[i + 1].up].max
         
     | 
| 
      
 29 
     | 
    
         
            +
                    elsif i > default
         
     | 
| 
      
 30 
     | 
    
         
            +
                      @down += [minimum, item.up + VS + @items[i - 1].down + @items[i - 1].height].max
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  @down -= @items[default].height # already counted in @height
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 38 
     | 
    
         
            +
                  items = @items.map(&:to_s).join(', ')
         
     | 
| 
      
 39 
     | 
    
         
            +
                  "MultipleChoice(#{@default}, #{@type}, #{items})"
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  # Hook up the two sides if self is narrower than its stated width.
         
     | 
| 
      
 46 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  default = @items[@default]
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  # Do the elements that curve above
         
     | 
| 
      
 53 
     | 
    
         
            +
                  above = @items[0...@default].reverse
         
     | 
| 
      
 54 
     | 
    
         
            +
                  distance_from_y = 0
         
     | 
| 
      
 55 
     | 
    
         
            +
                  distance_from_y = [10 + AR, default.up + VS + above.first.down + above.first.height].max if above.any?
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  double_enumerate(above).each do |i, ni, item|
         
     | 
| 
      
 58 
     | 
    
         
            +
                    Path.new(x + 30, y).up(distance_from_y - AR).arc('wn').add(self)
         
     | 
| 
      
 59 
     | 
    
         
            +
                    item.format(x + 30 + AR, y - distance_from_y, @inner_width).add(self)
         
     | 
| 
      
 60 
     | 
    
         
            +
                    Path.new(x + 30 + AR + @inner_width, y - distance_from_y + item.height)
         
     | 
| 
      
 61 
     | 
    
         
            +
                        .arc('ne')
         
     | 
| 
      
 62 
     | 
    
         
            +
                        .down(distance_from_y - item.height + default.height - AR - 10)
         
     | 
| 
      
 63 
     | 
    
         
            +
                        .add(self)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    distance_from_y += [AR, item.up + VS + above[i + 1].down + above[i + 1].height].max if ni < -1
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  # Do the straight-line path.
         
     | 
| 
      
 68 
     | 
    
         
            +
                  Path.new(x + 30, y).right(AR).add(self)
         
     | 
| 
      
 69 
     | 
    
         
            +
                  @items[@default].format(x + 30 + AR, y, @inner_width).add(self)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  Path.new(x + 30 + AR + @inner_width, y + @height).right(AR).add(self)
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                  # Do the elements that curve below
         
     | 
| 
      
 73 
     | 
    
         
            +
                  below = @items[(@default + 1)..] || []
         
     | 
| 
      
 74 
     | 
    
         
            +
                  distance_from_y = [10 + AR, default.height + default.down + VS + below.first.up].max if below.any?
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  below.each_with_index do |item, i|
         
     | 
| 
      
 77 
     | 
    
         
            +
                    Path.new(x + 30, y).down(distance_from_y - AR).arc('ws').add(self)
         
     | 
| 
      
 78 
     | 
    
         
            +
                    item.format(x + 30 + AR, y + distance_from_y, @inner_width).add(self)
         
     | 
| 
      
 79 
     | 
    
         
            +
                    Path.new(x + 30 + AR + @inner_width, y + distance_from_y + item.height)
         
     | 
| 
      
 80 
     | 
    
         
            +
                        .arc('se')
         
     | 
| 
      
 81 
     | 
    
         
            +
                        .up(distance_from_y - AR + item.height - default.height - 10)
         
     | 
| 
      
 82 
     | 
    
         
            +
                        .add(self)
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                    distance_from_y += [AR, item.height + item.down + VS + (below[i + 1]&.up || 0)].max
         
     | 
| 
      
 85 
     | 
    
         
            +
                  end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                  text = DiagramItem.new('g', attrs: { 'class' => 'diagram-text' }).add(self)
         
     | 
| 
      
 88 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 89 
     | 
    
         
            +
                    'title',
         
     | 
| 
      
 90 
     | 
    
         
            +
                    text: @type == 'any' ? 'take one or more branches, once each, in any order' : 'take all branches, once each, in any order'
         
     | 
| 
      
 91 
     | 
    
         
            +
                  ).add(text)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 94 
     | 
    
         
            +
                    'path',
         
     | 
| 
      
 95 
     | 
    
         
            +
                    attrs: {
         
     | 
| 
      
 96 
     | 
    
         
            +
                      'd' => "M #{x + 30} #{y - 10} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z",
         
     | 
| 
      
 97 
     | 
    
         
            +
                      'class' => 'diagram-text'
         
     | 
| 
      
 98 
     | 
    
         
            +
                    }
         
     | 
| 
      
 99 
     | 
    
         
            +
                  ).add(text)
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 102 
     | 
    
         
            +
                    'text',
         
     | 
| 
      
 103 
     | 
    
         
            +
                    text: @type == 'any' ? '1+' : 'all',
         
     | 
| 
      
 104 
     | 
    
         
            +
                    attrs: { 'x' => x + 15, 'y' => y + 4, 'class' => 'diagram-text' }
         
     | 
| 
      
 105 
     | 
    
         
            +
                  ).add(text)
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 108 
     | 
    
         
            +
                    'path',
         
     | 
| 
      
 109 
     | 
    
         
            +
                    attrs: {
         
     | 
| 
      
 110 
     | 
    
         
            +
                      'd' => "M #{x + @width - 20} #{y - 10} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z",
         
     | 
| 
      
 111 
     | 
    
         
            +
                      'class' => 'diagram-text'
         
     | 
| 
      
 112 
     | 
    
         
            +
                    }
         
     | 
| 
      
 113 
     | 
    
         
            +
                  ).add(text)
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 116 
     | 
    
         
            +
                    'text',
         
     | 
| 
      
 117 
     | 
    
         
            +
                    text: '↺',
         
     | 
| 
      
 118 
     | 
    
         
            +
                    attrs: { 'x' => x + @width - 10, 'y' => y + 4, 'class' => 'diagram-arrow' }
         
     | 
| 
      
 119 
     | 
    
         
            +
                  ).add(text)
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                  self
         
     | 
| 
      
 122 
     | 
    
         
            +
                end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 125 
     | 
    
         
            +
                  multi_repeat = TextDiagram.get_parts(['multi_repeat']).first
         
     | 
| 
      
 126 
     | 
    
         
            +
                  any_all = TextDiagram.rect(@type == 'any' ? '1+' : 'all')
         
     | 
| 
      
 127 
     | 
    
         
            +
                  diagram_td = Choice.text_diagram(self)
         
     | 
| 
      
 128 
     | 
    
         
            +
                  repeat_td = TextDiagram.rect(multi_repeat)
         
     | 
| 
      
 129 
     | 
    
         
            +
                  diagram_td = any_all.append_right(diagram_td, '')
         
     | 
| 
      
 130 
     | 
    
         
            +
                  diagram_td.append_right(repeat_td, '')
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                private
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
                def double_enumerate(seq)
         
     | 
| 
      
 136 
     | 
    
         
            +
                  length = seq.length
         
     | 
| 
      
 137 
     | 
    
         
            +
                  seq.each_with_index.map { |item, i| [i, i - length, item] }
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
              end
         
     | 
| 
      
 140 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,67 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class NonTerminal < DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(text, href = nil, title = nil, cls: '')
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('g', attrs: { 'class' => "non-terminal #{cls}" })
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @text = text
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @href = href
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @title = title
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @cls = cls
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @width = (text.length * CHAR_WIDTH) + 20
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @up = 11
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @down = 11
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @needs_space = true
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 18 
     | 
    
         
            +
                  "NonTerminal(#{@text}, href=#{@href}, title=#{@title}, cls=#{@cls})"
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  # Hook up the two sides if self is narrower than its stated width.
         
     | 
| 
      
 25 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y).h(right_gap).add(self)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  DiagramItem.new(
         
     | 
| 
      
 29 
     | 
    
         
            +
                    'rect',
         
     | 
| 
      
 30 
     | 
    
         
            +
                    attrs: {
         
     | 
| 
      
 31 
     | 
    
         
            +
                      'x' => x + left_gap,
         
     | 
| 
      
 32 
     | 
    
         
            +
                      'y' => y - 11,
         
     | 
| 
      
 33 
     | 
    
         
            +
                      'width' => @width,
         
     | 
| 
      
 34 
     | 
    
         
            +
                      'height' => @up + @down
         
     | 
| 
      
 35 
     | 
    
         
            +
                    }
         
     | 
| 
      
 36 
     | 
    
         
            +
                  ).add(self)
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  text = DiagramItem.new(
         
     | 
| 
      
 39 
     | 
    
         
            +
                    'text',
         
     | 
| 
      
 40 
     | 
    
         
            +
                    attrs: {
         
     | 
| 
      
 41 
     | 
    
         
            +
                      'x' => x + left_gap + (@width / 2),
         
     | 
| 
      
 42 
     | 
    
         
            +
                      'y' => y + 4
         
     | 
| 
      
 43 
     | 
    
         
            +
                    },
         
     | 
| 
      
 44 
     | 
    
         
            +
                    text: @text
         
     | 
| 
      
 45 
     | 
    
         
            +
                  )
         
     | 
| 
      
 46 
     | 
    
         
            +
                  if @href
         
     | 
| 
      
 47 
     | 
    
         
            +
                    a = DiagramItem.new(
         
     | 
| 
      
 48 
     | 
    
         
            +
                      'a',
         
     | 
| 
      
 49 
     | 
    
         
            +
                      attrs: {
         
     | 
| 
      
 50 
     | 
    
         
            +
                        'xlink:href' => @href
         
     | 
| 
      
 51 
     | 
    
         
            +
                      },
         
     | 
| 
      
 52 
     | 
    
         
            +
                      text:
         
     | 
| 
      
 53 
     | 
    
         
            +
                    ).add(self)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    text.add(a)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  else
         
     | 
| 
      
 56 
     | 
    
         
            +
                    text.add(self)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                  DiagramItem.new('title', attrs: {}, text: @title).add(self) if @title
         
     | 
| 
      
 59 
     | 
    
         
            +
                  self
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 63 
     | 
    
         
            +
                  # NOTE: href, title, and cls are ignored for text diagrams.
         
     | 
| 
      
 64 
     | 
    
         
            +
                  TextDiagram.rect(@text)
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class OneOrMore < DiagramItem
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(item, repeat = nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('g')
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @item = wrap_string(item)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  repeat ||= Skip.new
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @rep = wrap_string(repeat)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @width = [@item.width, @rep.width].max + (AR * 2)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @height = @item.height
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @up = @item.up
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @down = [AR * 2, @item.down + VS + @rep.up + @rep.height + @rep.down].max
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @needs_space = true
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 18 
     | 
    
         
            +
                  "OneOrMore(#{@item}, repeat=#{@rep})"
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  # Hook up the two sides if self is narrower than its stated width.
         
     | 
| 
      
 25 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  # Draw item
         
     | 
| 
      
 30 
     | 
    
         
            +
                  Path.new(x, y).right(AR).add(self)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @item.format(x + AR, y, @width - (AR * 2)).add(self)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  Path.new(x + @width - AR, y + @height).right(AR).add(self)
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  # Draw repeat arc
         
     | 
| 
      
 35 
     | 
    
         
            +
                  distance_from_y = [AR * 2, @item.height + @item.down + VS + @rep.up].max
         
     | 
| 
      
 36 
     | 
    
         
            +
                  Path.new(x + AR, y).arc('nw').down(distance_from_y - (AR * 2)).arc('ws').add(self)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @rep.format(x + AR, y + distance_from_y, @width - (AR * 2)).add(self)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  Path.new(x + @width - AR, y + distance_from_y + @rep.height)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      .arc('se')
         
     | 
| 
      
 40 
     | 
    
         
            +
                      .up(distance_from_y - (AR * 2) + @rep.height - @item.height)
         
     | 
| 
      
 41 
     | 
    
         
            +
                      .arc('en')
         
     | 
| 
      
 42 
     | 
    
         
            +
                      .add(self)
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  self
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 48 
     | 
    
         
            +
                  parts = TextDiagram.get_parts(
         
     | 
| 
      
 49 
     | 
    
         
            +
                    %w[
         
     | 
| 
      
 50 
     | 
    
         
            +
                      line repeat_top_left repeat_left repeat_bot_left repeat_top_right repeat_right repeat_bot_right
         
     | 
| 
      
 51 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 52 
     | 
    
         
            +
                  )
         
     | 
| 
      
 53 
     | 
    
         
            +
                  line, repeat_top_left, repeat_left, repeat_bot_left, repeat_top_right, repeat_right, repeat_bot_right = parts
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  # Format the item and then format the repeat append it to the bottom, after a spacer.
         
     | 
| 
      
 56 
     | 
    
         
            +
                  item_td = @item.text_diagram
         
     | 
| 
      
 57 
     | 
    
         
            +
                  repeat_td = @rep.text_diagram
         
     | 
| 
      
 58 
     | 
    
         
            +
                  fir_width = TextDiagram._max_width(item_td, repeat_td)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  repeat_td = repeat_td.expand(0, fir_width - repeat_td.width, 0, 0)
         
     | 
| 
      
 60 
     | 
    
         
            +
                  item_td = item_td.expand(0, fir_width - item_td.width, 0, 0)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  item_and_repeat_td = item_td.append_below(repeat_td, [])
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                  # Build the left side of the repeat line and append the combined item and repeat to its right.
         
     | 
| 
      
 64 
     | 
    
         
            +
                  left_lines = []
         
     | 
| 
      
 65 
     | 
    
         
            +
                  left_lines << (repeat_top_left + line)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  left_lines += [repeat_left + ' '] * ((item_td.height - item_td.entry) + repeat_td.entry - 1)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  left_lines << (repeat_bot_left + line)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  left_td = TextDiagram.new(0, 0, left_lines)
         
     | 
| 
      
 69 
     | 
    
         
            +
                  left_td = left_td.append_right(item_and_repeat_td, '')
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  # Build the right side of the repeat line and append it to the combined left side, item, and repeat's right.
         
     | 
| 
      
 72 
     | 
    
         
            +
                  right_lines = []
         
     | 
| 
      
 73 
     | 
    
         
            +
                  right_lines << (line + repeat_top_right)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  right_lines += [' ' + repeat_right] * ((item_td.height - item_td.exit) + repeat_td.exit - 1)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  right_lines << (line + repeat_bot_right)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  right_td = TextDiagram.new(0, 0, right_lines)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  left_td.append_right(right_td, '')
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                def walk(callback)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  callback.call(self)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  @item.walk(callback)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  @rep.walk(callback)
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,214 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class OptionalSequence < DiagramMultiContainer
         
     | 
| 
      
 5 
     | 
    
         
            +
                def self.new(*items)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  return Sequence.new(*items) if items.size <= 1
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  super
         
     | 
| 
      
 9 
     | 
    
         
            +
                end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                def initialize(*items)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  super('g', items)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @needs_space = false
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @width = 0
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @up = 0
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @height = @items.sum(&:height)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @down = @items.first.down
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  height_so_far = 0.0
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  @items.each_with_index do |item, i|
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @up = [@up, [AR * 2, item.up + VS].max - height_so_far].max
         
     | 
| 
      
 23 
     | 
    
         
            +
                    height_so_far += item.height
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                    if i.positive?
         
     | 
| 
      
 26 
     | 
    
         
            +
                      @down = [
         
     | 
| 
      
 27 
     | 
    
         
            +
                        @height + @down,
         
     | 
| 
      
 28 
     | 
    
         
            +
                        height_so_far + [AR * 2, item.down + VS].max
         
     | 
| 
      
 29 
     | 
    
         
            +
                      ].max - @height
         
     | 
| 
      
 30 
     | 
    
         
            +
                    end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                    item_width = item.width + (item.needs_space ? 10 : 0)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    @width += if i.zero?
         
     | 
| 
      
 34 
     | 
    
         
            +
                                AR + [item_width, AR].max
         
     | 
| 
      
 35 
     | 
    
         
            +
                              else
         
     | 
| 
      
 36 
     | 
    
         
            +
                                (AR * 2) + [item_width, AR].max + AR
         
     | 
| 
      
 37 
     | 
    
         
            +
                              end
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 42 
     | 
    
         
            +
                  items = @items.map(&:to_s).join(', ')
         
     | 
| 
      
 43 
     | 
    
         
            +
                  "OptionalSequence(#{items})"
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  Path.new(x, y).right(left_gap).add(self)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).right(right_gap).add(self)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 51 
     | 
    
         
            +
                  upper_line_y = y - @up
         
     | 
| 
      
 52 
     | 
    
         
            +
                  last = @items.size - 1
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                  @items.each_with_index do |item, i|
         
     | 
| 
      
 55 
     | 
    
         
            +
                    item_space = item.needs_space ? 10 : 0
         
     | 
| 
      
 56 
     | 
    
         
            +
                    item_width = item.width + item_space
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    if i.zero?
         
     | 
| 
      
 59 
     | 
    
         
            +
                      # Upper skip
         
     | 
| 
      
 60 
     | 
    
         
            +
                      Path.new(x, y)
         
     | 
| 
      
 61 
     | 
    
         
            +
                          .arc('se')
         
     | 
| 
      
 62 
     | 
    
         
            +
                          .up(y - upper_line_y - (AR * 2))
         
     | 
| 
      
 63 
     | 
    
         
            +
                          .arc('wn')
         
     | 
| 
      
 64 
     | 
    
         
            +
                          .right(item_width - AR)
         
     | 
| 
      
 65 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 66 
     | 
    
         
            +
                          .down(y + item.height - upper_line_y - (AR * 2))
         
     | 
| 
      
 67 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 68 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                      # Straight line
         
     | 
| 
      
 71 
     | 
    
         
            +
                      Path.new(x, y).right(item_space + AR).add(self)
         
     | 
| 
      
 72 
     | 
    
         
            +
                      item.format(x + item_space + AR, y, item.width).add(self)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      x += item_width + AR
         
     | 
| 
      
 74 
     | 
    
         
            +
                      y += item.height
         
     | 
| 
      
 75 
     | 
    
         
            +
                    elsif i < last
         
     | 
| 
      
 76 
     | 
    
         
            +
                      # Upper skip
         
     | 
| 
      
 77 
     | 
    
         
            +
                      Path.new(x, upper_line_y)
         
     | 
| 
      
 78 
     | 
    
         
            +
                          .right((AR * 2) + [item_width, AR].max + AR)
         
     | 
| 
      
 79 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 80 
     | 
    
         
            +
                          .down(y - upper_line_y + item.height - (AR * 2))
         
     | 
| 
      
 81 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 82 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                      # Straight line
         
     | 
| 
      
 85 
     | 
    
         
            +
                      Path.new(x, y).right(AR * 2).add(self)
         
     | 
| 
      
 86 
     | 
    
         
            +
                      item.format(x + (AR * 2), y, item.width).add(self)
         
     | 
| 
      
 87 
     | 
    
         
            +
                      Path.new(x + item.width + (AR * 2), y + item.height)
         
     | 
| 
      
 88 
     | 
    
         
            +
                          .right(item_space + AR)
         
     | 
| 
      
 89 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                      # Lower skip
         
     | 
| 
      
 92 
     | 
    
         
            +
                      Path.new(x, y)
         
     | 
| 
      
 93 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 94 
     | 
    
         
            +
                          .down(item.height + [item.down + VS, AR * 2].max - (AR * 2))
         
     | 
| 
      
 95 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 96 
     | 
    
         
            +
                          .right(item_width - AR)
         
     | 
| 
      
 97 
     | 
    
         
            +
                          .arc('se')
         
     | 
| 
      
 98 
     | 
    
         
            +
                          .up(item.down + VS - (AR * 2))
         
     | 
| 
      
 99 
     | 
    
         
            +
                          .arc('wn')
         
     | 
| 
      
 100 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                      x += (AR * 2) + [item_width, AR].max + AR
         
     | 
| 
      
 103 
     | 
    
         
            +
                      y += item.height
         
     | 
| 
      
 104 
     | 
    
         
            +
                    else
         
     | 
| 
      
 105 
     | 
    
         
            +
                      # Straight line
         
     | 
| 
      
 106 
     | 
    
         
            +
                      Path.new(x, y).right(AR * 2).add(self)
         
     | 
| 
      
 107 
     | 
    
         
            +
                      item.format(x + (AR * 2), y, item.width).add(self)
         
     | 
| 
      
 108 
     | 
    
         
            +
                      Path.new(x + (AR * 2) + item.width, y + item.height)
         
     | 
| 
      
 109 
     | 
    
         
            +
                          .right(item_space + AR)
         
     | 
| 
      
 110 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                      # Lower skip
         
     | 
| 
      
 113 
     | 
    
         
            +
                      Path.new(x, y)
         
     | 
| 
      
 114 
     | 
    
         
            +
                          .arc('ne')
         
     | 
| 
      
 115 
     | 
    
         
            +
                          .down(item.height + [item.down + VS, AR * 2].max - (AR * 2))
         
     | 
| 
      
 116 
     | 
    
         
            +
                          .arc('ws')
         
     | 
| 
      
 117 
     | 
    
         
            +
                          .right(item_width - AR)
         
     | 
| 
      
 118 
     | 
    
         
            +
                          .arc('se')
         
     | 
| 
      
 119 
     | 
    
         
            +
                          .up(item.down + VS - (AR * 2))
         
     | 
| 
      
 120 
     | 
    
         
            +
                          .arc('wn')
         
     | 
| 
      
 121 
     | 
    
         
            +
                          .add(self)
         
     | 
| 
      
 122 
     | 
    
         
            +
                    end
         
     | 
| 
      
 123 
     | 
    
         
            +
                  end
         
     | 
| 
      
 124 
     | 
    
         
            +
                  self
         
     | 
| 
      
 125 
     | 
    
         
            +
                end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 128 
     | 
    
         
            +
                  line, line_vertical, roundcorner_bot_left, roundcorner_bot_right,
         
     | 
| 
      
 129 
     | 
    
         
            +
                  roundcorner_top_left, roundcorner_top_right = TextDiagram.get_parts(
         
     | 
| 
      
 130 
     | 
    
         
            +
                    %w[line line_vertical roundcorner_bot_left roundcorner_bot_right roundcorner_top_left roundcorner_top_right]
         
     | 
| 
      
 131 
     | 
    
         
            +
                  )
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                  # Format all the child items, so we can know the maximum entry.
         
     | 
| 
      
 134 
     | 
    
         
            +
                  item_tds = @items.map(&:text_diagram)
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                  # diagramEntry: distance from top to lowest entry, aka distance from top to diagram entry, aka final diagram entry and exit.
         
     | 
| 
      
 137 
     | 
    
         
            +
                  diagram_entry = item_tds.map(&:entry).max
         
     | 
| 
      
 138 
     | 
    
         
            +
                  # SOILHeight: distance from top to lowest entry before rightmost item, aka distance from skip-over-items line to rightmost entry, aka SOIL height.
         
     | 
| 
      
 139 
     | 
    
         
            +
                  soil_height = item_tds[0...-1].map(&:entry).max
         
     | 
| 
      
 140 
     | 
    
         
            +
                  # topToSOIL: distance from top to skip-over-items line.
         
     | 
| 
      
 141 
     | 
    
         
            +
                  top_to_soil = diagram_entry - soil_height
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                  # The diagram starts with a line from its entry up to the skip-over-items line:
         
     | 
| 
      
 144 
     | 
    
         
            +
                  lines = ['  '] * top_to_soil
         
     | 
| 
      
 145 
     | 
    
         
            +
                  lines += [roundcorner_top_left + line]
         
     | 
| 
      
 146 
     | 
    
         
            +
                  lines += ["#{line_vertical} "] * soil_height
         
     | 
| 
      
 147 
     | 
    
         
            +
                  lines += [roundcorner_bot_right + line]
         
     | 
| 
      
 148 
     | 
    
         
            +
                  diagram_td = TextDiagram.new(lines.size - 1, lines.size - 1, lines)
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                  @items.each_with_index do |item_td, i|
         
     | 
| 
      
 151 
     | 
    
         
            +
                    if i.positive?
         
     | 
| 
      
 152 
     | 
    
         
            +
                      # All items except the leftmost start with a line from their entry down to their skip-under-item line,
         
     | 
| 
      
 153 
     | 
    
         
            +
                      # with a joining-line across at the skip-over-items line:
         
     | 
| 
      
 154 
     | 
    
         
            +
                      lines = (['  '] * top_to_soil) + [line * 2] +
         
     | 
| 
      
 155 
     | 
    
         
            +
                              (['  '] * (diagram_td.exit - top_to_soil - 1)) +
         
     | 
| 
      
 156 
     | 
    
         
            +
                              [line + roundcorner_top_right] +
         
     | 
| 
      
 157 
     | 
    
         
            +
                              ([" #{line_vertical}"] * (item_td.height - item_td.entry - 1)) +
         
     | 
| 
      
 158 
     | 
    
         
            +
                              [" #{roundcorner_bot_left}"]
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                      skip_down_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
         
     | 
| 
      
 161 
     | 
    
         
            +
                      diagram_td = diagram_td.append_right(skip_down_td, '')
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                      # All items except the leftmost next have a line from skip-over-items line down to their entry,
         
     | 
| 
      
 164 
     | 
    
         
            +
                      # with joining-lines at their entry and at their skip-under-item line:
         
     | 
| 
      
 165 
     | 
    
         
            +
                      lines = (['  '] * top_to_soil) + [line + roundcorner_top_right +
         
     | 
| 
      
 166 
     | 
    
         
            +
                                                        # All such items except the rightmost also have a continuation of the skip-over-items line:
         
     | 
| 
      
 167 
     | 
    
         
            +
                                                        (i < item_tds.size - 1 ? line : ' ')] +
         
     | 
| 
      
 168 
     | 
    
         
            +
                              ([" #{line_vertical} "] * (diagram_td.exit - top_to_soil - 1)) +
         
     | 
| 
      
 169 
     | 
    
         
            +
                              [line + roundcorner_bot_left + line] +
         
     | 
| 
      
 170 
     | 
    
         
            +
                              ([' ' * 3] * (item_td.height - item_td.entry - 1)) +
         
     | 
| 
      
 171 
     | 
    
         
            +
                              [line * 3]
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                      entry_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
         
     | 
| 
      
 174 
     | 
    
         
            +
                      diagram_td = diagram_td.append_right(entry_td, '')
         
     | 
| 
      
 175 
     | 
    
         
            +
                    end
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                    part_td = TextDiagram.new(0, 0, [])
         
     | 
| 
      
 178 
     | 
    
         
            +
                    if i < item_tds.size - 1
         
     | 
| 
      
 179 
     | 
    
         
            +
                      # All items except the rightmost have a segment of the skip-over-items line at the top,
         
     | 
| 
      
 180 
     | 
    
         
            +
                      # followed by enough blank lines to push their entry down to the previous item's exit:
         
     | 
| 
      
 181 
     | 
    
         
            +
                      lines = [line * item_td.width] + ([' ' * item_td.width] * (soil_height - item_td.entry))
         
     | 
| 
      
 182 
     | 
    
         
            +
                      soil_segment = TextDiagram.new(0, 0, lines)
         
     | 
| 
      
 183 
     | 
    
         
            +
                      part_td = part_td.append_below(soil_segment, [])
         
     | 
| 
      
 184 
     | 
    
         
            +
                    end
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
                    part_td = part_td.append_below(item_td, [], move_entry: true, move_exit: true)
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                    if i.positive?
         
     | 
| 
      
 189 
     | 
    
         
            +
                      # All items except the leftmost have their skip-under-item line at the bottom.
         
     | 
| 
      
 190 
     | 
    
         
            +
                      soil_segment = TextDiagram.new(0, 0, [line * item_td.width])
         
     | 
| 
      
 191 
     | 
    
         
            +
                      part_td = part_td.append_below(soil_segment, [])
         
     | 
| 
      
 192 
     | 
    
         
            +
                    end
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                    diagram_td = diagram_td.append_right(part_td, '')
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                    next unless i.positive?
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
                    # All items except the leftmost have a line from their skip-under-item line to their exit:
         
     | 
| 
      
 199 
     | 
    
         
            +
                    lines = (['  '] * top_to_soil) +
         
     | 
| 
      
 200 
     | 
    
         
            +
                            # All such items except the rightmost also have a joining-line across at the skip-over-items line:
         
     | 
| 
      
 201 
     | 
    
         
            +
                            [(i < item_tds.size - 1 ? line * 2 : '  ')] +
         
     | 
| 
      
 202 
     | 
    
         
            +
                            (['  '] * (diagram_td.exit - top_to_soil - 1)) +
         
     | 
| 
      
 203 
     | 
    
         
            +
                            [line + roundcorner_top_left] +
         
     | 
| 
      
 204 
     | 
    
         
            +
                            ([" #{line_vertical}"] * (part_td.height - part_td.exit - 2)) +
         
     | 
| 
      
 205 
     | 
    
         
            +
                            [line + roundcorner_bot_right]
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                    skip_up_td = TextDiagram.new(diagram_td.exit, diagram_td.exit, lines)
         
     | 
| 
      
 208 
     | 
    
         
            +
                    diagram_td = diagram_td.append_right(skip_up_td, '')
         
     | 
| 
      
 209 
     | 
    
         
            +
                  end
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                  diagram_td
         
     | 
| 
      
 212 
     | 
    
         
            +
                end
         
     | 
| 
      
 213 
     | 
    
         
            +
              end
         
     | 
| 
      
 214 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,117 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Path
         
     | 
| 
      
 5 
     | 
    
         
            +
                attr_reader :x, :y, :attrs
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(x, y)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @x = x
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @y = y
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @attrs = { 'd' => "M#{x} #{y}" }
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def m(x, y)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @attrs['d'] += "m#{x} #{y}"
         
     | 
| 
      
 15 
     | 
    
         
            +
                  self
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def l(x, y)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @attrs['d'] += "l#{x} #{y}"
         
     | 
| 
      
 20 
     | 
    
         
            +
                  self
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                def h(val)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @attrs['d'] += "h#{val}"
         
     | 
| 
      
 25 
     | 
    
         
            +
                  self
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def right(val)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  h([0, val].max)
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def left(val)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  h(-[0, val].max)
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def v(val)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @attrs['d'] += "v#{val}"
         
     | 
| 
      
 38 
     | 
    
         
            +
                  self
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def down(val)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  v([0, val].max)
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def up(val)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  v(-[0, val].max)
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def arc_8(start, dir)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  arc = AR
         
     | 
| 
      
 51 
     | 
    
         
            +
                  s2 = 1 / Math.sqrt(2) * arc
         
     | 
| 
      
 52 
     | 
    
         
            +
                  s2inv = arc - s2
         
     | 
| 
      
 53 
     | 
    
         
            +
                  sweep = dir == 'cw' ? '1' : '0'
         
     | 
| 
      
 54 
     | 
    
         
            +
                  path = "a #{arc} #{arc} 0 0 #{sweep} "
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  sd = start + dir
         
     | 
| 
      
 57 
     | 
    
         
            +
                  offset = case sd
         
     | 
| 
      
 58 
     | 
    
         
            +
                           when 'ncw' then [s2, s2inv]
         
     | 
| 
      
 59 
     | 
    
         
            +
                           when 'necw' then [s2inv, s2]
         
     | 
| 
      
 60 
     | 
    
         
            +
                           when 'ecw' then [-s2inv, s2]
         
     | 
| 
      
 61 
     | 
    
         
            +
                           when 'secw' then [-s2, s2inv]
         
     | 
| 
      
 62 
     | 
    
         
            +
                           when 'scw' then [-s2, -s2inv]
         
     | 
| 
      
 63 
     | 
    
         
            +
                           when 'swcw' then [-s2inv, -s2]
         
     | 
| 
      
 64 
     | 
    
         
            +
                           when 'wcw' then [s2inv, -s2]
         
     | 
| 
      
 65 
     | 
    
         
            +
                           when 'nwcw' then [s2, -s2inv]
         
     | 
| 
      
 66 
     | 
    
         
            +
                           when 'nccw' then [-s2, s2inv]
         
     | 
| 
      
 67 
     | 
    
         
            +
                           when 'nwccw' then [-s2inv, s2]
         
     | 
| 
      
 68 
     | 
    
         
            +
                           when 'wccw' then [s2inv, s2]
         
     | 
| 
      
 69 
     | 
    
         
            +
                           when 'swccw' then [s2, s2inv]
         
     | 
| 
      
 70 
     | 
    
         
            +
                           when 'sccw' then [s2, -s2inv]
         
     | 
| 
      
 71 
     | 
    
         
            +
                           when 'seccw' then [s2inv, -s2]
         
     | 
| 
      
 72 
     | 
    
         
            +
                           when 'eccw' then [-s2inv, -s2]
         
     | 
| 
      
 73 
     | 
    
         
            +
                           when 'neccw' then [-s2, -s2inv]
         
     | 
| 
      
 74 
     | 
    
         
            +
                           end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  path += offset.map(&:to_s).join(' ')
         
     | 
| 
      
 77 
     | 
    
         
            +
                  @attrs['d'] += path
         
     | 
| 
      
 78 
     | 
    
         
            +
                  self
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                def arc(sweep)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  x = AR
         
     | 
| 
      
 83 
     | 
    
         
            +
                  y = AR
         
     | 
| 
      
 84 
     | 
    
         
            +
                  x *= -1 if sweep[0] == 'e' || sweep[1] == 'w'
         
     | 
| 
      
 85 
     | 
    
         
            +
                  y *= -1 if sweep[0] == 's' || sweep[1] == 'n'
         
     | 
| 
      
 86 
     | 
    
         
            +
                  cw = %w[ne es sw wn].include?(sweep) ? 1 : 0
         
     | 
| 
      
 87 
     | 
    
         
            +
                  @attrs['d'] += "a#{AR} #{AR} 0 0 #{cw} #{x} #{y}"
         
     | 
| 
      
 88 
     | 
    
         
            +
                  self
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def add(parent)
         
     | 
| 
      
 92 
     | 
    
         
            +
                  parent.children << self
         
     | 
| 
      
 93 
     | 
    
         
            +
                  self
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                def write_svg(write)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  write.call('<path')
         
     | 
| 
      
 98 
     | 
    
         
            +
                  @attrs.sort.each do |name, value|
         
     | 
| 
      
 99 
     | 
    
         
            +
                    write.call(" #{name}=\"#{RailroadDiagrams.escape_attr(value)}\"")
         
     | 
| 
      
 100 
     | 
    
         
            +
                  end
         
     | 
| 
      
 101 
     | 
    
         
            +
                  write.call(' />')
         
     | 
| 
      
 102 
     | 
    
         
            +
                end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                def format
         
     | 
| 
      
 105 
     | 
    
         
            +
                  @attrs['d'] += 'h.5'
         
     | 
| 
      
 106 
     | 
    
         
            +
                  self
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 110 
     | 
    
         
            +
                  TextDiagram.new(0, 0, [])
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 114 
     | 
    
         
            +
                  "Path(#{@x.inspect}, #{@y.inspect})"
         
     | 
| 
      
 115 
     | 
    
         
            +
                end
         
     | 
| 
      
 116 
     | 
    
         
            +
              end
         
     | 
| 
      
 117 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RailroadDiagrams
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Sequence < DiagramMultiContainer
         
     | 
| 
      
 5 
     | 
    
         
            +
                def initialize(*items)
         
     | 
| 
      
 6 
     | 
    
         
            +
                  super('g', items)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @needs_space = false
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @up = 0
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @down = 0
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @height = 0
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @width = 0
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @items.each do |item|
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @width += item.width + (item.needs_space ? 20 : 0)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @up = [@up, item.up - @height].max
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @height += item.height
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @down = [@down - item.height, item.down].max
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @width -= 10 if @items[0].needs_space
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @width -= 10 if @items[-1].needs_space
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def to_s
         
     | 
| 
      
 23 
     | 
    
         
            +
                  items = @items.map(&:to_s).join(', ')
         
     | 
| 
      
 24 
     | 
    
         
            +
                  "Sequence(#{items})"
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def format(x, y, width)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  left_gap, right_gap = determine_gaps(width, @width)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  Path.new(x, y).h(left_gap).add(self)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  x += left_gap
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @items.each_with_index do |item, i|
         
     | 
| 
      
 33 
     | 
    
         
            +
                    if item.needs_space && i.positive?
         
     | 
| 
      
 34 
     | 
    
         
            +
                      Path.new(x, y).h(10).add(self)
         
     | 
| 
      
 35 
     | 
    
         
            +
                      x += 10
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
                    item.format(x, y, item.width).add(self)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    x += item.width
         
     | 
| 
      
 39 
     | 
    
         
            +
                    y += item.height
         
     | 
| 
      
 40 
     | 
    
         
            +
                    if item.needs_space && i < @items.length - 1
         
     | 
| 
      
 41 
     | 
    
         
            +
                      Path.new(x, y).h(10).add(self)
         
     | 
| 
      
 42 
     | 
    
         
            +
                      x += 10
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                  self
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                def text_diagram
         
     | 
| 
      
 49 
     | 
    
         
            +
                  separator, = TextDiagram.get_parts(['separator'])
         
     | 
| 
      
 50 
     | 
    
         
            +
                  diagram_td = TextDiagram.new(0, 0, [''])
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @items.each do |item|
         
     | 
| 
      
 52 
     | 
    
         
            +
                    item_td = item.text_diagram
         
     | 
| 
      
 53 
     | 
    
         
            +
                    item_td = item_td.expand(1, 1, 0, 0) if item.needs_space
         
     | 
| 
      
 54 
     | 
    
         
            +
                    diagram_td = diagram_td.append_right(item_td, separator)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                  diagram_td
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     |