commit
7c2ac4a655
8 changed files with 433 additions and 26 deletions
|
|
@ -671,6 +671,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
||||||
"INPUTDEBUG": validate_bool,
|
"INPUTDEBUG": validate_bool,
|
||||||
"FORCEDENDLINE": validate_bool,
|
"FORCEDENDLINE": validate_bool,
|
||||||
"LOCALECHO": validate_bool,
|
"LOCALECHO": validate_bool,
|
||||||
|
"TRUECOLOR": validate_bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
name = self.lhs.upper()
|
name = self.lhs.upper()
|
||||||
|
|
@ -794,12 +795,12 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
|
||||||
testing which colors your client support
|
testing which colors your client support
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
color ansi | xterm256
|
color ansi | xterm256 | truecolor
|
||||||
|
|
||||||
Prints a color map along with in-mud color codes to use to produce
|
Prints a color map along with in-mud color codes to use to produce
|
||||||
them. It also tests what is supported in your client. Choices are
|
them. It also tests what is supported in your client. Choices are
|
||||||
16-color ansi (supported in most muds) or the 256-color xterm256
|
16-color ansi (supported in most muds), the 256-color xterm256
|
||||||
standard. No checking is done to determine your client supports
|
standard, or truecolor. No checking is done to determine your client supports
|
||||||
color - if not you will see rubbish appear.
|
color - if not you will see rubbish appear.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -838,6 +839,18 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
|
||||||
)
|
)
|
||||||
return ftable
|
return ftable
|
||||||
|
|
||||||
|
def make_hex_color_from_column(self, column_number):
|
||||||
|
r = 255 - column_number * 255 / 76
|
||||||
|
g = column_number * 510 / 76
|
||||||
|
b = column_number * 255 / 76
|
||||||
|
|
||||||
|
if g > 255:
|
||||||
|
g = 510 - g
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"#{hex(round(r))[2:].zfill(2)}{hex(round(g))[2:].zfill(2)}{hex(round(b))[2:].zfill(2)}"
|
||||||
|
)
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Show color tables"""
|
"""Show color tables"""
|
||||||
|
|
||||||
|
|
@ -916,9 +929,24 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
|
||||||
table = self.table_format(table)
|
table = self.table_format(table)
|
||||||
string += "\n" + "\n".join("".join(row) for row in table)
|
string += "\n" + "\n".join("".join(row) for row in table)
|
||||||
self.msg(string)
|
self.msg(string)
|
||||||
|
|
||||||
|
elif self.args.startswith("t"):
|
||||||
|
# show abbreviated truecolor sample (16.7 million colors in truecolor)
|
||||||
|
string = ""
|
||||||
|
for i in range(76):
|
||||||
|
string += f"|[{self.make_hex_color_from_column(i)} |n"
|
||||||
|
|
||||||
|
string += (
|
||||||
|
"\n"
|
||||||
|
+ "some of the truecolor colors (if not all hues show, your client might not report that it can"
|
||||||
|
" handle trucolor.):"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.msg(string)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# malformed input
|
# malformed input
|
||||||
self.msg("Usage: color ansi||xterm256")
|
self.msg("Usage: color ansi || xterm256 || truecolor")
|
||||||
|
|
||||||
|
|
||||||
class CmdQuell(COMMAND_DEFAULT_CLASS):
|
class CmdQuell(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
||||||
- xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
- xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
||||||
- noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
- noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
||||||
- nocolor: Strip all Color, regardless of ansi/xterm256 setting.
|
- nocolor: Strip all Color, regardless of ansi/xterm256 setting.
|
||||||
|
- truecolor: Enforce truecolor, regardless of TTYPE.
|
||||||
- raw: Pass string through without any ansi processing
|
- raw: Pass string through without any ansi processing
|
||||||
(i.e. include Evennia ansi markers but do not
|
(i.e. include Evennia ansi markers but do not
|
||||||
convert them into ansi tokens)
|
convert them into ansi tokens)
|
||||||
|
|
@ -447,6 +448,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
||||||
xterm256 = options.get(
|
xterm256 = options.get(
|
||||||
"xterm256", flags.get("XTERM256", False) if flags.get("TTYPE", False) else True
|
"xterm256", flags.get("XTERM256", False) if flags.get("TTYPE", False) else True
|
||||||
)
|
)
|
||||||
|
truecolor = options.get(
|
||||||
|
"truecolor", flags.get("TRUECOLOR", False) if flags.get("TTYPE", False) else True
|
||||||
|
)
|
||||||
useansi = options.get(
|
useansi = options.get(
|
||||||
"ansi", flags.get("ANSI", False) if flags.get("TTYPE", False) else True
|
"ansi", flags.get("ANSI", False) if flags.get("TTYPE", False) else True
|
||||||
)
|
)
|
||||||
|
|
@ -470,6 +474,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
||||||
_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
|
_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
|
||||||
strip_ansi=nocolor,
|
strip_ansi=nocolor,
|
||||||
xterm256=xterm256,
|
xterm256=xterm256,
|
||||||
|
truecolor=truecolor
|
||||||
)
|
)
|
||||||
if mxp:
|
if mxp:
|
||||||
prompt = mxp_parse(prompt)
|
prompt = mxp_parse(prompt)
|
||||||
|
|
@ -506,6 +511,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
|
||||||
strip_ansi=nocolor,
|
strip_ansi=nocolor,
|
||||||
xterm256=xterm256,
|
xterm256=xterm256,
|
||||||
mxp=mxp,
|
mxp=mxp,
|
||||||
|
truecolor=truecolor
|
||||||
)
|
)
|
||||||
if mxp:
|
if mxp:
|
||||||
linetosend = mxp_parse(linetosend)
|
linetosend = mxp_parse(linetosend)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class Ttype:
|
||||||
def __init__(self, protocol):
|
def __init__(self, protocol):
|
||||||
"""
|
"""
|
||||||
Initialize ttype by storing protocol on ourselves and calling
|
Initialize ttype by storing protocol on ourselves and calling
|
||||||
the client to see if it supporst ttype.
|
the client to see if it supports ttype.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
protocol (Protocol): The protocol instance.
|
protocol (Protocol): The protocol instance.
|
||||||
|
|
@ -147,9 +147,19 @@ class Ttype:
|
||||||
):
|
):
|
||||||
xterm256 = True
|
xterm256 = True
|
||||||
|
|
||||||
|
# use name to identify support for xterm truecolor
|
||||||
|
truecolor = False
|
||||||
|
if (clientname.endswith("-TRUECOLOR") or
|
||||||
|
clientname in (
|
||||||
|
"AXMUD",
|
||||||
|
"TINTIN"
|
||||||
|
)):
|
||||||
|
truecolor = True
|
||||||
|
|
||||||
# all clients supporting TTYPE at all seem to support ANSI
|
# all clients supporting TTYPE at all seem to support ANSI
|
||||||
self.protocol.protocol_flags["ANSI"] = True
|
self.protocol.protocol_flags["ANSI"] = True
|
||||||
self.protocol.protocol_flags["XTERM256"] = xterm256
|
self.protocol.protocol_flags["XTERM256"] = xterm256
|
||||||
|
self.protocol.protocol_flags["TRUECOLOR"] = truecolor
|
||||||
self.protocol.protocol_flags["CLIENTNAME"] = clientname
|
self.protocol.protocol_flags["CLIENTNAME"] = clientname
|
||||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ from django.conf import settings
|
||||||
|
|
||||||
from evennia.utils import logger, utils
|
from evennia.utils import logger, utils
|
||||||
from evennia.utils.utils import to_str
|
from evennia.utils.utils import to_str
|
||||||
|
from evennia.utils.hex_colors import HexColors
|
||||||
|
|
||||||
|
hex2truecolor = HexColors()
|
||||||
|
hex_sub = HexColors.hex_sub
|
||||||
|
|
||||||
MXP_ENABLED = settings.MXP_ENABLED
|
MXP_ENABLED = settings.MXP_ENABLED
|
||||||
|
|
||||||
|
|
@ -432,7 +436,7 @@ class ANSIParser(object):
|
||||||
"""
|
"""
|
||||||
return self.unsafe_tokens.sub("", string)
|
return self.unsafe_tokens.sub("", string)
|
||||||
|
|
||||||
def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
|
def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False, truecolor=False):
|
||||||
"""
|
"""
|
||||||
Parses a string, subbing color codes according to the stored
|
Parses a string, subbing color codes according to the stored
|
||||||
mapping.
|
mapping.
|
||||||
|
|
@ -459,13 +463,17 @@ class ANSIParser(object):
|
||||||
|
|
||||||
# check cached parsings
|
# check cached parsings
|
||||||
global _PARSE_CACHE
|
global _PARSE_CACHE
|
||||||
cachekey = "%s-%s-%s-%s" % (string, strip_ansi, xterm256, mxp)
|
cachekey = f"{string}-{strip_ansi}-{xterm256}-{mxp}-{truecolor}"
|
||||||
|
|
||||||
if cachekey in _PARSE_CACHE:
|
if cachekey in _PARSE_CACHE:
|
||||||
return _PARSE_CACHE[cachekey]
|
return _PARSE_CACHE[cachekey]
|
||||||
|
|
||||||
# pre-convert bright colors to xterm256 color tags
|
# pre-convert bright colors to xterm256 color tags
|
||||||
string = self.brightbg_sub.sub(self.sub_brightbg, string)
|
string = self.brightbg_sub.sub(self.sub_brightbg, string)
|
||||||
|
|
||||||
|
def do_truecolor(part: re.Match, truecolor=truecolor):
|
||||||
|
return hex2truecolor.sub_truecolor(part, truecolor)
|
||||||
|
|
||||||
def do_xterm256_fg(part):
|
def do_xterm256_fg(part):
|
||||||
return self.sub_xterm256(part, xterm256, "fg")
|
return self.sub_xterm256(part, xterm256, "fg")
|
||||||
|
|
||||||
|
|
@ -484,7 +492,8 @@ class ANSIParser(object):
|
||||||
parsed_string = []
|
parsed_string = []
|
||||||
parts = self.ansi_escapes.split(in_string) + [" "]
|
parts = self.ansi_escapes.split(in_string) + [" "]
|
||||||
for part, sep in zip(parts[::2], parts[1::2]):
|
for part, sep in zip(parts[::2], parts[1::2]):
|
||||||
pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, part)
|
pstring = hex_sub.sub(do_truecolor, part)
|
||||||
|
pstring = self.xterm256_fg_sub.sub(do_xterm256_fg, pstring)
|
||||||
pstring = self.xterm256_bg_sub.sub(do_xterm256_bg, pstring)
|
pstring = self.xterm256_bg_sub.sub(do_xterm256_bg, pstring)
|
||||||
pstring = self.xterm256_gfg_sub.sub(do_xterm256_gfg, pstring)
|
pstring = self.xterm256_gfg_sub.sub(do_xterm256_gfg, pstring)
|
||||||
pstring = self.xterm256_gbg_sub.sub(do_xterm256_gbg, pstring)
|
pstring = self.xterm256_gbg_sub.sub(do_xterm256_gbg, pstring)
|
||||||
|
|
@ -516,7 +525,9 @@ ANSI_PARSER = ANSIParser()
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False):
|
def parse_ansi(
|
||||||
|
string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False, truecolor=False
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Parses a string, subbing color codes as needed.
|
Parses a string, subbing color codes as needed.
|
||||||
|
|
||||||
|
|
@ -526,13 +537,16 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp
|
||||||
parser (ansi.AnsiParser, optional): A parser instance to use.
|
parser (ansi.AnsiParser, optional): A parser instance to use.
|
||||||
xterm256 (bool, optional): Support xterm256 or not.
|
xterm256 (bool, optional): Support xterm256 or not.
|
||||||
mxp (bool, optional): Support MXP markup or not.
|
mxp (bool, optional): Support MXP markup or not.
|
||||||
|
truecolor (bool, optional): Support for truecolor or not.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string (str): The parsed string.
|
string (str): The parsed string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
string = string or ""
|
string = string or ""
|
||||||
return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp)
|
return parser.parse_ansi(
|
||||||
|
string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp, truecolor=truecolor
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def strip_ansi(string, parser=ANSI_PARSER):
|
def strip_ansi(string, parser=ANSI_PARSER):
|
||||||
|
|
|
||||||
173
evennia/utils/hex_colors.py
Normal file
173
evennia/utils/hex_colors.py
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class HexColors:
|
||||||
|
"""
|
||||||
|
This houses a method for converting hex codes to xterm truecolor codes
|
||||||
|
or falls back to evennia xterm256 codes to be handled by sub_xterm256
|
||||||
|
|
||||||
|
Based on code from @InspectorCaracal
|
||||||
|
"""
|
||||||
|
|
||||||
|
_RE_FG = "\|#"
|
||||||
|
_RE_BG = "\|\[#"
|
||||||
|
_RE_FG_OR_BG = "\|\[?#"
|
||||||
|
_RE_HEX_LONG = "[0-9a-fA-F]{6}"
|
||||||
|
_RE_HEX_SHORT = "[0-9a-fA-F]{3}"
|
||||||
|
_RE_BYTE = "[0-2]?[0-9]?[0-9]"
|
||||||
|
_RE_XTERM_TRUECOLOR = rf"\[([34])8;2;({_RE_BYTE});({_RE_BYTE});({_RE_BYTE})m"
|
||||||
|
|
||||||
|
# Used in hex_sub
|
||||||
|
_RE_HEX_PATTERN = f"({_RE_FG_OR_BG})({_RE_HEX_LONG}|{_RE_HEX_SHORT})"
|
||||||
|
|
||||||
|
# Used for greyscale
|
||||||
|
_GREYS = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
TRUECOLOR_FG = f"\x1b\[38;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||||
|
TRUECOLOR_BG = f"\x1b\[48;2;{_RE_BYTE};{_RE_BYTE};{_RE_BYTE}m"
|
||||||
|
|
||||||
|
# Our matchers for use with ANSIParser and ANSIString
|
||||||
|
hex_sub = re.compile(rf"{_RE_HEX_PATTERN}", re.DOTALL)
|
||||||
|
|
||||||
|
def _split_hex_to_bytes(self, tag: str) -> tuple[str, str, str]:
|
||||||
|
"""
|
||||||
|
Splits hex string into separate bytes:
|
||||||
|
#00FF00 -> ('00', 'FF', '00')
|
||||||
|
#CF3 -> ('CC', 'FF', '33')
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag (str): the tag to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: the text with converted tags
|
||||||
|
"""
|
||||||
|
strip_leading = re.compile(rf"{self._RE_FG_OR_BG}")
|
||||||
|
tag = strip_leading.sub("", tag)
|
||||||
|
|
||||||
|
if len(tag) == 6:
|
||||||
|
# 6 digits
|
||||||
|
r, g, b = (tag[i : i + 2] for i in range(0, 6, 2))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 3 digits
|
||||||
|
r, g, b = (tag[i : i + 1] * 2 for i in range(0, 3, 1))
|
||||||
|
|
||||||
|
return r, g, b
|
||||||
|
|
||||||
|
def _grey_int(self, num: int) -> int:
|
||||||
|
"""
|
||||||
|
Returns a grey greyscale integer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return round(max((int(num) - 8), 0) / 10)
|
||||||
|
|
||||||
|
def _hue_int(self, num: int) -> int:
|
||||||
|
return round(max((int(num) - 45), 0) / 40)
|
||||||
|
|
||||||
|
def _hex_to_rgb_24_bit(self, hex_code: str) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Converts a hex color code (#000 or #000000) into
|
||||||
|
a 3-int tuple (0, 255, 90)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hex_code (str): HTML hex color code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
24-bit rgb tuple: (int, int, int)
|
||||||
|
"""
|
||||||
|
# Strip the leading indicator if present
|
||||||
|
hex_code = re.sub(rf"{self._RE_FG_OR_BG}", "", hex_code)
|
||||||
|
|
||||||
|
r, g, b = self._split_hex_to_bytes(hex_code)
|
||||||
|
|
||||||
|
return int(r, 16), int(g, 16), int(b, 16)
|
||||||
|
|
||||||
|
def _rgb_24_bit_to_256(self, r: int, g: int, b: int) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
converts 0-255 hex color codes to 0-5
|
||||||
|
|
||||||
|
Args:
|
||||||
|
r (int): red
|
||||||
|
g (int): green
|
||||||
|
b (int): blue
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
256 color rgb tuple: (int, int, int)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._hue_int(r), self._hue_int(g), self._hue_int(b)
|
||||||
|
|
||||||
|
def sub_truecolor(self, match: re.Match, truecolor=False) -> str:
|
||||||
|
"""
|
||||||
|
Converts a hex string to xterm truecolor code, greyscale, or
|
||||||
|
falls back to evennia xterm256 to be handled by sub_xterm256
|
||||||
|
|
||||||
|
Args:
|
||||||
|
match (re.match): first group is the leading indicator,
|
||||||
|
second is the tag
|
||||||
|
truecolor (bool): return xterm truecolor or fallback
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Newly formatted indicator and tag (str)
|
||||||
|
|
||||||
|
"""
|
||||||
|
indicator, tag = match.groups()
|
||||||
|
|
||||||
|
# Remove the # sign
|
||||||
|
indicator = indicator.replace("#", "")
|
||||||
|
|
||||||
|
r, g, b = self._hex_to_rgb_24_bit(tag)
|
||||||
|
|
||||||
|
# Is it greyscale?
|
||||||
|
if r == g and g == b:
|
||||||
|
return f"{indicator}=" + self._GREYS[self._grey_int(r)]
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not truecolor:
|
||||||
|
# Fallback to xterm256 syntax
|
||||||
|
r, g, b = self._rgb_24_bit_to_256(r, g, b)
|
||||||
|
return f"{indicator}{r}{g}{b}"
|
||||||
|
|
||||||
|
else:
|
||||||
|
xtag = f"\033["
|
||||||
|
if "[" in indicator:
|
||||||
|
# Background Color
|
||||||
|
xtag += "4"
|
||||||
|
|
||||||
|
else:
|
||||||
|
xtag += "3"
|
||||||
|
|
||||||
|
xtag += f"8;2;{r};{g};{b}m"
|
||||||
|
return xtag
|
||||||
|
|
||||||
|
def xterm_truecolor_to_html_style(self, fg="", bg="") -> str:
|
||||||
|
"""
|
||||||
|
Converts xterm truecolor to an html style property
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fg: xterm truecolor
|
||||||
|
bg: xterm truecolor
|
||||||
|
|
||||||
|
Returns: style='color and or background-color'
|
||||||
|
|
||||||
|
"""
|
||||||
|
prop = 'style="'
|
||||||
|
if fg != "":
|
||||||
|
res = re.search(self._RE_XTERM_TRUECOLOR, fg, re.DOTALL)
|
||||||
|
fg_bg, r, g, b = res.groups()
|
||||||
|
r = hex(int(r))[2:].zfill(2)
|
||||||
|
g = hex(int(g))[2:].zfill(2)
|
||||||
|
b = hex(int(b))[2:].zfill(2)
|
||||||
|
prop += f"color: #{r}{g}{b};"
|
||||||
|
if bg != "":
|
||||||
|
res = re.search(self._RE_XTERM_TRUECOLOR, bg, re.DOTALL)
|
||||||
|
fg_bg, r, g, b = res.groups()
|
||||||
|
r = hex(int(r))[2:].zfill(2)
|
||||||
|
g = hex(int(g))[2:].zfill(2)
|
||||||
|
b = hex(int(b))[2:].zfill(2)
|
||||||
|
prop += f"background-color: #{r}{g}{b};"
|
||||||
|
prop += f'"'
|
||||||
|
return prop
|
||||||
|
|
@ -46,6 +46,14 @@ class TestText2Html(TestCase):
|
||||||
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
|
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# True Color
|
||||||
|
self.assertEqual(
|
||||||
|
'<span class="" style="color: #ff0000;">red</span>foo',
|
||||||
|
parser.format_styles(
|
||||||
|
f'\x1b[38;2;255;0;0m' + "red" + ansi.ANSI_NORMAL + "foo"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_remove_bells(self):
|
def test_remove_bells(self):
|
||||||
parser = text2html.HTML_PARSER
|
parser = text2html.HTML_PARSER
|
||||||
self.assertEqual("foo", parser.remove_bells("foo"))
|
self.assertEqual("foo", parser.remove_bells("foo"))
|
||||||
|
|
|
||||||
127
evennia/utils/tests/test_truecolor.py
Normal file
127
evennia/utils/tests/test_truecolor.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from evennia.utils.ansi import ANSIString as AN, ANSIParser
|
||||||
|
|
||||||
|
parser = ANSIParser().parse_ansi
|
||||||
|
|
||||||
|
|
||||||
|
class TestANSIStringHex(TestCase):
|
||||||
|
"""
|
||||||
|
Tests the conversion of html hex colors
|
||||||
|
to xterm-style colors
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.str = "test "
|
||||||
|
self.output1 = "\x1b[38;5;16mtest \x1b[0m"
|
||||||
|
self.output2 = "\x1b[48;5;16mtest \x1b[0m"
|
||||||
|
self.output3 = "\x1b[38;5;46mtest \x1b[0m"
|
||||||
|
self.output4 = "\x1b[48;5;46mtest \x1b[0m"
|
||||||
|
|
||||||
|
def test_long_grayscale_fg(self):
|
||||||
|
raw = f"|#000000{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output1, "Output")
|
||||||
|
|
||||||
|
def test_long_grayscale_bg(self):
|
||||||
|
raw = f"|[#000000{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output2, "Output")
|
||||||
|
|
||||||
|
def test_short_grayscale_fg(self):
|
||||||
|
raw = f"|#000{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output1, "Output")
|
||||||
|
|
||||||
|
def test_short_grayscale_bg(self):
|
||||||
|
raw = f"|[#000{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output2, "Output")
|
||||||
|
|
||||||
|
def test_short_color_fg(self):
|
||||||
|
raw = f"|#0F0{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output3, "Output")
|
||||||
|
|
||||||
|
def test_short_color_bg(self):
|
||||||
|
raw = f"|[#0f0{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output4, "Output")
|
||||||
|
|
||||||
|
def test_long_color_fg(self):
|
||||||
|
raw = f"|#00ff00{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output3, "Output")
|
||||||
|
|
||||||
|
def test_long_color_bg(self):
|
||||||
|
raw = f"|[#00FF00{self.str}|n"
|
||||||
|
ansi = AN(raw)
|
||||||
|
self.assertEqual(ansi.clean(), self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi.raw(), self.output4, "Output")
|
||||||
|
|
||||||
|
|
||||||
|
class TestANSIParser(TestCase):
|
||||||
|
"""
|
||||||
|
Tests the ansi fallback of the hex color conversion and
|
||||||
|
truecolor conversion
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.parser = ANSIParser().parse_ansi
|
||||||
|
self.str = "test "
|
||||||
|
|
||||||
|
# ANSI FALLBACK
|
||||||
|
# Red
|
||||||
|
self.output1 = "\x1b[1m\x1b[31mtest \x1b[0m"
|
||||||
|
# White
|
||||||
|
self.output2 = "\x1b[1m\x1b[37mtest \x1b[0m"
|
||||||
|
# Red BG
|
||||||
|
self.output3 = "\x1b[41mtest \x1b[0m"
|
||||||
|
# Blue FG, Red BG
|
||||||
|
self.output4 = "\x1b[41m\x1b[1m\x1b[34mtest \x1b[0m"
|
||||||
|
|
||||||
|
def test_hex_color(self):
|
||||||
|
raw = f"|#F00{self.str}|n"
|
||||||
|
ansi = parser(raw)
|
||||||
|
# self.assertEqual(ansi, self.str, "Cleaned")
|
||||||
|
self.assertEqual(ansi, self.output1, "Output")
|
||||||
|
|
||||||
|
def test_hex_greyscale(self):
|
||||||
|
raw = f"|#FFF{self.str}|n"
|
||||||
|
ansi = parser(raw)
|
||||||
|
self.assertEqual(ansi, self.output2, "Output")
|
||||||
|
|
||||||
|
def test_hex_color_bg(self):
|
||||||
|
raw = f"|[#Ff0000{self.str}|n"
|
||||||
|
ansi = parser(raw)
|
||||||
|
self.assertEqual(ansi, self.output3, "Output")
|
||||||
|
|
||||||
|
def test_hex_color_fg_bg(self):
|
||||||
|
raw = f"|[#Ff0000|#00f{self.str}|n"
|
||||||
|
ansi = parser(raw)
|
||||||
|
self.assertEqual(ansi, self.output4, "Output")
|
||||||
|
|
||||||
|
def test_truecolor_fg(self):
|
||||||
|
raw = f"|#00c700{self.str}|n"
|
||||||
|
ansi = parser(raw, truecolor=True)
|
||||||
|
output = f"\x1b[38;2;0;199;0m{self.str}\x1b[0m"
|
||||||
|
self.assertEqual(ansi, output, "Output")
|
||||||
|
|
||||||
|
def test_truecolor_bg(self):
|
||||||
|
raw = f"|[#00c700{self.str}|n"
|
||||||
|
ansi = parser(raw, truecolor=True)
|
||||||
|
output = f"\x1b[48;2;0;199;0m{self.str}\x1b[0m"
|
||||||
|
self.assertEqual(ansi, output, "Output")
|
||||||
|
|
||||||
|
def test_truecolor_fg_bg(self):
|
||||||
|
raw = f"|[#00c700|#880000{self.str}|n"
|
||||||
|
ansi = parser(raw, truecolor=True)
|
||||||
|
output = f"\x1b[48;2;0;199;0m\x1b[38;2;136;0;0m{self.str}\x1b[0m"
|
||||||
|
self.assertEqual(ansi, output, "Output")
|
||||||
|
|
@ -13,11 +13,15 @@ from html import escape as html_escape
|
||||||
|
|
||||||
from .ansi import *
|
from .ansi import *
|
||||||
|
|
||||||
|
from .hex_colors import HexColors
|
||||||
|
|
||||||
# All xterm256 RGB equivalents
|
# All xterm256 RGB equivalents
|
||||||
|
|
||||||
XTERM256_FG = "\033[38;5;{}m"
|
XTERM256_FG = "\033[38;5;{}m"
|
||||||
XTERM256_BG = "\033[48;5;{}m"
|
XTERM256_BG = "\033[48;5;{}m"
|
||||||
|
|
||||||
|
hex_colors = HexColors()
|
||||||
|
|
||||||
|
|
||||||
class TextToHTMLparser(object):
|
class TextToHTMLparser(object):
|
||||||
"""
|
"""
|
||||||
|
|
@ -67,12 +71,12 @@ class TextToHTMLparser(object):
|
||||||
]
|
]
|
||||||
|
|
||||||
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
|
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
|
||||||
|
|
||||||
re_style = re.compile(
|
re_style = re.compile(
|
||||||
r"({})".format(
|
r"({}|{})".format(
|
||||||
"|".join(
|
"|".join(
|
||||||
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
|
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
|
||||||
).replace("[", r"\[")
|
).replace("[", r"\["),
|
||||||
|
"|".join([HexColors.TRUECOLOR_FG, HexColors.TRUECOLOR_BG]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -244,6 +248,7 @@ class TextToHTMLparser(object):
|
||||||
|
|
||||||
# split out the ANSI codes and clean out any empty items
|
# split out the ANSI codes and clean out any empty items
|
||||||
str_list = [substr for substr in self.re_style.split(text) if substr]
|
str_list = [substr for substr in self.re_style.split(text) if substr]
|
||||||
|
|
||||||
# initialize all the flags and classes
|
# initialize all the flags and classes
|
||||||
classes = []
|
classes = []
|
||||||
clean = True
|
clean = True
|
||||||
|
|
@ -253,6 +258,8 @@ class TextToHTMLparser(object):
|
||||||
fg = ANSI_WHITE
|
fg = ANSI_WHITE
|
||||||
# default bg is black
|
# default bg is black
|
||||||
bg = ANSI_BACK_BLACK
|
bg = ANSI_BACK_BLACK
|
||||||
|
truecolor_fg = ""
|
||||||
|
truecolor_bg = ""
|
||||||
|
|
||||||
for i, substr in enumerate(str_list):
|
for i, substr in enumerate(str_list):
|
||||||
# reset all current styling
|
# reset all current styling
|
||||||
|
|
@ -266,6 +273,8 @@ class TextToHTMLparser(object):
|
||||||
hilight = ANSI_UNHILITE
|
hilight = ANSI_UNHILITE
|
||||||
fg = ANSI_WHITE
|
fg = ANSI_WHITE
|
||||||
bg = ANSI_BACK_BLACK
|
bg = ANSI_BACK_BLACK
|
||||||
|
truecolor_fg = ""
|
||||||
|
truecolor_bg = ""
|
||||||
|
|
||||||
# change color
|
# change color
|
||||||
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
|
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
|
||||||
|
|
@ -281,6 +290,14 @@ class TextToHTMLparser(object):
|
||||||
# set new bg
|
# set new bg
|
||||||
bg = substr
|
bg = substr
|
||||||
|
|
||||||
|
elif re.match(hex_colors.TRUECOLOR_FG, substr):
|
||||||
|
str_list[i] = ""
|
||||||
|
truecolor_fg = substr
|
||||||
|
|
||||||
|
elif re.match(hex_colors.TRUECOLOR_BG, substr):
|
||||||
|
str_list[i] = ""
|
||||||
|
truecolor_bg = substr
|
||||||
|
|
||||||
# non-color codes
|
# non-color codes
|
||||||
elif substr in self.style_codes:
|
elif substr in self.style_codes:
|
||||||
# erase ANSI code from output
|
# erase ANSI code from output
|
||||||
|
|
@ -319,6 +336,20 @@ class TextToHTMLparser(object):
|
||||||
color_index = self.colorlist.index(fg)
|
color_index = self.colorlist.index(fg)
|
||||||
|
|
||||||
if inverse:
|
if inverse:
|
||||||
|
if truecolor_fg != "" and truecolor_bg != "":
|
||||||
|
# True startcolor only
|
||||||
|
truecolor_fg, truecolor_bg = truecolor_bg, truecolor_fg
|
||||||
|
elif truecolor_fg != "" and truecolor_bg == "":
|
||||||
|
# Truecolor fg, class based bg
|
||||||
|
truecolor_bg = truecolor_fg
|
||||||
|
truecolor_fg = ""
|
||||||
|
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
|
||||||
|
elif truecolor_fg == "" and truecolor_bg != "":
|
||||||
|
# Truecolor bg, class based fg
|
||||||
|
truecolor_fg = truecolor_bg
|
||||||
|
truecolor_bg = ""
|
||||||
|
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
|
||||||
|
else:
|
||||||
# inverse means swap fg and bg indices
|
# inverse means swap fg and bg indices
|
||||||
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
|
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
|
||||||
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
|
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
|
||||||
|
|
@ -333,8 +364,18 @@ class TextToHTMLparser(object):
|
||||||
# light grey text is the default, don't explicitly style
|
# light grey text is the default, don't explicitly style
|
||||||
if color_class != "color-007":
|
if color_class != "color-007":
|
||||||
classes.append(color_class)
|
classes.append(color_class)
|
||||||
|
|
||||||
# define the new style span
|
# define the new style span
|
||||||
prefix = '<span class="{}">'.format(" ".join(classes))
|
if truecolor_fg == "" and truecolor_bg == "":
|
||||||
|
prefix = f'<span class="{" ".join(classes)}">'
|
||||||
|
else:
|
||||||
|
# Classes can't be used for truecolor--but they can be extras such as 'blink'
|
||||||
|
prefix = (
|
||||||
|
f"<span "
|
||||||
|
f'class="{" ".join(classes)}" '
|
||||||
|
f"{hex_colors.xterm_truecolor_to_html_style(fg=truecolor_fg, bg=truecolor_bg)}>"
|
||||||
|
)
|
||||||
|
|
||||||
# close any prior span
|
# close any prior span
|
||||||
if not clean:
|
if not clean:
|
||||||
prefix = "</span>" + prefix
|
prefix = "</span>" + prefix
|
||||||
|
|
@ -366,7 +407,7 @@ class TextToHTMLparser(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# parse everything to ansi first
|
# parse everything to ansi first
|
||||||
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
|
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True, truecolor=True)
|
||||||
# convert all ansi to html
|
# convert all ansi to html
|
||||||
result = re.sub(self.re_string, self.sub_text, text)
|
result = re.sub(self.re_string, self.sub_text, text)
|
||||||
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue