diff --git a/evennia/utils/tests/test_text2html.py b/evennia/utils/tests/test_text2html.py
index 3b67cd426e..361ab086cf 100644
--- a/evennia/utils/tests/test_text2html.py
+++ b/evennia/utils/tests/test_text2html.py
@@ -7,20 +7,20 @@ import mock
class TestText2Html(TestCase):
- def test_re_color(self):
+ def test_format_styles(self):
parser = text2html.HTML_PARSER
- self.assertEqual("foo", parser.re_color("foo"))
+ self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual(
'redfoo',
- parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
+ parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'redfoo',
- parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
+ parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
- 'redfoo',
- parser.re_color(
+ 'redfoo',
+ parser.format_styles(
ansi.ANSI_BACK_RED
+ ansi.ANSI_UNHILITE
+ ansi.ANSI_GREEN
@@ -29,63 +29,37 @@ class TestText2Html(TestCase):
+ "foo"
),
)
-
- @unittest.skip("parser issues")
- def test_re_bold(self):
- parser = text2html.HTML_PARSER
- self.assertEqual("foo", parser.re_bold("foo"))
self.assertEqual(
- # "a redfoo", # TODO: why not?
- "a redfoo",
- parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
- )
-
- @unittest.skip("parser issues")
- def test_re_underline(self):
- parser = text2html.HTML_PARSER
- self.assertEqual("foo", parser.re_underline("foo"))
- self.assertEqual(
- 'a red' + ansi.ANSI_NORMAL + "foo",
- parser.re_underline(
+ 'a redfoo',
+ parser.format_styles(
"a "
+ ansi.ANSI_UNDERLINE
+ "red"
- + ansi.ANSI_NORMAL # TODO: why does it keep it?
+ + ansi.ANSI_NORMAL
+ "foo"
),
)
-
- @unittest.skip("parser issues")
- def test_re_blinking(self):
- parser = text2html.HTML_PARSER
- self.assertEqual("foo", parser.re_blinking("foo"))
self.assertEqual(
- 'a red' + ansi.ANSI_NORMAL + "foo",
- parser.re_blinking(
+ 'a redfoo',
+ parser.format_styles(
"a "
+ ansi.ANSI_BLINK
+ "red"
- + ansi.ANSI_NORMAL # TODO: why does it keep it?
+ + ansi.ANSI_NORMAL
+ "foo"
),
)
-
- @unittest.skip("parser issues")
- def test_re_inversing(self):
- parser = text2html.HTML_PARSER
- self.assertEqual("foo", parser.re_inversing("foo"))
self.assertEqual(
- 'a red' + ansi.ANSI_NORMAL + "foo",
- parser.re_inversing(
+ 'a redfoo',
+ parser.format_styles(
"a "
+ ansi.ANSI_INVERSE
+ "red"
- + ansi.ANSI_NORMAL # TODO: why does it keep it?
+ + ansi.ANSI_NORMAL
+ "foo"
),
)
- @unittest.skip("parser issues")
def test_remove_bells(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.remove_bells("foo"))
@@ -95,7 +69,7 @@ class TestText2Html(TestCase):
"a "
+ ansi.ANSI_BEEP
+ "red"
- + ansi.ANSI_NORMAL # TODO: why does it keep it?
+ + ansi.ANSI_NORMAL
+ "foo"
),
)
@@ -110,7 +84,6 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.convert_linebreaks("foo"))
self.assertEqual("a
redfoo
", parser.convert_linebreaks("a\n redfoo\n"))
- @unittest.skip("parser issues")
def test_convert_urls(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.convert_urls("foo"))
@@ -118,7 +91,6 @@ class TestText2Html(TestCase):
'a http://redfoo runs',
parser.convert_urls("a http://redfoo runs"),
)
- # TODO: doesn't URL encode correctly
def test_sub_mxp_links(self):
parser = text2html.HTML_PARSER
@@ -186,22 +158,22 @@ class TestText2Html(TestCase):
self.assertEqual("foo", text2html.parse_html("foo"))
self.maxDiff = None
self.assertEqual(
- # TODO: note that the blink is currently *not* correctly aborted
- # with |n here! This is probably not possible to correctly handle
- # with regex - a stateful parser may be needed.
- # blink back-cyan normal underline red green yellow blue magenta cyan back-green
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
- ''
- 'Hello' # noqa
- ''
- 'W' # noqa
- 'o'
- 'r'
- 'l'
- 'd'
- '!'
- '!' # noqa
- ""
- ""
- "",
+ ''
+ 'Hello'
+ ''
+ 'W'
+ ''
+ 'o'
+ ''
+ 'r'
+ ''
+ 'l'
+ ''
+ 'd'
+ ''
+ '!'
+ ''
+ '!'
+ '',
)
diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py
index be8f459c87..af066fec68 100644
--- a/evennia/utils/text2html.py
+++ b/evennia/utils/text2html.py
@@ -12,11 +12,10 @@ import re
from html import escape as html_escape
from .ansi import *
-
# All xterm256 RGB equivalents
-XTERM256_FG = "\033[38;5;%sm"
-XTERM256_BG = "\033[48;5;%sm"
+XTERM256_FG = "\033[38;5;{}m"
+XTERM256_BG = "\033[48;5;{}m"
class TextToHTMLparser(object):
@@ -25,77 +24,55 @@ class TextToHTMLparser(object):
"""
tabstop = 4
- # mapping html color name <-> ansi code.
- hilite = ANSI_HILITE
- unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
- normal = ANSI_NORMAL # "
- underline = ANSI_UNDERLINE
- blink = ANSI_BLINK
- inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
- colorcodes = [
- ("color-000", unhilite + ANSI_BLACK), # pure black
- ("color-001", unhilite + ANSI_RED),
- ("color-002", unhilite + ANSI_GREEN),
- ("color-003", unhilite + ANSI_YELLOW),
- ("color-004", unhilite + ANSI_BLUE),
- ("color-005", unhilite + ANSI_MAGENTA),
- ("color-006", unhilite + ANSI_CYAN),
- ("color-007", unhilite + ANSI_WHITE), # light grey
- ("color-008", hilite + ANSI_BLACK), # dark grey
- ("color-009", hilite + ANSI_RED),
- ("color-010", hilite + ANSI_GREEN),
- ("color-011", hilite + ANSI_YELLOW),
- ("color-012", hilite + ANSI_BLUE),
- ("color-013", hilite + ANSI_MAGENTA),
- ("color-014", hilite + ANSI_CYAN),
- ("color-015", hilite + ANSI_WHITE), # pure white
- ] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
- colorback = [
- ("bgcolor-000", ANSI_BACK_BLACK), # pure black
- ("bgcolor-001", ANSI_BACK_RED),
- ("bgcolor-002", ANSI_BACK_GREEN),
- ("bgcolor-003", ANSI_BACK_YELLOW),
- ("bgcolor-004", ANSI_BACK_BLUE),
- ("bgcolor-005", ANSI_BACK_MAGENTA),
- ("bgcolor-006", ANSI_BACK_CYAN),
- ("bgcolor-007", ANSI_BACK_WHITE), # light grey
- ("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey
- ("bgcolor-009", hilite + ANSI_BACK_RED),
- ("bgcolor-010", hilite + ANSI_BACK_GREEN),
- ("bgcolor-011", hilite + ANSI_BACK_YELLOW),
- ("bgcolor-012", hilite + ANSI_BACK_BLUE),
- ("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
- ("bgcolor-014", hilite + ANSI_BACK_CYAN),
- ("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
- ] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
+ style_codes = [
+ # non-color style markers
+ ANSI_NORMAL,
+ ANSI_UNDERLINE,
+ ANSI_HILITE,
+ ANSI_UNHILITE,
+ ANSI_INVERSE,
+ ANSI_BLINK,
+ ANSI_INV_HILITE,
+ ANSI_BLINK_HILITE,
+ ANSI_INV_BLINK,
+ ANSI_INV_BLINK_HILITE,
+ ]
+
+ ansi_color_codes = [
+ # Foreground colors
+ ANSI_BLACK,
+ ANSI_RED,
+ ANSI_GREEN,
+ ANSI_YELLOW,
+ ANSI_BLUE,
+ ANSI_MAGENTA,
+ ANSI_CYAN,
+ ANSI_WHITE,
+ ]
+
+ xterm_fg_codes = [ XTERM256_FG.format(i + 16) for i in range(240) ]
- # make sure to escape [
- # colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
- # colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
- fg_colormap = dict((code, clr) for clr, code in colorcodes)
- bg_colormap = dict((code, clr) for clr, code in colorback)
+ ansi_bg_codes = [
+ # Background colors
+ ANSI_BACK_BLACK,
+ ANSI_BACK_RED,
+ ANSI_BACK_GREEN,
+ ANSI_BACK_YELLOW,
+ ANSI_BACK_BLUE,
+ ANSI_BACK_MAGENTA,
+ ANSI_BACK_CYAN,
+ ANSI_BACK_WHITE,
+ ]
+
+ xterm_bg_codes = [ XTERM256_BG.format(i + 16) for i in range(240) ]
+
+ re_style = re.compile(r"({})".format('|'.join(style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes).replace("[",r"\[")))
- # create stop markers
- fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
- bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
- bgfgstop = bgstop[:-2] + fgstop
+ colorlist = [ ANSI_UNHILITE + code for code in ansi_color_codes ] + [ ANSI_HILITE + code for code in ansi_color_codes ] + xterm_fg_codes
- fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
- bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
- bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
+ bglist = ansi_bg_codes + [ ANSI_HILITE + code for code in ansi_bg_codes ] + xterm_bg_codes
- # extract color markers, tagging the start marker and the text marked
- re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
- re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")")
- re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")")
-
- re_normal = re.compile(normal.replace("[", r"\["))
- re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
- re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
- re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
- re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
- re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
re_string = re.compile(
r"(?P[<&>])|(?P[\t]+)|(?P\r\n|\r|\n)",
re.S | re.M | re.I,
@@ -106,100 +83,6 @@ class TextToHTMLparser(object):
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
- def _sub_bgfg(self, colormatch):
- # print("colormatch.groups()", colormatch.groups())
- bgcode, fgcode, text = colormatch.groups()
- if not fgcode:
- ret = r"""%s""" % (
- self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
- text,
- )
- else:
- ret = r"""%s""" % (
- self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
- self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
- text,
- )
- return ret
-
- def _sub_fg(self, colormatch):
- code, text = colormatch.groups()
- return r"""%s""" % (self.fg_colormap.get(code, "err"), text)
-
- def _sub_bg(self, colormatch):
- code, text = colormatch.groups()
- return r"""%s""" % (self.bg_colormap.get(code, "err"), text)
-
- def re_color(self, text):
- """
- Replace ansi colors with html color class names. Let the
- client choose how it will display colors, if it wishes to.
-
- Args:
- text (str): the string with color to replace.
-
- Returns:
- text (str): Re-colored text.
-
- """
- text = self.re_bgfg.sub(self._sub_bgfg, text)
- text = self.re_fgs.sub(self._sub_fg, text)
- text = self.re_bgs.sub(self._sub_bg, text)
- text = self.re_normal.sub("", text)
- return text
-
- def re_bold(self, text):
- """
- Clean out superfluous hilights rather than set to make
- it match the look of telnet.
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- text = self.re_hilite.sub(r"\1", text)
- return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
-
- def re_underline(self, text):
- """
- Replace ansi underline with html underline class name.
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
-
- """
- return self.re_uline.sub(r'\1', text)
-
- def re_blinking(self, text):
- """
- Replace ansi blink with custom blink css class
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
- """
- return self.re_blink.sub(r'\1', text)
-
- def re_inversing(self, text):
- """
- Replace ansi inverse with custom inverse css class
-
- Args:
- text (str): Text to process.
-
- Returns:
- text (str): Processed text.
- """
- return self.re_inverse.sub(r'\1', text)
-
def remove_bells(self, text):
"""
Remove ansi specials
@@ -211,7 +94,7 @@ class TextToHTMLparser(object):
text (str): Processed text.
"""
- return text.replace("\07", "")
+ return text.replace(ANSI_BEEP, "")
def remove_backspaces(self, text):
"""
@@ -292,7 +175,7 @@ class TextToHTMLparser(object):
url=url, text=text
)
return val
-
+
def sub_text(self, match):
"""
Helper method to be passed to re.sub,
@@ -314,6 +197,126 @@ class TextToHTMLparser(object):
text = cdict["tab"].replace("\t", " " * (self.tabstop))
return text
return None
+
+ def format_styles(self, text):
+ """
+ Takes a string with parsed ANSI codes and replaces them with
+ HTML spans and CSS classes.
+
+ Args:
+ text (str): The string to process.
+
+ Returns:
+ text (str): Processed text.
+ """
+
+ # 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
+ inverse = False
+ # default color is light grey - unhilite + white
+ hilight = ANSI_UNHILITE
+ fg = ANSI_WHITE
+ # default bg is black
+ bg = ANSI_BACK_BLACK
+
+ for i, substr in enumerate(str_list):
+ # reset all current styling
+ if substr == ANSI_NORMAL and not clean:
+ # replace with close existing tag
+ str_list[i] = ""
+ # reset to defaults
+ classes = []
+ clean = True
+ inverse = False
+ hilight = ANSI_UNHILITE
+ fg = ANSI_WHITE
+ bg = ANSI_BACK_BLACK
+
+ # change color
+ elif substr in self.ansi_color_codes + self.xterm_fg_codes:
+ # erase ANSI code from output
+ str_list[i] = ""
+ # set new color
+ fg = substr
+
+ # change bg color
+ elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
+ # erase ANSI code from output
+ str_list[i] = ""
+ # set new bg
+ bg = substr
+
+ # non-color codes
+ elif substr in self.style_codes:
+ # erase ANSI code from output
+ str_list[i] = ""
+
+ # hilight codes
+ if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
+ # set new hilight status
+ hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
+
+ # inversion codes
+ if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
+ inverse = True
+
+ # blink codes
+ if substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE) and "blink" not in classes:
+ classes.append("blink")
+
+ # underline
+ if substr == ANSI_UNDERLINE and "underline" not in classes:
+ classes.append("underline")
+
+ else:
+ # normal text, add text back to list
+ if not str_list[i-1]:
+ # prior entry was cleared, which means style change
+ # get indices for the fg and bg codes
+ bg_index = self.bglist.index(bg)
+ try:
+ color_index = self.colorlist.index(hilight + fg)
+ except ValueError:
+ # xterm256 colors don't have the hilight codes
+ 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"))
+ else:
+ # use fg and bg indices for classes
+ bg_class = "bgcolor-{}".format(str(bg_index).rjust(3,"0"))
+ color_class = "color-{}".format(str(color_index).rjust(3,"0"))
+
+ # black bg is the default, don't explicitly style
+ if bg_class != "bgcolor-000":
+ classes.append(bg_class)
+ # 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))
+ # close any prior span
+ if not clean:
+ prefix = '' + prefix
+ # add span to output
+ str_list[i-1] = prefix
+
+ # clean out color classes to easily update next time
+ classes = [cls for cls in classes if "color" not in cls]
+ # flag as currently being styled
+ clean = False
+
+ # close span if necessary
+ if not clean:
+ str_list.append("")
+ # recombine back into string
+ return "".join(str_list)
+
def parse(self, text, strip_ansi=False):
"""
@@ -328,19 +331,14 @@ class TextToHTMLparser(object):
text (str): Parsed text.
"""
- # print(f"incoming text:\n{text}")
# parse everything to ansi first
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=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)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
- result = self.re_color(result)
- result = self.re_bold(result)
- result = self.re_underline(result)
- result = self.re_blinking(result)
- result = self.re_inversing(result)
result = self.remove_bells(result)
+ result = self.format_styles(result)
result = self.convert_linebreaks(result)
result = self.remove_backspaces(result)
result = self.convert_urls(result)