Class: RailroadDiagrams::TextDiagram

Inherits:
Object
  • Object
show all
Defined in:
lib/railroad_diagrams/text_diagram.rb

Constant Summary collapse

PARTS_UNICODE =
{
  'cross_diag' => '╳',
  'corner_bot_left' => '└',
  'corner_bot_right' => '┘',
  'corner_top_left' => '┌',
  'corner_top_right' => '┐',
  'cross' => '┼',
  'left' => '│',
  'line' => '─',
  'line_vertical' => '│',
  'multi_repeat' => '↺',
  'rect_bot' => '─',
  'rect_bot_dashed' => '┄',
  'rect_bot_left' => '└',
  'rect_bot_right' => '┘',
  'rect_left' => '│',
  'rect_left_dashed' => '┆',
  'rect_right' => '│',
  'rect_right_dashed' => '┆',
  'rect_top' => '─',
  'rect_top_dashed' => '┄',
  'rect_top_left' => '┌',
  'rect_top_right' => '┐',
  'repeat_bot_left' => '╰',
  'repeat_bot_right' => '╯',
  'repeat_left' => '│',
  'repeat_right' => '│',
  'repeat_top_left' => '╭',
  'repeat_top_right' => '╮',
  'right' => '│',
  'roundcorner_bot_left' => '╰',
  'roundcorner_bot_right' => '╯',
  'roundcorner_top_left' => '╭',
  'roundcorner_top_right' => '╮',
  'roundrect_bot' => '─',
  'roundrect_bot_dashed' => '┄',
  'roundrect_bot_left' => '╰',
  'roundrect_bot_right' => '╯',
  'roundrect_left' => '│',
  'roundrect_left_dashed' => '┆',
  'roundrect_right' => '│',
  'roundrect_right_dashed' => '┆',
  'roundrect_top' => '─',
  'roundrect_top_dashed' => '┄',
  'roundrect_top_left' => '╭',
  'roundrect_top_right' => '╮',
  'separator' => '─',
  'tee_left' => '┤',
  'tee_right' => '├'
}.freeze
PARTS_ASCII =
{
  'cross_diag' => 'X',
  'corner_bot_left' => '\\',
  'corner_bot_right' => '/',
  'corner_top_left' => '/',
  'corner_top_right' => '\\',
  'cross' => '+',
  'left' => '|',
  'line' => '-',
  'line_vertical' => '|',
  'multi_repeat' => '&',
  'rect_bot' => '-',
  'rect_bot_dashed' => '-',
  'rect_bot_left' => '+',
  'rect_bot_right' => '+',
  'rect_left' => '|',
  'rect_left_dashed' => '|',
  'rect_right' => '|',
  'rect_right_dashed' => '|',
  'rect_top' => '-',
  'rect_top_dashed' => '-',
  'rect_top_left' => '+',
  'rect_top_right' => '+',
  'repeat_bot_left' => '\\',
  'repeat_bot_right' => '/',
  'repeat_left' => '|',
  'repeat_right' => '|',
  'repeat_top_left' => '/',
  'repeat_top_right' => '\\',
  'right' => '|',
  'roundcorner_bot_left' => '\\',
  'roundcorner_bot_right' => '/',
  'roundcorner_top_left' => '/',
  'roundcorner_top_right' => '\\',
  'roundrect_bot' => '-',
  'roundrect_bot_dashed' => '-',
  'roundrect_bot_left' => '\\',
  'roundrect_bot_right' => '/',
  'roundrect_left' => '|',
  'roundrect_left_dashed' => '|',
  'roundrect_right' => '|',
  'roundrect_right_dashed' => '|',
  'roundrect_top' => '-',
  'roundrect_top_dashed' => '-',
  'roundrect_top_left' => '/',
  'roundrect_top_right' => '\\',
  'separator' => '-',
  'tee_left' => '|',
  'tee_right' => '|'
}.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entry, exit, lines) ⇒ TextDiagram

Returns a new instance of TextDiagram.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/railroad_diagrams/text_diagram.rb', line 247

def initialize(entry, exit, lines)
  @entry = entry
  @exit = exit
  @lines = lines.dup
  @height = lines.size
  @width = lines.any? ? lines[0].length : 0

  raise "Entry is not within diagram vertically:\n#{dump(false)}" unless entry <= lines.length
  raise "Exit is not within diagram vertically:\n#{dump(false)}" unless exit <= lines.length

  lines.each do |line|
    raise "Diagram data is not rectangular:\n#{dump(false)}" unless lines[0].length == line.length
  end
