diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py
index 4fdb88f4ff..c0296a43dc 100644
--- a/evennia/commands/default/account.py
+++ b/evennia/commands/default/account.py
@@ -671,6 +671,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
"INPUTDEBUG": validate_bool,
"FORCEDENDLINE": validate_bool,
"LOCALECHO": validate_bool,
+ "TRUECOLOR": validate_bool,
}
name = self.lhs.upper()
@@ -794,12 +795,12 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
testing which colors your client support
Usage:
- color ansi | xterm256
+ color ansi | xterm256 | truecolor
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
- 16-color ansi (supported in most muds) or the 256-color xterm256
- standard. No checking is done to determine your client supports
+ 16-color ansi (supported in most muds), the 256-color xterm256
+ standard, or truecolor. No checking is done to determine your client supports
color - if not you will see rubbish appear.
"""
@@ -838,6 +839,18 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
)
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):
"""Show color tables"""
@@ -916,9 +929,24 @@ class CmdColorTest(COMMAND_DEFAULT_CLASS):
table = self.table_format(table)
string += "\n" + "\n".join("".join(row) for row in table)
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:
# malformed input
- self.msg("Usage: color ansi||xterm256")
+ self.msg("Usage: color ansi || xterm256 || truecolor")
class CmdQuell(COMMAND_DEFAULT_CLASS):
diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py
index e961152b16..cc0d2256a4 100644
--- a/evennia/server/portal/telnet.py
+++ b/evennia/server/portal/telnet.py
@@ -429,6 +429,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
- xterm256: Enforce xterm256 colors, regardless of TTYPE.
- noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
- nocolor: Strip all Color, regardless of ansi/xterm256 setting.
+ - truecolor: Enforce truecolor, regardless of TTYPE.
- raw: Pass string through without any ansi processing
(i.e. include Evennia ansi markers but do not
convert them into ansi tokens)
@@ -447,6 +448,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
xterm256 = options.get(
"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(
"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"),
strip_ansi=nocolor,
xterm256=xterm256,
+ truecolor=truecolor
)
if mxp:
prompt = mxp_parse(prompt)
@@ -506,6 +511,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
strip_ansi=nocolor,
xterm256=xterm256,
mxp=mxp,
+ truecolor=truecolor
)
if mxp:
linetosend = mxp_parse(linetosend)
diff --git a/evennia/server/portal/ttype.py b/evennia/server/portal/ttype.py
index 532d43f8a4..58705eb12d 100644
--- a/evennia/server/portal/ttype.py
+++ b/evennia/server/portal/ttype.py
@@ -44,7 +44,7 @@ class Ttype:
def __init__(self, protocol):
"""
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:
protocol (Protocol): The protocol instance.
@@ -130,10 +130,10 @@ class Ttype:
self.protocol.protocol_flags["NOPROMPTGOAHEAD"] = False
if (
- clientname.startswith("XTERM")
- or clientname.endswith("-256COLOR")
- or clientname
- in (
+ clientname.startswith("XTERM")
+ or clientname.endswith("-256COLOR")
+ or clientname
+ in (
"ATLANTIS", # > 0.9.9.0 (aug 2009)
"CMUD", # > 3.04 (mar 2009)
"KILDCLIENT", # > 2.2.0 (sep 2005)
@@ -143,13 +143,23 @@ class Ttype:
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
"POTATO", # > 2.00 (maybe earlier)
"TINYFUGUE", # > 4.x (maybe earlier)
- )
+ )
):
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
self.protocol.protocol_flags["ANSI"] = True
self.protocol.protocol_flags["XTERM256"] = xterm256
+ self.protocol.protocol_flags["TRUECOLOR"] = truecolor
self.protocol.protocol_flags["CLIENTNAME"] = clientname
self.protocol.requestNegotiation(TTYPE, SEND)
@@ -159,9 +169,9 @@ class Ttype:
tupper = term.upper()
# identify xterm256 based on flag
xterm256 = (
- tupper.endswith("-256COLOR")
- or tupper.endswith("XTERM") # Apple Terminal, old Tintin
- and not tupper.endswith("-COLOR") # old Tintin, Putty
+ tupper.endswith("-256COLOR")
+ or tupper.endswith("XTERM") # Apple Terminal, old Tintin
+ and not tupper.endswith("-COLOR") # old Tintin, Putty
)
if xterm256:
self.protocol.protocol_flags["ANSI"] = True
diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py
index 60c88c4be8..68dbc0c103 100644
--- a/evennia/utils/ansi.py
+++ b/evennia/utils/ansi.py
@@ -70,6 +70,10 @@ from django.conf import settings
from evennia.utils import logger, utils
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
@@ -432,7 +436,7 @@ class ANSIParser(object):
"""
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
mapping.
@@ -459,13 +463,17 @@ class ANSIParser(object):
# check cached parsings
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:
return _PARSE_CACHE[cachekey]
# pre-convert bright colors to xterm256 color tags
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):
return self.sub_xterm256(part, xterm256, "fg")
@@ -484,7 +492,8 @@ 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_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_gfg_sub.sub(do_xterm256_gfg, 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.
@@ -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.
xterm256 (bool, optional): Support xterm256 or not.
mxp (bool, optional): Support MXP markup or not.
+ truecolor (bool, optional): Support for truecolor or not.
Returns:
string (str): The parsed string.
"""
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):
diff --git a/evennia/utils/hex_colors.py b/evennia/utils/hex_colors.py
new file mode 100644
index 0000000000..781496ac71
--- /dev/null
+++ b/evennia/utils/hex_colors.py
@@ -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
diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py
index aea0cb87e7..74bd61bf18 100644
--- a/evennia/utils/tests/test_text2html.py
+++ b/evennia/utils/tests/test_text2html.py
@@ -46,6 +46,14 @@ class TestText2Html(TestCase):
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
)
+ # True Color
+ self.assertEqual(
+ 'redfoo',
+ parser.format_styles(
+ f'\x1b[38;2;255;0;0m' + "red" + ansi.ANSI_NORMAL + "foo"
+ ),
+ )
+
def test_remove_bells(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.remove_bells("foo"))
diff --git a/evennia/utils/tests/test_truecolor.py b/evennia/utils/tests/test_truecolor.py
new file mode 100644
index 0000000000..33fddeca16
--- /dev/null
+++ b/evennia/utils/tests/test_truecolor.py
@@ -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")
diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py
index 1effdd3266..bb9d642915 100644
--- a/evennia/utils/text2html.py
+++ b/evennia/utils/text2html.py
@@ -13,11 +13,15 @@ from html import escape as html_escape
from .ansi import *
+from .hex_colors import HexColors
+
# All xterm256 RGB equivalents
XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;{}m"
+hex_colors = HexColors()
+
class TextToHTMLparser(object):
"""
@@ -67,12 +71,12 @@ class TextToHTMLparser(object):
]
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
-
re_style = re.compile(
- r"({})".format(
+ r"({}|{})".format(
"|".join(
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
str_list = [substr for substr in self.re_style.split(text) if substr]
+
# initialize all the flags and classes
classes = []
clean = True
@@ -253,6 +258,8 @@ class TextToHTMLparser(object):
fg = ANSI_WHITE
# default bg is black
bg = ANSI_BACK_BLACK
+ truecolor_fg = ""
+ truecolor_bg = ""
for i, substr in enumerate(str_list):
# reset all current styling
@@ -266,6 +273,8 @@ class TextToHTMLparser(object):
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
bg = ANSI_BACK_BLACK
+ truecolor_fg = ""
+ truecolor_bg = ""
# change color
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
@@ -281,6 +290,14 @@ class TextToHTMLparser(object):
# set new bg
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
elif substr in self.style_codes:
# erase ANSI code from output
@@ -319,9 +336,23 @@ class TextToHTMLparser(object):
color_index = self.colorlist.index(fg)
if inverse:
- # inverse means swap fg and bg indices
- bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
- color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
+ 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
+ bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
+ color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
else:
# use fg and bg indices for classes
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
@@ -333,8 +364,18 @@ class TextToHTMLparser(object):
# light grey text is the default, don't explicitly style
if color_class != "color-007":
classes.append(color_class)
+
# define the new style span
- prefix = ''.format(" ".join(classes))
+ if truecolor_fg == "" and truecolor_bg == "":
+ prefix = f''
+ else:
+ # Classes can't be used for truecolor--but they can be extras such as 'blink'
+ prefix = (
+ f""
+ )
+
# close any prior span
if not clean:
prefix = "" + prefix
@@ -366,7 +407,7 @@ class TextToHTMLparser(object):
"""
# 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
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)