class ANSIString

Attributes

raw[R]
without_ansi[R]

Public Class Methods

new(str) click to toggle source
# File lib/ansi_string.rb, line 12
def initialize(str)
  process_string raw_string_for(str)
end

Public Instance Methods

+(other) click to toggle source
# File lib/ansi_string.rb, line 16
def +(other)
  self.class.new @raw + raw_string_for(other)
end
<<(other) click to toggle source
# File lib/ansi_string.rb, line 20
def <<(other)
  range = length..length
  str = replace_in_string(range, other)
  process_string raw_string_for(str)
  self
end
<=>(other) click to toggle source
# File lib/ansi_string.rb, line 219
def <=>(other)
  (other.class == self.class && @raw <=> other.raw)
end
==(other) click to toggle source
# File lib/ansi_string.rb, line 215
def ==(other)
  (other.class == self.class && other.raw == @raw) || (other.kind_of?(String) && other == @raw)
end
[](range) click to toggle source
# File lib/ansi_string.rb, line 39
def [](range)
  # convert numeric position to a range
  range = (range..range) if range.is_a?(Integer)

  range_begin = range.begin
  range_end = range.end

  if range.exclude_end?
    if range_begin == 0 && range_end == 0
      return ""
    else
      range_end -= 1
    end
  end

  range_begin = @without_ansi.length - range.begin.abs if range.begin < 0
  range_end = @without_ansi.length - range.end.abs if range.end < 0

  str = build_string_with_ansi_for(range_begin..range_end)
  ANSIString.new str if str
end
[]=(range, replacement_str) click to toggle source
# File lib/ansi_string.rb, line 61
def []=(range, replacement_str)
  # convert numeric position to a range
  range = (range..range) if range.is_a?(Integer)

  range_begin = range.begin
  range_end = range.exclude_end? ? range.end - 1 : range.end

  range_begin = @without_ansi.length - range.begin.abs if range.begin < 0
  range_end = @without_ansi.length - range.end.abs if range.end < 0

  updated_string = replace_in_string(range_begin..range_end, replacement_str)
  process_string raw_string_for(updated_string)
  self
end
dup() click to toggle source
# File lib/ansi_string.rb, line 184
def dup
  ANSIString.new(@raw.dup)
end
empty?() click to toggle source
# File lib/ansi_string.rb, line 35
def empty?
  length == 0
end
insert(position, string) click to toggle source
# File lib/ansi_string.rb, line 27
def insert(position, string)
  if position < 0
    position = @without_ansi.length + position + 1
  end
  self[position...position] = string
  self
end
inspect() click to toggle source
# File lib/ansi_string.rb, line 211
def inspect
  to_s.inspect
end
length() click to toggle source
# File lib/ansi_string.rb, line 140
def length
  @without_ansi.length
end
lines() click to toggle source
# File lib/ansi_string.rb, line 144
def lines
  result = []
  current_string = ""
  @ansi_sequence_locations.map do |location|
    if location[:text] == "\n"
      result << ANSIString.new(current_string + "\n")
      current_string = ""
      next
    end

    location[:text].scan(/.*(?:\n|$)/).each_with_index do |line, i|
      break if line == ""

      if i == 0
        current_string << [
          location[:start_ansi_sequence],
          line,
          location[:end_ansi_sequence]
        ].join
      else
        result << ANSIString.new(current_string)
        current_string = ""
        current_string << [
          location[:start_ansi_sequence],
          line,
          location[:end_ansi_sequence]
        ].join
      end
    end

    if location[:text].end_with?("\n")
      result << ANSIString.new(current_string)
      current_string = ""
      next
    end
  end
  result << ANSIString.new(current_string) if current_string.length > 0
  result
end
replace(str) click to toggle source
# File lib/ansi_string.rb, line 81
def replace(str)
  process_string raw_string_for(str)
  self