end

Class Attribute Details

.partsObject

Returns the value of attribute parts.



108
109
110
# File 'lib/railroad_diagrams/text_diagram.rb', line 108

def parts
  @parts
end

Instance Attribute Details

#entryObject (readonly)

Returns the value of attribute entry.



245
246
247
# File 'lib/railroad_diagrams/text_diagram.rb', line 245

def entry
  @entry
end

#exitObject (readonly)

Returns the value of attribute exit.



245
246
247
# File 'lib/railroad_diagrams/text_diagram.rb', line 245

def exit
  @exit
end

#heightObject (readonly)

Returns the value of attribute height.



245
246
247
# File 'lib/railroad_diagrams/text_diagram.rb', line 245

def height
  @height
end

#linesObject (readonly)

Returns the value of attribute lines.



245
246
247
# File 'lib/railroad_diagrams/text_diagram.rb', line 245

def lines
  @lines
end

#widthObject (readonly)

Returns the value of attribute width.



245
246
247
# File 'lib/railroad_diagrams/text_diagram.rb', line 245

def width
  @width
end

Class Method Details

.enclose_lines(lines, lefts, rights) ⇒ Object



165
166
167
168
169
170
171
# File 'lib/railroad_diagrams/text_diagram.rb', line 165

def enclose_lines(lines, lefts, rights)
  unless lines.length == lefts.length && lines.length == rights.length
    raise 'All arguments must be the same length'
  end

  lines.each_with_index.map { |line, i| lefts[i] + line + rights[i] }
end

.gaps(outer_width, inner_width) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/railroad_diagrams/text_diagram.rb', line 173

def gaps(outer_width, inner_width)
  diff = outer_width - inner_width
  case INTERNAL_ALIGNMENT
  when 'left'
    [0, diff]
  when 'right'
    [diff, 0]
  else
    left = diff / 2
    right = diff - left
    [left, right]
  end
end

.get_parts(part_names) ⇒ Object



161
162
163
# File 'lib/railroad_diagrams/text_diagram.rb', line 161

def get_parts(part_names)
  part_names.map { |name| @parts[name] }
end

.max_width(*args) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/railroad_diagrams/text_diagram.rb', line 128

def max_width(*args)
  max_width = 0
  args.each do |arg|
    width =
      case arg
      when TextDiagram
        arg.width
      when Array
        arg.map(&:length).max
      when Numeric
        arg.to_s.length
      else
        arg.length
      end
    max_width = width if width > max_width
  end
  max_width
end

.pad_l(string, width, pad) ⇒ Object



147
148
149
150
151
152
# File 'lib/railroad_diagrams/text_diagram.rb', line 147

def pad_l(string, width, pad)
  gap = width - string.length
  raise "Gap #{gap} must be a multiple of pad string '#{pad}'" unless (gap % pad.length).zero?

  (pad * (gap / pad.length)) + string
end

.pad_r(string, width, pad) ⇒ Object



154
155
156
157
158
159
# File 'lib/railroad_diagrams/text_diagram.rb', line 154

def pad_r(string, width, pad)
  gap = width - string.length
  raise "Gap #{gap} must be a multiple of pad string '#{pad}'" unless (gap % pad.length).zero?

  string + (pad * (gap / pad.length))
end

.rect(item, dashed: false) ⇒ Object



120
121
122
# File 'lib/railroad_diagrams/text_diagram.rb', line 120

def rect(item, dashed: false)
  rectish('rect', item, dashed)
end

.round_rect(item, dashed: false) ⇒ Object



124
125
126
# File 'lib/railroad_diagrams/text_diagram.rb', line 124

def round_rect(item, dashed: false)
  rectish('roundrect', item, dashed)
end

.set_formatting(characters = nil, defaults = nil) ⇒ Object



110
111
112
113
114
115
116
117
118
# File 'lib/railroad_diagrams/text_diagram.rb', line 110

def set_formatting(characters = nil, defaults = nil)
  return unless characters

  @parts = defaults ? defaults.dup : {}
  @parts.merge!(characters)
  @parts.each do |name, value|
    raise ArgumentError, "Text part #{name} is more than 1 character: #{value}" if value.size != 1
  end
end

Instance Method Details

#alter(new_entry: nil, new_exit: nil, new_lines: nil) ⇒ Object



262
263
264
265
266
267
268
# File 'lib/railroad_diagrams/text_diagram.rb', line 262

def alter(new_entry: nil, new_exit: nil, new_lines: nil)
  self.class.new(
    new_entry || @entry,
    new_exit || @exit,
    new_lines || @lines.dup
  )
end

#append_below(item, lines_between, move_entry: false, move_exit: false) ⇒ Object



270
271
272
273
274
275
276
277
278
279
280
# File 'lib/railroad_diagrams/text_diagram.rb', line 270

def append_below(item, lines_between, move_entry: false, move_exit: false)
  new_width = [@width, item.width].max
  new_lines = center(new_width).lines
  lines_between.each { |line| new_lines << TextDiagram.pad_r(line, new_width, ' ') }
  new_lines += item.center(new_width).lines

  new_entry = move_entry ? @height + lines_between.size + item.entry : @entry
  new_exit = move_exit ? @height + lines_between.size + item.exit : @exit

  self.class.new(new_entry, new_exit, new_lines)
end

#append_right(item, chars_between) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/railroad_diagrams/text_diagram.rb', line 282

def append_right(item, chars_between)
  join_line = [@exit, item.entry].max
  new_height = [@height - @exit, item.height - item.entry].max + join_line

  left = expand(0, 0, join_line - @exit, new_height - @height - (join_line - @exit))
  right = item.expand(0, 0, join_line - item.entry, new_height - item.height - (join_line - item.entry))

  new_lines = (0...new_height).map do |i|
    sep = i == join_line ? chars_between : ' ' * chars_between.size
    left_line = i < left.lines.size ? left.lines[i] : ' ' * left.width
    right_line = i < right.lines.size ? right.lines[i] : ' ' * right.width
    "#{left_line}#{sep}#{right_line}"
  end

  self.class.new(
    @entry + (join_line - @exit),
    item.exit + (join_line - item.entry),
    new_lines
  )
end

#center(new_width, pad = ' ') ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
# File 'lib/railroad_diagrams/text_diagram.rb', line 303

def center(new_width, pad = ' ')
  raise 'Cannot center into smaller width' if width < @width
  return copy if new_width == @width

  total_padding = new_width - @width
  left_width = total_padding / 2
  left = [pad * left_width] * @height
  right = [pad * (total_padding - left_width)] * @height

  self.class.new(@entry, @exit, self.class.enclose_lines(@lines, left, right))
end

#copyObject



315
316
317
# File 'lib/railroad_diagrams/text_diagram.rb', line 315

def copy
  self.class.new(@entry, @exit, @lines.dup)
end

#dump(show = true) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/railroad_diagrams/text_diagram.rb', line 340

def dump(show = true)
  result = "height=#{@height}; len(lines)=#{@lines.length}"

  result += "; entry outside diagram: entry=#{@ntry}" if @entry > @lines.length
  result += "; exit outside diagram: exit=#{@exit}" if @exit > @lines.length

  (0...[@lines.length, @entry + 1, @exit + 1].max).each do |y|
    result += "\n[#{format('%03d', y)}]"
    result += " '#{@lines[y]}' len=#{@lines[y].length}" if y < @lines.length
    if y == @entry && y == @exit
      result += ' <- entry, exit'
    elsif y == @entry
      result += ' <- entry'
    elsif y == @exit
      result += ' <- exit'
    end
  end

  if show
    puts result
  else
    result
  end
end

#expand(left, right, top, bottom) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/railroad_diagrams/text_diagram.rb', line 319

def expand(left, right, top, bottom)
  return copy if [left, right, top, bottom].all?(&:zero?)

  new_lines = []
  top.times { new_lines << (' ' * (@width + left + right)) }

  @lines.each do |line|
    left_part = (line == @lines[@entry] ? self.class.parts['line'] : ' ') * left
    right_part = (line == @lines[@exit] ? self.class.parts['line'] : ' ') * right
    new_lines << "#{left_part}#{line}#{right_part}"
  end

  bottom.times { new_lines << (' ' * (@width + left + right)) }

  self.class.new(
    @entry + top,
    @exit + top,
    new_lines
  )
end