class Verse::Wrapping

A class responsible for text wrapping

Constants

DEFAULT_WIDTH

Attributes

text[R]

The text to wrap

@api private

Public Class Methods

new(text, options = {}) click to toggle source

Initialize a Wrapping

@param [String] text

the text to be wrapped

@param [Hash] options

@option options [Symbol] :padding the desired spacing

@api public

# File lib/verse/wrapping.rb, line 17
def initialize(text, options = {})
  @text       = text
  @line_width = options.fetch(:line_width) { DEFAULT_WIDTH }
end
wrap(text, wrap_at, options = {}) click to toggle source

Wrap a text into lines no longer than wrap_at

@api public

# File lib/verse/wrapping.rb, line 25
def self.wrap(text, wrap_at, options = {})
  new(text, options).wrap(wrap_at)
end

Public Instance Methods

wrap(wrap_at = DEFAULT_WIDTH) click to toggle source

Wrap a text into lines no longer than wrap_at length. Preserves existing lines and existing word boundaries.

@example

wrapping = Verse::Wrapping.new "Some longish text"

wrapping.wrap(8)
# => >Some
     >longish
     >text

@api public

# File lib/verse/wrapping.rb, line 41
def wrap(wrap_at = DEFAULT_WIDTH)
  if text.length < wrap_at.to_i || wrap_at.to_i.zero?
    return text
  end
  ansi_stack = []
  text.split(NEWLINE, -1).map do |paragraph|
    format_paragraph(paragraph, wrap_at, ansi_stack)
  end * NEWLINE
end

Protected Instance Methods

display_width(string) click to toggle source

Visible width of string

@api private

# File lib/verse/wrapping.rb, line 187
def display_width(string)
  Unicode::DisplayWidth.of(Sanitizer.sanitize(string))
end
format_paragraph(paragraph, wrap_at, ansi_stack) click to toggle source

Format paragraph to be maximum of wrap_at length

@param [String] paragraph

the paragraph to format

@param [Integer] wrap_at

the maximum length to wrap the paragraph

@return [Array]

the wrapped lines

@api private

# File lib/verse/wrapping.rb, line 69
def format_paragraph(paragraph, wrap_at, ansi_stack)
  cleared_para = Sanitizer.replace(paragraph)
  lines = []
  line = ''
  word = ''
  word_length = 0
  line_length = 0
  char_length = 0 # visible char length
  text_length = display_width(cleared_para)
  total_length = 0
  ansi = ''
  matched = nil
  to_chars(cleared_para) do |char|
    if char == ANSI # found ansi
      ansi << char && next
    end

    if ansi.length > 0
      ansi << char
      if Sanitizer.ansi?(ansi) # we found ansi let's consume
        matched = ansi
      elsif matched
        ansi_stack << [matched[0...-1], line_length + word_length]
        matched = nil
        ansi    = ''
      end
      next if ansi.length > 0
    end

    char_length = display_width(char)
    total_length += char_length
    if line_length + word_length + char_length <= wrap_at
      if char == SPACE || total_length == text_length
        line << word + char
        line_length += word_length + char_length
        word = ''
        word_length = 0
      else
        word << char
        word_length += char_length
      end
      next
    end

    if char == SPACE # ends with space
      lines << insert_ansi(ansi_stack, line)
      line = ''
      line_length = 0
      word += char
      word_length += char_length
    elsif word_length + char_length <= wrap_at
      lines << insert_ansi(ansi_stack, line)
      line = word + char
      line_length = word_length + char_length
      word = ''
      word_length = 0
    else # hyphenate word - too long to fit a line
      lines << insert_ansi(ansi_stack, word)
      line_length = 0
      word = char
      word_length = char_length
    end
  end
  lines << insert_ansi(ansi_stack, line) unless line.empty?
  lines << insert_ansi(ansi_stack, word) unless word.empty?
  lines
end
insert_ansi(ansi_stack, string) click to toggle source

Insert ANSI code into string

Check if there are any ANSI states, if present insert ANSI codes at given positions unwinding the stack.

@param [Array[Array[String, Integer]]] ansi_stack

the ANSI codes to apply

@param [String] string

the string to insert ANSI codes into

@return [String]

@api private

# File lib/verse/wrapping.rb, line 151
def insert_ansi(ansi_stack, string)
  return string if ansi_stack.empty?
  to_remove = 0
  reset_index = -1
  output = string.dup
  resetting = false
  ansi_stack.reverse_each do |state|
    if state[0] =~ /#{Regexp.quote(RESET)}/
      resetting = true
      reset_index = state[1]
      to_remove += 2
      next
    elsif !resetting
      reset_index = -1
      resetting = false
    end

    color, color_index = *state
    output.insert(reset_index, RESET).insert(color_index, color)
  end
  ansi_stack.pop(to_remove) # remove used states
  output
end
to_chars(text, &block) click to toggle source

@api private

# File lib/verse/wrapping.rb, line 176
def to_chars(text, &block)
  if block_given?
    UnicodeUtils.each_grapheme(text, &block)
  else
    UnicodeUtils.each_grapheme(text)
  end
end