Move {- and %c color markup to contrib, make easily extendable, as per #1229

This commit is contained in:
Griatch 2017-07-19 20:32:21 +02:00
parent 83163ae4b3
commit 03f4ecb3a4
4 changed files with 168 additions and 98 deletions

View file

@ -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))])

View file

@ -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')

View file

@ -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

View file

@ -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"(?<!\|)%s" % re.escape(tup[0]) for tup in ansi_bright_bgs]), re.DOTALL)
xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ext_ansi_map]), re.DOTALL)
xterm256_fg_sub = re.compile(r"|".join(xterm256_fg), re.DOTALL)
xterm256_bg_sub = re.compile(r"|".join(xterm256_bg), re.DOTALL)
xterm256_gfg_sub = re.compile(r"|".join(xterm256_gfg), re.DOTALL)
xterm256_gbg_sub = re.compile(r"|".join(xterm256_gbg), re.DOTALL)
# xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ansi_map]), re.DOTALL)
mxp_sub = re.compile(mxp_re, re.DOTALL)
# used by regex replacer to correctly map ansi sequences
ansi_map = dict(ext_ansi_map)
ansi_map_dict = dict(ansi_map)
ansi_bright_bgs_map = dict(ansi_bright_bgs)
# prepare matching ansi codes overall