Merge pull request #3494 from michaelfaith84/hex_colors

Hex colors
This commit is contained in:
Griatch 2024-06-14 09:33:18 +02:00 committed by GitHub
commit 7c2ac4a655
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 433 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View 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")

View file

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