From 558a3d76c7fb9a10e4dcee0d9a60569a77cdaaff Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 26 Oct 2020 21:35:15 +0100 Subject: [PATCH] Fix ANSIString parsing on partial slice from start/end of string. Resolve #2205. --- evennia/utils/ansi.py | 25 +++++++++++- evennia/utils/evform.py | 7 +++- evennia/utils/tests/test_tagparsing.py | 53 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 5c8eed5820..370dda870d 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -526,6 +526,13 @@ def raw(string): return string.replace("{", "{{").replace("|", "||") +# ------------------------------------------------------------ +# +# ANSIString - ANSI-aware string class +# +# ------------------------------------------------------------ + + def _spacing_preflight(func): """ This wrapper function is used to do some preflight checks on @@ -896,9 +903,23 @@ class ANSIString(str, metaclass=ANSIMeta): replayed. """ - slice_indexes = self._char_indexes[slc] + char_indexes = self._char_indexes + slice_indexes = char_indexes[slc] # If it's the end of the string, we need to append final color codes. if not slice_indexes: + # if we find no characters it may be because we are just outside + # of the interval, using an open-ended slice. We must replay all + # of the escape characters until/after this point. + if char_indexes: + if slc.start is None and slc.stop is None: + # a [:] slice of only escape characters + return ANSIString(self._raw_string[slc]) + if slc.start is None: + # this is a [:x] slice + return ANSIString(self._raw_string[:char_indexes[0]]) + if slc.stop is None: + # a [x:] slice + return ANSIString(self._raw_string[char_indexes[-1] + 1:]) return ANSIString("") try: string = self[slc.start or 0]._raw_string @@ -918,7 +939,7 @@ class ANSIString(str, metaclass=ANSIMeta): # raw_string not long enough pass if i is not None: - append_tail = self._get_interleving(self._char_indexes.index(i) + 1) + append_tail = self._get_interleving(char_indexes.index(i) + 1) else: append_tail = "" return ANSIString(string + append_tail, decoded=True) diff --git a/evennia/utils/evform.py b/evennia/utils/evform.py index e7b9151c5d..7bbe7a4915 100644 --- a/evennia/utils/evform.py +++ b/evennia/utils/evform.py @@ -165,9 +165,12 @@ def _to_rect(lines): def _to_ansi(obj, regexable=False): "convert to ANSIString" - if isinstance(obj, str): + if isinstance(obj, ANSIString): + return obj + elif isinstance(obj, str): # since ansi will be parsed twice (here and in the normal ansi send), we have to - # escape the |-structure twice. + # escape the |-structure twice. TODO: This is tied to the default color-tag syntax + # which is not ideal for those wanting to replace/extend it ... obj = _ANSI_ESCAPE.sub(r"||||", obj) if isinstance(obj, dict): return dict((key, _to_ansi(value, regexable=regexable)) for key, value in obj.items()) diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py index e82b7d551e..6435c5c2ce 100644 --- a/evennia/utils/tests/test_tagparsing.py +++ b/evennia/utils/tests/test_tagparsing.py @@ -177,6 +177,59 @@ class ANSIStringTestCase(TestCase): self.assertEqual(a.rstrip(), ANSIString(" |r Test of stuff |b with spaces|n")) self.assertEqual(b.strip(), b) + def test_regex_search(self): + """ + Test regex-search in ANSIString - the found position should ignore any ansi-markers + """ + string = ANSIString(" |r|[b Test ") + match = re.search(r"Test", string) + self.assertTrue(match) + self.assertEqual(match.span(), (3, 7)) + + def test_regex_replace(self): + """ + Inserting text into an ansistring at an index position should ignore + the ansi markers but not remove them! + + """ + string = ANSIString("A |rTest|n string") + match = re.search(r"Test", string) + ix1, ix2 = match.span() + self.assertEqual((ix1, ix2), (2, 6)) + + result = string[:ix1] + "Replacement" + string[ix2:] + expected = ANSIString("A |rReplacement|n string") + + self.assertEqual(expected, result) + + def test_slice_insert(self): + """ + Inserting a slice should not remove ansi markup (issue #2205) + """ + string = ANSIString("|rTest|n") + split_string = string[:0] + "Test" + string[4:] + self.assertEqual(string.raw(), split_string.raw()) + + def test_slice_insert_longer(self): + """ + The ANSIString replays the color code before the split in order to + produce a *visually* identical result. The result is a longer string in + raw characters, but one which correctly represents the color output. + """ + string = ANSIString("A bigger |rTest|n of things |bwith more color|n") + # from evennia import set_trace;set_trace() + split_string = string[:9] + "Test" + string[13:] + self.assertEqual( + repr((ANSIString("A bigger ") + + ANSIString("|rTest") # note that the |r|n is replayed together on next line + + ANSIString("|r|n of things |bwith more color|n")).raw()), + repr(split_string.raw())) + + def test_slice_full(self): + string = ANSIString("A bigger |rTest|n of things |bwith more color|n") + split_string = string[:] + self.assertEqual(string.raw(), split_string.raw()) + class TestTextToHTMLparser(TestCase): def setUp(self):