end
reverse() click to toggle source
# File lib/ansi_string.rb, line 86
def reverse
  str = @ansi_sequence_locations.reverse.map do |location|
    [location[:start_ansi_sequence], location[:text].reverse, location[:end_ansi_sequence]].join
  end.join
  ANSIString.new str
end
rindex(*args) click to toggle source

See String#rindex for arguments

# File lib/ansi_string.rb, line 77
def rindex(*args)
  @without_ansi.rindex(*args)
end
scan(pattern) click to toggle source
# File lib/ansi_string.rb, line 93
def scan(pattern)
  results = []
  without_ansi.enum_for(:scan, pattern).each do
    md = Regexp.last_match
    if md.captures.any?
      results << md.captures.map.with_index do |_, i|
        # captures use 1-based indexing
        self[md.begin(i+1)..md.end(i+1)-1]
      end
    else
      results << self[md.begin(0)..md.end(0)-1]
    end
  end
  results
end
slice(index, length=nil) click to toggle source
# File lib/ansi_string.rb, line 109
def slice(index, length=nil)
  return ANSIString.new("") if length == 0
  range = nil
  index = index.without_ansi if index.is_a?(ANSIString)
  index = Regexp.new Regexp.escape(index) if index.is_a?(String)
  if index.is_a?(Integer)
    length ||= 1
    range = (index..index+length-1)
  elsif index.is_a?(Range)
    range = index
  elsif index.is_a?(Regexp)
    md = @without_ansi.match(index)
    capture_group_index = length || 0
    if md
      capture_group = md.offset(capture_group_index)
      range = (capture_group.first..capture_group.last-1)
    end
  else
    raise(ArgumentError, "Must pass in at least an index or a range.")
  end
  self[range] if range
end
split(*args) click to toggle source
# File lib/ansi_string.rb, line 132
def split(*args)
  raw.split(*args).map { |s| ANSIString.new(s) }
end
strip() click to toggle source
# File lib/ansi_string.rb, line 136
def strip
  ANSIString.new raw.strip
end
sub(pattern, replacement) click to toggle source
# File lib/ansi_string.rb, line 188
def sub(pattern, replacement)
  str = ""
  count = 0
  max_count = 1
  index = 0
  @without_ansi.enum_for(:scan, pattern).each do
    md = Regexp.last_match
    str << build_string_with_ansi_for(index...(index + md.begin(0)))
    index = md.end(0)
    break if (count += 1) == max_count
  end
  if index != @without_ansi.length
    str << build_string_with_ansi_for(index..@without_ansi.length)
  end
  nstr = str.gsub /(\033\[[0-9;]*m)(.+?)\033\[0m\1/, '\1\2'
  ANSIString.new(nstr)
end
to_s() click to toggle source
# File lib/ansi_string.rb, line 206
def to_s
  @raw.dup
end
Also aliased as: to_str
to_str()
Alias for: to_s

Private Instance Methods

build_string_with_ansi_for(range) click to toggle source
# File lib/ansi_string.rb, line 341
def build_string_with_ansi_for(range)
  return nil if range.begin > length

  str = ""

  if range.exclude_end?
    range = range.begin..(range.end - 1)
  end

  @ansi_sequence_locations.each do |location|
    # If the given range encompasses part of the location, then we want to
    # include the whole location
    if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
      str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join

    elsif location[:begins_at] >= range.begin && location[:begins_at] <= range.end
      str << [location[:start_ansi_sequence], location[:text][0..(range.end - location[:begins_at])], location[:end_ansi_sequence]].join

    # If the location falls within the given range then  make sure we pull
    # out the bits that we want, and keep ANSI escape sequenece intact while
    # doing so.
    elsif (location[:begins_at] <= range.begin && location[:ends_at] >= range.end) || range.cover?(location[:ends_at])
      start_index = range.begin - location[:begins_at]
      end_index = range.end - location[:begins_at]
      str << [location[:start_ansi_sequence], location[:text][start_index..end_index], location[:end_ansi_sequence]].join
    end
  end
  str
end
process_string(raw_str) click to toggle source
# File lib/ansi_string.rb, line 229
def process_string(raw_str)
  @without_ansi = ""
  @ansi_sequence_locations = []
  raw_str.enum_for(:scan, /(\e\[[0-9;]*m)?(.*?)(?=\e\[[0-9;]*m|\Z)/m ).each do
    md = Regexp.last_match
    ansi_sequence, text = md.captures

    previous_sequence_location = @ansi_sequence_locations.last
    if previous_sequence_location
      if ansi_sequence == "\e[0m"
        previous_sequence_location[:end_ansi_sequence] = ansi_sequence
        ansi_sequence = nil
      elsif previous_sequence_location[:start_ansi_sequence] == ansi_sequence
        previous_sequence_location[:text] << text
        previous_sequence_location[:ends_at] += text.length
        previous_sequence_location[:length] += text.length
        @without_ansi << text
        next
      end
    end

    if ansi_sequence.nil? && text.to_s.length == 0
      next
    end

    @ansi_sequence_locations.push(
      begins_at: @without_ansi.length,
      ends_at: [@without_ansi.length + text.length - 1, 0].max,
      length: text.length,
      text: text,
      start_ansi_sequence: ansi_sequence
    )

    @without_ansi << text
  end

  @raw = @ansi_sequence_locations.map do |location|
    [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].compact.join
  end.join

  @ansi_sequence_locations
end
raw_string_for(str) click to toggle source
# File lib/ansi_string.rb, line 225
def raw_string_for(str)
  str.is_a?(ANSIString) ? str.raw : str.to_s
end
replace_in_string(range, replacement_str) click to toggle source
# File lib/ansi_string.rb, line 272
def replace_in_string(range, replacement_str)
  raise RangeError, "#{range.inspect} out of range" if range.begin > length
  return replacement_str if @ansi_sequence_locations.empty?

  range = range.begin..(range.end - 1) if range.exclude_end?
  str = ""
  @ansi_sequence_locations.each_with_index do |location, j|
    # If the given range encompasses part of the location, then we want to
    # include the whole location
    if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
      end_index = range.end - location[:begins_at] + 1

      str << [
        location[:start_ansi_sequence],
        replacement_str,
        location[:text][end_index..-1],
        location[:end_ansi_sequence]
      ].join

    # If the location falls within the given range then  make sure we pull
    # out the bits that we want, and keep ANSI escape sequenece intact while
    # doing so.
    elsif location[:begins_at] <= range.begin && location[:ends_at] >= range.end
      start_index = range.begin - location[:begins_at]
      end_index = range.end - location[:begins_at] + 1

      str << [
        location[:start_ansi_sequence],
        location[:text][0...start_index],
        replacement_str,
        location[:text][end_index..-1],
        location[:end_ansi_sequence]
      ].join

    elsif location[:ends_at] == range.begin
      start_index = range.begin - location[:begins_at]
      end_index = range.end
      num_chars_to_remove_from_next_location = range.end - location[:ends_at]

      str << [
        location[:start_ansi_sequence],
        location[:text][location[:begins_at]...(location[:begins_at]+start_index)],
        replacement_str,
        location[:text][end_index..-1],
        location[:end_ansi_sequence],
      ].join

      if location=@ansi_sequence_locations[j+1]
        old = location.dup
        location[:text][0...num_chars_to_remove_from_next_location] = ""
        location[:begins_at] += num_chars_to_remove_from_next_location
        location[:ends_at] += num_chars_to_remove_from_next_location
      end

    # If we're pushing onto the end of the string
    elsif range.begin == length && location[:ends_at] == length - 1
      if replacement_str.is_a?(ANSIString)
        str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence], replacement_str].join
      else
        str << [location[:start_ansi_sequence], location[:text], replacement_str, location[:end_ansi_sequence]].join
      end
    else
      str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
    end
  end

  str
end