diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 6d337be5e..937c956b1 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -716,10 +716,10 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS): ap = ansi.ANSI_PARSER # ansi colors # show all ansi color-related codes - col1 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[48:56]] - col2 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ext_ansi_map[56:64]] + col1 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ansi_map[48:56]] + col2 = ["%s%s|n" % (code, code.replace("|", "||")) for code, _ in ap.ansi_map[56:64]] col3 = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) - for code, _ in ap.ext_ansi_map[-8:]] + for code, _ in ap.ansi_map[-8:]] col4 = ["%s%s|n" % (code.replace("\\", ""), code.replace("|", "||").replace("\\", "")) for code, _ in ap.ansi_bright_bgs[-8:]] col2.extend(["" for _ in range(len(col1)-len(col2))]) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 0f6887ad5..cc4078d40 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -4,7 +4,9 @@ Testing suite for contrib folder """ +import sys import datetime +from django.test import override_settings from evennia.commands.default.tests import CommandTest from evennia.utils.test_resources import EvenniaTest from mock import Mock, patch @@ -984,3 +986,72 @@ class TestUnixCommand(CommandTest): lines = ret.splitlines() self.assertTrue(any(l.startswith("usage:") for l in lines)) self.assertTrue(any(l.startswith("dummy: error:") for l in lines)) + + +from evennia.contrib import color_markups +from evennia.utils import ansi + + +class TestColorMarkup(EvenniaTest): + + def test_default_markup(self): + reload(ansi) + self.assertEqual(ansi.parse_ansi("A |rred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A {rred test"), 'A {rred test') + self.assertEqual(ansi.parse_ansi("A %crred test"), "A %crred test") + + @override_settings( + COLOR_ANSI_EXTRA_MAP=color_markups.CURLY_COLOR_ANSI_EXTRA_MAP, + COLOR_XTERM256_EXTRA_FG=color_markups.CURLY_COLOR_XTERM256_EXTRA_FG, + COLOR_XTERM256_EXTRA_BG=color_markups.CURLY_COLOR_XTERM256_EXTRA_BG, + COLOR_XTERM256_EXTRA_GFG=color_markups.CURLY_COLOR_XTERM256_EXTRA_GFG, + COLOR_XTERM256_EXTRA_GBG=color_markups.CURLY_COLOR_XTERM256_EXTRA_GBG, + COLOR_ANSI_BRIGHT_BG_EXTRA_MAP=color_markups.CURLY_COLOR_ANSI_BRIGHT_BG_EXTRA_MAP) + def test_curly_markup(self): + reload(ansi) # this is important since the module is initialized + self.assertEqual(ansi.parse_ansi("A |rred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A {rred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A {[Rred test"), 'A \x1b[41mred test') + self.assertEqual(ansi.parse_ansi("A |500red test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A {500red test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A {[500red test"), 'A \x1b[41mred test') + self.assertEqual(ansi.parse_ansi("A {=hgray test"), 'A \x1b[1m\x1b[30mgray test') + self.assertEqual(ansi.parse_ansi("A {[=hgray test"), 'A \x1b[40mgray test') + self.assertEqual(ansi.parse_ansi("A %crred test"), "A %crred test") + # fake ansi bright background + self.assertEqual(ansi.parse_ansi("A {[rred test"), 'A \x1b[41mred test') + + @override_settings( + COLOR_ANSI_EXTRA_MAP=color_markups.MUX_COLOR_ANSI_EXTRA_MAP, + COLOR_XTERM256_EXTRA_FG=color_markups.MUX_COLOR_XTERM256_EXTRA_FG, + COLOR_XTERM256_EXTRA_BG=color_markups.MUX_COLOR_XTERM256_EXTRA_BG, + COLOR_XTERM256_EXTRA_GFG=color_markups.MUX_COLOR_XTERM256_EXTRA_GFG, + COLOR_XTERM256_EXTRA_GBG=color_markups.MUX_COLOR_XTERM256_EXTRA_GBG, + COLOR_ANSI_BRIGHT_BG_EXTRA_MAP=color_markups.MUX_COLOR_ANSI_BRIGHT_BG_EXTRA_MAP) + def test_mux_markup(self): + reload(ansi) + self.assertEqual(ansi.parse_ansi("A |rred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A %ch%crred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A %ch%crred test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A |500red test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A %c500red test"), 'A \x1b[1m\x1b[31mred test') + self.assertEqual(ansi.parse_ansi("A %c[500red test"), 'A \x1b[41mred test') + self.assertEqual(ansi.parse_ansi("A %c=hgray test"), 'A \x1b[1m\x1b[30mgray test') + self.assertEqual(ansi.parse_ansi("A %c[=hgray test"), 'A \x1b[40mgray test') + self.assertEqual(ansi.parse_ansi("A {rred test"), "A {rred test") + # fake ansi bright background + self.assertEqual(ansi.parse_ansi("A %ch%cRred test"), 'A \x1b[41mred test') + + @override_settings( + COLOR_ANSI_EXTRA_MAP=color_markups.MUX_COLOR_ANSI_EXTRA_MAP, + COLOR_XTERM256_EXTRA_FG=color_markups.MUX_COLOR_XTERM256_EXTRA_FG, + COLOR_XTERM256_EXTRA_BG=color_markups.MUX_COLOR_XTERM256_EXTRA_BG, + COLOR_XTERM256_EXTRA_GFG=color_markups.MUX_COLOR_XTERM256_EXTRA_GFG, + COLOR_XTERM256_EXTRA_GBG=color_markups.MUX_COLOR_XTERM256_EXTRA_GBG, + COLOR_ANSI_BRIGHT_BG_EXTRA_MAP=color_markups.MUX_COLOR_ANSI_BRIGHT_BG_EXTRA_MAP, + COLOR_NO_DEFAULT=True) + def test_override(self): + reload(ansi) + self.assertEqual(ansi.parse_ansi("A |rred test"), "A |rred test") + self.assertEqual(ansi.parse_ansi("A {rred test"), "A {rred test") + self.assertEqual(ansi.parse_ansi("A %ch%crred test"), 'A \x1b[1m\x1b[31mred test') diff --git a/evennia/settings_default.py b/evennia/settings_default.py index a9b499943..cd33a6709 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -331,10 +331,34 @@ PROTOTYPE_MODULES = ["world.prototypes"] # dummyrunner for more information) DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings" # Mapping to extend Evennia's normal ANSI color tags. The mapping is a list of -# tuples mapping the tag to the ANSI convertion, like `("%c%r", ansi.ANSI_RED)` -# (the evennia.utils.ansi module contains all ANSI escape sequences). This is -# mainly supplied for support of legacy codebase tag formats. +# tuples mapping the tag to the ANSI convertion, like `(r"%c%r", ansi.ANSI_RED)` +# (the evennia.utils.ansi module contains all ANSI escape sequences). Default +# is to use `|` and `|[` prefixes. COLOR_ANSI_EXTRA_MAP = [] +# Extend the available regexes for adding XTERM256 colors in-game. This is given +# as a list of regexes, where each regex must contain three anonymous groups for +# holding integers 0-5 for the red, green and blue components Default is +# is r'\|([0-5])([0-5])([0-5])', which allows e.g. |500 for red. +# XTERM256 foreground color replacement +COLOR_XTERM256_EXTRA_FG = [] +# XTERM256 background color replacement. Default is \|\[([0-5])([0-5])([0-5])' +COLOR_XTERM256_EXTRA_BG = [] +# Extend the available regexes for adding XTERM256 grayscale values in-game. Given +# as a list of regexes, where each regex must contain one anonymous group containing +# a single letter a-z to mark the level from white to black. Default is r'\|=([a-z])', +# which allows e.g. |=k for a medium gray. +# XTERM256 grayscale foreground +COLOR_XTERM256_EXTRA_GFG = [] +# XTERM256 grayscale background. Default is \|\[=([a-z])' +COLOR_XTERM256_EXTRA_GBG = [] +# ANSI does not support bright backgrounds, so Evennia fakes this by mapping it to +# XTERM256 backgrounds where supported. This is a list of tuples that maps the wanted +# regex to a valid XTERM256 background tag, such as `(r'{[r', r'{[500')`. +COLOR_ANSI_BRIGHT_BG_EXTRA_MAP = [] +# If set True, the above color settings *replace* the default |-style color markdown +# rather than extend it. +COLOR_NO_DEFAULT = False + ###################################################################### # Default command sets diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index dead3e00b..7c42dde0c 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -6,9 +6,6 @@ to apply colour to text according to the ANSI standard. Examples: This is |rRed text|n and this is normal again. - This is {rRed text{n and this is normal again. # soon to be depreciated - This is %crRed text%cn and this is normal again. # depreciated - Mostly you should not need to call parse_ansi() explicitly; it is run by Evennia just before returning data to/from the @@ -19,13 +16,17 @@ the ansi mapping. from builtins import object, range import re +from collections import OrderedDict from django.conf import settings from evennia.utils import utils +from evennia.utils import logger + from evennia.utils.utils import to_str, to_unicode from future.utils import with_metaclass + # ANSI definitions ANSI_BEEP = "\07" @@ -70,10 +71,11 @@ ANSI_SPACE = " " # Escapes ANSI_ESCAPES = ("{{", "\\\\", "\|\|") -from collections import OrderedDict _PARSE_CACHE = OrderedDict() _PARSE_CACHE_SIZE = 10000 +_COLOR_NO_DEFAULT = settings.COLOR_NO_DEFAULT + class ANSIParser(object): """ @@ -97,7 +99,7 @@ class ANSIParser(object): processed (str): The processed match string. """ - return self.ansi_map.get(ansimatch.group(), "") + return self.ansi_map_dict.get(ansimatch.group(), "") def sub_brightbg(self, ansimatch): """ @@ -113,7 +115,7 @@ class ANSIParser(object): """ return self.ansi_bright_bgs_map.get(ansimatch.group(), "") - def sub_xterm256(self, rgbmatch, use_xterm256=False): + def sub_xterm256(self, rgbmatch, use_xterm256=False, color_type="fg"): """ This is a replacer method called by `re.sub` with the matched tag. It must return the correct ansi sequence. @@ -124,6 +126,7 @@ class ANSIParser(object): Args: rgbmatch (re.matchobject): The match. use_xterm256 (bool, optional): Don't convert 256-colors to 16. + color_type (str): One of 'fg', 'bg', 'gfg', 'gbg'. Returns: processed (str): The processed match string. @@ -133,19 +136,26 @@ class ANSIParser(object): return "" # get tag, stripping the initial marker - rgbtag = rgbmatch.group()[1:] + #rgbtag = rgbmatch.group()[1:] + + background = color_type in ("bg", "gbg") + grayscale = color_type in ("gfg", "gbg") - background = rgbtag[0] == '[' - grayscale = rgbtag[0 + int(background)] == '=' if not grayscale: # 6x6x6 color-cube (xterm indexes 16-231) - if background: - red, green, blue = int(rgbtag[1]), int(rgbtag[2]), int(rgbtag[3]) - else: - red, green, blue = int(rgbtag[0]), int(rgbtag[1]), int(rgbtag[2]) + try: + red, green, blue = [int(val) for val in rgbmatch.groups() if val is not None] + except (IndexError, ValueError): + logger.log_trace() + return rgbmatch.group(0) else: # grayscale values (xterm indexes 0, 232-255, 15) for full spectrum - letter = rgbtag[int(background) + 1] + try: + letter = [val for val in rgbmatch.groups() if val is not None][0] + except IndexError: + logger.log_trace() + return rgbmatch.group(0) + if letter == 'a': colval = 16 # pure black @ index 16 (first color cube entry) elif letter == 'z': @@ -286,8 +296,17 @@ class ANSIParser(object): # pre-convert bright colors to xterm256 color tags string = self.brightbg_sub.sub(self.sub_brightbg, string) - def do_xterm256(part): - return self.sub_xterm256(part, xterm256) + def do_xterm256_fg(part): + return self.sub_xterm256(part, xterm256, "fg") + + def do_xterm256_bg(part): + return self.sub_xterm256(part, xterm256, "bg") + + def do_xterm256_gfg(part): + return self.sub_xterm256(part, xterm256, "gfg") + + def do_xterm256_gbg(part): + return self.sub_xterm256(part, xterm256, "gbg") in_string = utils.to_str(string) @@ -295,7 +314,10 @@ class ANSIParser(object): parsed_string = "" parts = self.ansi_escapes.split(in_string) + [" "] for part, sep in zip(parts[::2], parts[1::2]): - pstring = self.xterm256_sub.sub(do_xterm256, part) + pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, part) + pstring = self.xterm256_bg_sub.sub(do_xterm256_bg, pstring) + pstring = self.xterm256_gfg_sub.sub(do_xterm256_gfg, pstring) + pstring = self.xterm256_gbg_sub.sub(do_xterm256_gbg, pstring) pstring = self.ansi_sub.sub(self.sub_ansi, pstring) parsed_string += "%s%s" % (pstring, sep[0].strip()) @@ -319,55 +341,7 @@ class ANSIParser(object): hilite = ANSI_HILITE unhilite = ANSI_UNHILITE - ext_ansi_map = [ - (r'{n', ANSI_NORMAL), # reset - (r'{/', ANSI_RETURN), # line break - (r'{-', ANSI_TAB), # tab - (r'{_', ANSI_SPACE), # space - (r'{*', ANSI_INVERSE), # invert - (r'{^', ANSI_BLINK), # blinking text (very annoying and not supported by all clients) - (r'{u', ANSI_UNDERLINE), # underline - - (r'{r', hilite + ANSI_RED), - (r'{g', hilite + ANSI_GREEN), - (r'{y', hilite + ANSI_YELLOW), - (r'{b', hilite + ANSI_BLUE), - (r'{m', hilite + ANSI_MAGENTA), - (r'{c', hilite + ANSI_CYAN), - (r'{w', hilite + ANSI_WHITE), # pure white - (r'{x', hilite + ANSI_BLACK), # dark grey - - (r'{R', unhilite + ANSI_RED), - (r'{G', unhilite + ANSI_GREEN), - (r'{Y', unhilite + ANSI_YELLOW), - (r'{B', unhilite + ANSI_BLUE), - (r'{M', unhilite + ANSI_MAGENTA), - (r'{C', unhilite + ANSI_CYAN), - (r'{W', unhilite + ANSI_WHITE), # light grey - (r'{X', unhilite + ANSI_BLACK), # pure black - - # hilight-able colors - (r'{h', hilite), - (r'{H', unhilite), - - (r'{!R', ANSI_RED), - (r'{!G', ANSI_GREEN), - (r'{!Y', ANSI_YELLOW), - (r'{!B', ANSI_BLUE), - (r'{!M', ANSI_MAGENTA), - (r'{!C', ANSI_CYAN), - (r'{!W', ANSI_WHITE), # light grey - (r'{!X', ANSI_BLACK), # pure black - - # normal ANSI backgrounds - (r'{[R', ANSI_BACK_RED), - (r'{[G', ANSI_BACK_GREEN), - (r'{[Y', ANSI_BACK_YELLOW), - (r'{[B', ANSI_BACK_BLUE), - (r'{[M', ANSI_BACK_MAGENTA), - (r'{[C', ANSI_BACK_CYAN), - (r'{[W', ANSI_BACK_WHITE), # light grey background - (r'{[X', ANSI_BACK_BLACK), # pure black background + ansi_map = [ # alternative |-format @@ -420,21 +394,12 @@ class ANSIParser(object): (r'|[W', ANSI_BACK_WHITE), # light grey background (r'|[X', ANSI_BACK_BLACK) # pure black background ] - ext_ansi_map += settings.COLOR_ANSI_EXTRA_MAP ansi_bright_bgs = [ # "bright" ANSI backgrounds using xterm256 since ANSI # standard does not support it (will # fallback to dark ANSI background colors if xterm256 # is not supported by client) - (r'{[r', r'{[500'), - (r'{[g', r'{[050'), - (r'{[y', r'{[550'), - (r'{[b', r'{[005'), - (r'{[m', r'{[505'), - (r'{[c', r'{[055'), - (r'{[w', r'{[555'), # white background - (r'{[x', r'{[222'), # dark grey background # |-style variations (r'|[r', r'|[500'), @@ -446,33 +411,43 @@ class ANSIParser(object): (r'|[w', r'|[555'), # white background (r'|[x', r'|[222')] # dark grey background - # xterm256 {123, %c134. These are replaced directly by + # xterm256. These are replaced directly by # the sub_xterm256 method - xterm256_map = [ - (r'\{[0-5]{3}', ""), # {123 - foreground colour - (r'\{\[[0-5]{3}', ""), # {[123 - background colour - # |-style - (r'\|[0-5]{3}', ""), # |123 - foreground colour - (r'\|\[[0-5]{3}', ""), # |[123 - background colour - - # grayscale entries including ansi extremes: {=a .. {=z - (r'\{=[a-z]', ""), - (r'\{\[=[a-z]', ""), - (r'\|=[a-z]', ""), - (r'\|\[=[a-z]', ""), - ] + if settings.COLOR_NO_DEFAULT: + ansi_map = settings.COLOR_ANSI_EXTRA_MAP + xterm256_fg = settings.COLOR_XTERM256_EXTRA_FG + xterm256_bg = settings.COLOR_XTERM256_EXTRA_BG + xterm256_gfg = settings.COLOR_XTERM256_EXTRA_GFG + xterm256_gbg = settings.COLOR_XTERM256_EXTRA_GBG + ansi_bright_bgs = settings.COLOR_ANSI_BRIGHT_BG_EXTRA_MAP + else: + xterm256_fg = [r'\|([0-5])([0-5])([0-5])'] # |123 - foreground colour + xterm256_bg = [r'\|\[([0-5])([0-5])([0-5])'] # |[123 - background colour + xterm256_gfg = [r'\|=([a-z])'] # |=a - greyscale foreground + xterm256_gbg = [r'\|\[=([a-z])'] # |[=a - greyscale background + ansi_map += settings.COLOR_ANSI_EXTRA_MAP + xterm256_fg += settings.COLOR_XTERM256_EXTRA_FG + xterm256_bg += settings.COLOR_XTERM256_EXTRA_BG + xterm256_gfg += settings.COLOR_XTERM256_EXTRA_GFG + xterm256_gbg += settings.COLOR_XTERM256_EXTRA_GBG + ansi_bright_bgs += settings.COLOR_ANSI_BRIGHT_BG_EXTRA_MAP mxp_re = r'\|lc(.*?)\|lt(.*?)\|le' # prepare regex matching brightbg_sub = re.compile(r"|".join([r"(?