Fix ANSIString parsing on partial slice from start/end of string. Resolve #2205.
This commit is contained in:
parent
9bb0c77a74
commit
5fa7c62687
3 changed files with 111 additions and 39 deletions
|
|
@ -1,20 +1,19 @@
|
||||||
"""
|
"""
|
||||||
ANSI - Gives colour to text.
|
ANSI - Gives colour to text.
|
||||||
|
|
||||||
Use the codes defined in ANSIPARSER in your text to apply colour to text
|
Use the codes defined in ANSIPARSER in your text
|
||||||
according to the ANSI standard.
|
to apply colour to text according to the ANSI standard.
|
||||||
::
|
|
||||||
|
|
||||||
This is |rRed text|n and this is normal again.
|
Examples:
|
||||||
|
|
||||||
Mostly you should not need to call `parse_ansi()` explicitly; it is run by
|
```python
|
||||||
Evennia just before returning data to/from the user. Depreciated/decativated
|
"This is |rRed text|n and this is normal again."
|
||||||
example forms are available in contribs by extending the ansi mapping
|
```
|
||||||
|
|
||||||
This module also contains the `ANSIString` custom string-type, which correctly
|
Mostly you should not need to call `parse_ansi()` explicitly;
|
||||||
wraps/manipulates and tracks lengths of strings containing ANSI-markup.
|
it is run by Evennia just before returning data to/from the
|
||||||
|
user. Depreciated example forms are available by extending
|
||||||
----
|
the ansi mapping.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import functools
|
import functools
|
||||||
|
|
@ -82,14 +81,16 @@ _COLOR_NO_DEFAULT = settings.COLOR_NO_DEFAULT
|
||||||
|
|
||||||
class ANSIParser(object):
|
class ANSIParser(object):
|
||||||
"""
|
"""
|
||||||
A class that parses ANSI markup to ANSI command sequences
|
A class that parses ANSI markup
|
||||||
|
to ANSI command sequences
|
||||||
|
|
||||||
We also allow to escape colour codes by prepending with
|
We also allow to escape colour codes
|
||||||
an extra `|`, so `||r` will literally print `|r`.
|
by prepending with a \ for xterm256,
|
||||||
|
an extra | for Merc-style codes
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Mapping using |r, |n etc
|
# Mapping using {r {n etc
|
||||||
|
|
||||||
ansi_map = [
|
ansi_map = [
|
||||||
# alternative |-format
|
# alternative |-format
|
||||||
|
|
@ -525,6 +526,13 @@ def raw(string):
|
||||||
return string.replace("{", "{{").replace("|", "||")
|
return string.replace("{", "{{").replace("|", "||")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ANSIString - ANSI-aware string class
|
||||||
|
#
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _spacing_preflight(func):
|
def _spacing_preflight(func):
|
||||||
"""
|
"""
|
||||||
This wrapper function is used to do some preflight checks on
|
This wrapper function is used to do some preflight checks on
|
||||||
|
|
@ -588,9 +596,10 @@ def _on_raw(func_name):
|
||||||
|
|
||||||
def _transform(func_name):
|
def _transform(func_name):
|
||||||
"""
|
"""
|
||||||
Some string functions, like those manipulating capital letters, return a
|
Some string functions, like those manipulating capital letters,
|
||||||
string the same length as the original. This function allows us to do the
|
return a string the same length as the original. This function
|
||||||
same, replacing all the non-coded characters with the resulting string.
|
allows us to do the same, replacing all the non-coded characters
|
||||||
|
with the resulting string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -823,7 +832,7 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not offset:
|
if not offset:
|
||||||
return []
|
return iterable
|
||||||
return [i + offset for i in iterable]
|
return [i + offset for i in iterable]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -894,9 +903,23 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||||
replayed.
|
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 it's the end of the string, we need to append final color codes.
|
||||||
if not slice_indexes:
|
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("")
|
return ANSIString("")
|
||||||
try:
|
try:
|
||||||
string = self[slc.start or 0]._raw_string
|
string = self[slc.start or 0]._raw_string
|
||||||
|
|
@ -916,7 +939,7 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||||
# raw_string not long enough
|
# raw_string not long enough
|
||||||
pass
|
pass
|
||||||
if i is not None:
|
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:
|
else:
|
||||||
append_tail = ""
|
append_tail = ""
|
||||||
return ANSIString(string + append_tail, decoded=True)
|
return ANSIString(string + append_tail, decoded=True)
|
||||||
|
|
@ -985,12 +1008,9 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||||
occurrence of the separator rather than the first.
|
occurrence of the separator rather than the first.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
result (tuple):
|
ANSIString: The part of the string before the separator
|
||||||
- prefix (ANSIString): The part of the string before the
|
ANSIString: The separator itself
|
||||||
separator
|
ANSIString: The part of the string after the separator.
|
||||||
- sep (ANSIString): The separator itself
|
|
||||||
- postfix (ANSIString): The part of the string after the
|
|
||||||
separator.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if hasattr(sep, "_clean_string"):
|
if hasattr(sep, "_clean_string"):
|
||||||
|
|
@ -1289,27 +1309,23 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||||
Joins together strings in an iterable, using this string between each
|
Joins together strings in an iterable, using this string between each
|
||||||
one.
|
one.
|
||||||
|
|
||||||
|
NOTE: This should always be used for joining strings when ANSIStrings
|
||||||
|
are involved. Otherwise color information will be discarded by python,
|
||||||
|
due to details in the C implementation of strings.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
iterable (list of strings): A list of strings to join together
|
iterable (list of strings): A list of strings to join together
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
result (ANSIString): A single string with all of the iterable's
|
ANSIString: A single string with all of the iterable's
|
||||||
contents concatenated, with this string between each.
|
contents concatenated, with this string between each.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
::
|
|
||||||
ANSIString(', ').join(['up', 'right', 'left', 'down'])
|
|
||||||
|
|
||||||
Would return
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
>>> ANSIString(', ').join(['up', 'right', 'left', 'down'])
|
||||||
ANSIString('up, right, left, down')
|
ANSIString('up, right, left, down')
|
||||||
|
|
||||||
Notes:
|
|
||||||
This should always be used for joining strings when ANSIStrings are
|
|
||||||
involved. Otherwise color information will be discarded by python,
|
|
||||||
due to details in the C implementation of strings.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
result = ANSIString("")
|
result = ANSIString("")
|
||||||
last_item = None
|
last_item = None
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,12 @@ def _to_rect(lines):
|
||||||
|
|
||||||
def _to_ansi(obj, regexable=False):
|
def _to_ansi(obj, regexable=False):
|
||||||
"convert to ANSIString"
|
"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
|
# 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)
|
obj = _ANSI_ESCAPE.sub(r"||||", obj)
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return dict((key, _to_ansi(value, regexable=regexable)) for key, value in obj.items())
|
return dict((key, _to_ansi(value, regexable=regexable)) for key, value in obj.items())
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,59 @@ class ANSIStringTestCase(TestCase):
|
||||||
self.assertEqual(a.rstrip(), ANSIString(" |r Test of stuff |b with spaces|n"))
|
self.assertEqual(a.rstrip(), ANSIString(" |r Test of stuff |b with spaces|n"))
|
||||||
self.assertEqual(b.strip(), b)
|
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):
|
class TestTextToHTMLparser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue