diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py
index 5d95c402ed..2c07ccd7f1 100644
--- a/evennia/utils/ansi.py
+++ b/evennia/utils/ansi.py
@@ -69,9 +69,9 @@ from django.conf import settings
from evennia.utils import logger, utils
from evennia.utils.utils import to_str
-from evennia.utils.hex_colors import HexToTruecolor
+from evennia.utils.hex_colors import HexColors
-hex_sub = HexToTruecolor.hex_sub
+hex_sub = HexColors.hex_sub
MXP_ENABLED = settings.MXP_ENABLED
@@ -470,7 +470,7 @@ class ANSIParser(object):
string = self.brightbg_sub.sub(self.sub_brightbg, string)
def do_truecolor(part: re.Match, truecolor=truecolor):
- hex2truecolor = HexToTruecolor()
+ hex2truecolor = HexColors()
return hex2truecolor.sub_truecolor(part, truecolor)
def do_xterm256_fg(part):
diff --git a/evennia/utils/hex_colors.py b/evennia/utils/hex_colors.py
index 4118c179c3..dcea3531a7 100644
--- a/evennia/utils/hex_colors.py
+++ b/evennia/utils/hex_colors.py
@@ -1,7 +1,7 @@
import re
-class HexToTruecolor:
+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
@@ -14,19 +14,18 @@ class HexToTruecolor:
_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_3_BYTES = f'({_RE_BYTE})({_RE_BYTE})({_RE_BYTE})'
+ _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 truecolor_sub
- _RE_24_BIT_RGB_FG = f'{_RE_FG}{_RE_3_BYTES}'
- _RE_24_BIT_RGB_BG = f'{_RE_BG}{_RE_3_BYTES}'
-
# 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)
@@ -73,6 +72,35 @@ class HexToTruecolor:
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
+
def _split_hex_to_bytes(self, tag: str) -> tuple[str, str, str]:
"""
Splits hex string into separate bytes:
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/text2html.py b/evennia/utils/text2html.py
index 1effdd3266..e8000a7f81 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,13 +71,11 @@ 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]))
)
colorlist = (
@@ -244,6 +246,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 +256,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 +271,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 +288,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 +334,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 +362,16 @@ 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 true color
+ prefix = (f'')
+
# close any prior span
if not clean:
prefix = "" + prefix
@@ -366,7 +403,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)