diff --git a/src/ansi_string/ansi_string.py b/src/ansi_string/ansi_string.py index 5b7a22a..fc77c7e 100644 --- a/src/ansi_string/ansi_string.py +++ b/src/ansi_string/ansi_string.py @@ -33,7 +33,7 @@ ) from .ansi_parsing import ParsedAnsiControlSequenceString, parse_graphic_sequence, settings_to_dict -__version__ = '1.1.7' +__version__ = '1.1.8' PACKAGE_NAME = 'ansi_string' # Constant: all characters considered to be whitespaces - this is used in strip functionality @@ -315,8 +315,7 @@ def apply_formatting( settings - setting or list of settings to apply start - The string start index where setting(s) are to be applied end - The string index where the setting(s) should be removed - topmost - When true, the settings placed at the end of the set for the given - start_index, meaning it takes precedent over others; the opposite when False + topmost - When False, all other existing settings in this range will take precedent ''' start = self._slice_val_to_idx(start, 0) end = self._slice_val_to_idx(end, len(self._s)) @@ -336,6 +335,17 @@ def apply_formatting( self._fmts[start] = _AnsiSettingPoint() self._fmts[start].insert_settings(True, ansi_settings, topmost) + # When not topmost, do a remove and re-add of any settings that lead up to the start index + if not topmost: + remove_and_add_settings = [] + settings_at_start = self.ansi_settings_at(start) + for setting in settings_at_start: + if setting not in self._fmts[start].add: + remove_and_add_settings.append(setting) + if remove_and_add_settings: + self._fmts[start].insert_settings(False, remove_and_add_settings) + self._fmts[start].insert_settings(True, remove_and_add_settings) + # Remove settings if end not in self._fmts: self._fmts[end] = _AnsiSettingPoint() diff --git a/tests/test_ansi_string.py b/tests/test_ansi_string.py index b1086e0..8264cfe 100755 --- a/tests/test_ansi_string.py +++ b/tests/test_ansi_string.py @@ -475,6 +475,29 @@ def test_rpartition_not_found(self): ) self.assertIs(s, s_orig) + def test_apply_formatting_topmost(self): + s = AnsiString.join('This ', AnsiString('string', AnsiFormat.BOLD)) + s += AnsiString(' contains ') + AnsiString('multiple', AnsiFormat.BG_BLUE) + s += ' color settings across different ranges' + s.apply_formatting([AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC], 21, 35) + s.apply_formatting(AnsiFormat.FG_BLUE, 21, 44) + self.assertEqual(str(s), 'This \x1b[1mstring\x1b[m contains \x1b[44;34;3mmultiple\x1b[49m color\x1b[23m settings\x1b[m across different ranges') + + def test_apply_formatting_not_topmost(self): + s = AnsiString.join('This ', AnsiString('string', AnsiFormat.BOLD)) + s += AnsiString(' contains ') + AnsiString('multiple', AnsiFormat.BG_BLUE) + s += ' color settings across different ranges' + s.apply_formatting([AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC], 21, 35) + s.apply_formatting(AnsiFormat.FG_BLUE, 21, 44, topmost=False) # Should be ignored until index 35 + self.assertEqual(str(s), 'This \x1b[1mstring\x1b[m contains \x1b[38;5;214;44;3mmultiple\x1b[49m color\x1b[0;34m settings\x1b[m across different ranges') + + def test_apply_formatting_not_topmost2(self): + s = AnsiString.join('This ', AnsiString('string', AnsiFormat.BOLD)) + s += AnsiString(' contains ') + AnsiString('multiple', AnsiFormat.BG_BLUE) + s += ' color settings across different ranges' + s.apply_formatting([AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC], 21, 35) + s.apply_formatting(AnsiFormat.FG_BLUE, 30, 35, topmost=False) # Should be ignored + self.assertEqual(str(s), 'This \x1b[1mstring\x1b[m contains \x1b[44;38;5;214;3mmultiple\x1b[49m color\x1b[m settings across different ranges') def test_get_item_edge_case(self): # There used to be a bug where if a single character was retrieved right before the index where a new format was