diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 774b5917fe..a9f95b0c9c 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -82,7 +82,7 @@ class ANSIParser(object): """ return self.ansi_map.get(ansimatch.group(), "") - def sub_xterm256(self, rgbmatch): + def sub_xterm256(self, rgbmatch, convert=False): """ This is a replacer method called by `re.sub` with the matched tag. It must return the correct ansi sequence. @@ -102,7 +102,7 @@ class ANSIParser(object): else: red, green, blue = int(rgbtag[0]), int(rgbtag[1]), int(rgbtag[2]) - if self.do_xterm256: + if convert: colval = 16 + (red * 36) + (green * 6) + blue #print "RGB colours:", red, green, blue return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval/100, (colval % 100)/10, colval%10) @@ -201,15 +201,16 @@ class ANSIParser(object): if cachekey in _PARSE_CACHE: return _PARSE_CACHE[cachekey] - self.do_xterm256 = xterm256 - self.do_mxp = mxp + def do_xterm256(part): + return self.sub_xterm256(part, xterm256) + in_string = utils.to_str(string) # do string replacement parsed_string = "" parts = self.ansi_escapes.split(in_string) + [" "] for part, sep in zip(parts[::2], parts[1::2]): - pstring = self.xterm256_sub.sub(self.sub_xterm256, part) + pstring = self.xterm256_sub.sub(do_xterm256, part) pstring = self.ansi_sub.sub(self.sub_ansi, pstring) parsed_string += "%s%s" % (pstring, sep[0].strip()) @@ -221,8 +222,7 @@ class ANSIParser(object): # inserted in string) return self.strip_raw_codes(parsed_string) - - # cache and crop old cache + # cache and crop old cache _PARSE_CACHE[cachekey] = parsed_string if len(_PARSE_CACHE) > _PARSE_CACHE_SIZE: _PARSE_CACHE.popitem(last=False) @@ -342,10 +342,6 @@ class ANSIParser(object): ansi_re = r"\033\[[0-9;]+m" ansi_regex = re.compile(ansi_re) - # merged regex for both ansi and mxp, for use by ansistring - mxp_tags = r'\{lc.*?\{lt|\{le' - tags_regex = re.compile("%s|%s" % (ansi_re, mxp_tags), re.DOTALL) - # escapes - these double-chars will be replaced with a single # instance of each ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL) @@ -527,7 +523,7 @@ class ANSIString(unicode): decoded = True if not decoded: # Completely new ANSI String - clean_string = to_unicode(parser.parse_ansi(string, strip_ansi=True)) + clean_string = to_unicode(parser.parse_ansi(string, strip_ansi=True, mxp=True)) string = parser.parse_ansi(string, xterm256=True, mxp=True) elif clean_string is not None: # We have an explicit clean string. @@ -781,8 +777,7 @@ class ANSIString(unicode): """ code_indexes = [] - #for match in self.parser.ansi_regex.finditer(self._raw_string): - for match in self.parser.tags_regex.finditer(self._raw_string): + for match in self.parser.ansi_regex.finditer(self._raw_string): code_indexes.extend(range(match.start(), match.end())) if not code_indexes: # Plain string, no ANSI codes. diff --git a/evennia/utils/tests.py b/evennia/utils/tests.py index 463639b52d..b75d813632 100644 --- a/evennia/utils/tests.py +++ b/evennia/utils/tests.py @@ -5,8 +5,8 @@ try: except ImportError: from django.test import TestCase -from ansi import ANSIString -import utils +from .ansi import ANSIString +from evennia import utils class ANSIStringTestCase(TestCase): @@ -121,6 +121,20 @@ class ANSIStringTestCase(TestCase): result = u'\x1b[1m\x1b[32mTest\x1b[0m' self.checker(target.capitalize(), result, u'Test') + def test_mxp_agnostic(self): + """ + Make sure MXP tags are not treated like ANSI codes, but normal text. + """ + mxp1 = "{lclook{ltat{le" + mxp2 = "Start to {lclook here{ltclick somewhere here{le first" + self.assertEqual(15, len(ANSIString(mxp1))) + self.assertEqual(53, len(ANSIString(mxp2))) + # These would indicate an issue with the tables. + self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0])) + self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0])) + self.assertEqual(mxp1, ANSIString(mxp1)) + self.assertEqual(mxp2, unicode(ANSIString(mxp2))) + class TestIsIter(TestCase): def test_is_iter(self): @@ -177,3 +191,26 @@ class TestListToString(TestCase): self.assertEqual('1, 2 and 3', utils.list_to_string([1,2,3])) self.assertEqual('"1", "2" and "3"', utils.list_to_string([1,2,3], endsep="and", addquote=True)) + +class TestMLen(TestCase): + """ + Verifies that m_len behaves like len in all situations except those + where MXP may be involved. + """ + def test_non_mxp_string(self): + self.assertEqual(utils.m_len('Test_string'), 11) + + def test_mxp_string(self): + self.assertEqual(utils.m_len('{lclook{ltat{le'), 2) + + def test_mxp_ansi_string(self): + self.assertEqual(utils.m_len(ANSIString('{lcl{gook{ltat{le{n')), 2) + + def test_non_mxp_ansi_string(self): + self.assertEqual(utils.m_len(ANSIString('{gHello{n')), 5) + + def test_list(self): + self.assertEqual(utils.m_len([None, None]), 2) + + def test_dict(self): + self.assertEqual(utils.m_len({'hello': True, 'Goodbye': False}), 2) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index cb69950f7e..013447d977 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1245,3 +1245,13 @@ def calledby(callerdepth=1): return "[called by '%s': %s:%s %s]" % (frame[3], path, frame[2], frame[4]) +def m_len(target): + """ + Provides length checking for strings with MXP patterns, and falls + back to normal len for other objects. + """ + # Would create circular import if in module root. + from evennia.utils.ansi import ANSI_PARSER + if inherits_from(target, basestring): + return len(ANSI_PARSER.strip_mxp(target)) + return len(target)