lib/refinements/ansiless_string_refinement.rb in scryglass-1.1.0 vs lib/refinements/ansiless_string_refinement.rb in scryglass-2.0.0

- old
+ new

@@ -5,7 +5,152 @@ end def ansiless_length ansiless.length end + + ## Splits string into characters, with each ANSI escape code being its own + ## grouped item, like so: + ## irb> "PLAIN\e[32mCOLOR\e[0mPLAIN".ansi_string_breakout + ## => ["P", "L", "A", "I", "N", "\e[32m", "C", "O", "L", "O", "R", + ## "\e[0m", "P", "L", "A", "I", "N"] + def ansi_string_breakout + breakout_array = [] + working_self = self.dup + + while working_self[0] + if (working_self =~ /\e\[[\d\;]*m/) == 0 # if begins with + end_of_escape_code = (working_self.index('m')) + leading_escape_code = working_self[0..end_of_escape_code] + breakout_array << leading_escape_code + working_self = working_self[(end_of_escape_code + 1)..-1] + else + leading_character = working_self[0] + breakout_array << leading_character + working_self = working_self[1..-1] + end + end + + breakout_array + end + + def is_ansi_escape_code? + (self =~ /^\e\[[\d\;]*m$/) == 0 + end + + ## Returns the indexed character of the real string, determined by the index + ## given in reference to its ANSIless display form. e.g.: + ## > s = "\e[31mTEST\e[00m" + ## > puts s + ## TEST + ## > s.ansiless_pick(1) = 'y' + ## > s + ## => "\e[31mTyST\e[00m" + def ansiless_pick(given_index) + ansiless_self = self.ansiless + + return nil if ansiless_self[given_index].nil? + + given_index = (given_index + ansiless_self.length) if given_index.negative? + + mock_index = 0 # A scanning index that *doesn't* count ANSI codes + real_index = 0 # A scanning index that *does* count ANSI codes + + ansi_string_array = ansi_string_breakout + + until mock_index == given_index + while ansi_string_array.first.is_ansi_escape_code? + real_index += (ansi_string_array.shift.length) + end + + ansi_string_array.shift + + while ansi_string_array.first.is_ansi_escape_code? + real_index += (ansi_string_array.shift.length) + end + + mock_index += 1 + real_index += 1 + end + + self[real_index] + end + + ## Like ansiless_pick, but it can set that found string character instead + def ansiless_set!(given_index, string) + raise ArgumentError, 'First argument must be an Integer' unless given_index.is_a?(Integer) + raise ArgumentError, 'Second argument must be a String' unless string.is_a?(String) + + ansiless_self = self.ansiless + + return nil if ansiless_self[given_index].nil? + + given_index = (given_index + ansiless_self.length) if given_index.negative? + + + new_string = string.to_s + + mock_index = 0 # A scanning index that *doesn't* count ANSI codes + real_index = 0 # A scanning index that *does* count ANSI codes + + ansi_string_array = ansi_string_breakout + + until mock_index == given_index + while ansi_string_array.first.is_ansi_escape_code? + real_index += (ansi_string_array.shift.length) + end + + ansi_string_array.shift + + while ansi_string_array.first.is_ansi_escape_code? + real_index += (ansi_string_array.shift.length) + end + + mock_index += 1 + real_index += 1 + end + + self[real_index] = new_string + end + + def ansi_slice(arg1, arg2 = nil) # i.e. like 'test'[0..2] and 'test'[2, 1] + unless (arg1.is_a?(Range) && arg2.nil?) || + (arg1.is_a?(Integer) && arg2.is_a?(Integer)) + raise ArgumentError, 'ansi_slice takes either a single Range ' \ + 'or two integers (index and length) as its arguments.' + end + + target_range = arg1.is_a?(Range) ? arg1 : (arg1...(arg1 + arg2)) + + if target_range.min.negative? || target_range.max.negative? + raise ArgumentError, 'Range must be entirely positive!' + end + + args = [arg1, arg2].compact + return self[*args] if (self =~ /\e\[[\d\;]*m/).nil? # No work need be done + + ## And here we match the normal `:[]` behavior outside of boundaries, e.g: + ## irb> 'TEST'[4..9] + ## => "" + ## irb> 'TEST'[5..9] + ## => nil + return nil if target_range.min > self.ansiless_length + + mock_index = 0 # A scanning index that *doesn't* count ANSI codes + result_string_array = [] + + ansi_string_breakout.each do |char| + char_is_ansi_escape_code = char.is_ansi_escape_code? + within_target_range = + target_range.include?(mock_index) + + if within_target_range || char_is_ansi_escape_code + result_string_array << char + end + + mock_index += 1 unless char_is_ansi_escape_code + end + + result_string_array.join('') + end end end