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)