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