diff --git a/matrix/__init__.py b/matrix/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/matrix/colors.py b/matrix/colors.py
new file mode 100644
index 0000000..6e72b85
--- /dev/null
+++ b/matrix/colors.py
@@ -0,0 +1,845 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+from collections import namedtuple
+
+import webcolors
+
+try:
+ from HTMLParser import HTMLParser
+except ImportError:
+ from html.parser import HTMLParser
+
+
+FormattedString = namedtuple(
+ 'FormattedString',
+ ['text', 'attributes']
+)
+
+
+# TODO this should be a typed dict.
+DEFAULT_ATRIBUTES = {
+ "bold": False,
+ "italic": False,
+ "underline": False,
+ "strikethrough": False,
+ "quote": False,
+ "fgcolor": None,
+ "bgcolor": None
+}
+
+
+class MatrixHtmlParser(HTMLParser):
+ # TODO bg color
+ # TODO bullets
+ def __init__(self):
+ HTMLParser.__init__(self)
+ self.text = "" # type: str
+ self.substrings = [] # type: List[FormattedString]
+ self.attributes = DEFAULT_ATRIBUTES.copy()
+
+ def _toggle_attribute(self, attribute):
+ if self.text:
+ self.substrings.append(
+ FormattedString(self.text, self.attributes.copy())
+ )
+ self.text = ""
+ self.attributes[attribute] = not self.attributes[attribute]
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "strong":
+ self._toggle_attribute("bold")
+ elif tag == "em":
+ self._toggle_attribute("italic")
+ elif tag == "u":
+ self._toggle_attribute("underline")
+ elif tag == "del":
+ self._toggle_attribute("strikethrough")
+ elif tag == "blockquote":
+ self._toggle_attribute("quote")
+ elif tag == "blockquote":
+ self._toggle_attribute("quote")
+ elif tag == "font":
+ for key, value in attrs:
+ if key == "color":
+ color = color_html_to_weechat(value)
+
+ if not color:
+ continue
+
+ if self.text:
+ self.substrings.append(
+ FormattedString(self.text, self.attributes.copy())
+ )
+ self.text = ""
+ self.attributes["fgcolor"] = color
+ else:
+ # W.prnt("", "Unhandled tag {t}".format(t=tag))
+ pass
+
+ def handle_endtag(self, tag):
+ if tag == "strong":
+ self._toggle_attribute("bold")
+ elif tag == "em":
+ self._toggle_attribute("italic")
+ elif tag == "u":
+ self._toggle_attribute("underline")
+ elif tag == "del":
+ self._toggle_attribute("strikethrough")
+ elif tag == "blockquote":
+ self._toggle_attribute("quote")
+ elif tag == "font":
+ if self.text:
+ self.substrings.append(
+ FormattedString(self.text, self.attributes.copy())
+ )
+ self.text = ""
+ self.attributes["fgcolor"] = None
+ else:
+ pass
+
+ def handle_data(self, data):
+ self.text = self.text + data
+
+ def get_substrings(self):
+ if self.text:
+ self.substrings.append(
+ FormattedString(self.text, self.attributes.copy())
+ )
+
+ return self.substrings
+
+
+def color_line_to_weechat(color_string):
+ # type: (str) -> str
+ line_colors = {
+ "0": "white",
+ "1": "black",
+ "2": "blue",
+ "3": "green",
+ "4": "lightred",
+ "5": "red",
+ "6": "magenta",
+ "7": "brown",
+ "8": "yellow",
+ "9": "lightgreen",
+ "10": "cyan",
+ "11": "lightcyan",
+ "12": "lightblue",
+ "13": "lightmagenta",
+ "14": "darkgray",
+ "15": "gray",
+ "16": "52",
+ "17": "94",
+ "18": "100",
+ "19": "58",
+ "20": "22",
+ "21": "29",
+ "22": "23",
+ "23": "24",
+ "24": "17",
+ "25": "54",
+ "26": "53",
+ "27": "89",
+ "28": "88",
+ "29": "130",
+ "30": "142",
+ "31": "64",
+ "32": "28",
+ "33": "35",
+ "34": "30",
+ "35": "25",
+ "36": "18",
+ "37": "91",
+ "38": "90",
+ "39": "125",
+ "40": "124",
+ "41": "166",
+ "42": "184",
+ "43": "106",
+ "44": "34",
+ "45": "49",
+ "46": "37",
+ "47": "33",
+ "48": "19",
+ "49": "129",
+ "50": "127",
+ "51": "161",
+ "52": "196",
+ "53": "208",
+ "54": "226",
+ "55": "154",
+ "56": "46",
+ "57": "86",
+ "58": "51",
+ "59": "75",
+ "60": "21",
+ "61": "171",
+ "62": "201",
+ "63": "198",
+ "64": "203",
+ "65": "215",
+ "66": "227",
+ "67": "191",
+ "68": "83",
+ "69": "122",
+ "70": "87",
+ "71": "111",
+ "72": "63",
+ "73": "177",
+ "74": "207",
+ "75": "205",
+ "76": "217",
+ "77": "223",
+ "78": "229",
+ "79": "193",
+ "80": "157",
+ "81": "158",
+ "82": "159",
+ "83": "153",
+ "84": "147",
+ "85": "183",
+ "86": "219",
+ "87": "212",
+ "88": "16",
+ "89": "233",
+ "90": "235",
+ "91": "237",
+ "92": "239",
+ "93": "241",
+ "94": "244",
+ "95": "247",
+ "96": "250",
+ "97": "254",
+ "98": "231",
+ "99": "default"
+ }
+
+ assert color_string in line_colors
+
+ return line_colors[color_string]
+
+
+# The functions colour_dist_sq(), colour_to_6cube(), and colour_find_rgb
+# are python ports of the same named functions from the tmux
+# source, they are under the copyright of Nicholas Marriott, and Avi Halachmi
+# under the ISC license.
+# More info: https://github.com/tmux/tmux/blob/master/colour.c
+
+def colour_dist_sq(R, G, B, r, g, b):
+ # pylint: disable=invalid-name,too-many-arguments
+ # type: (int, int, int, int, int, int) -> int
+ return (R - r) * (R - r) + (G - g) * (G - g) + (B - b) * (B - b)
+
+
+def colour_to_6cube(v):
+ # pylint: disable=invalid-name
+ # type: (int) -> int
+ if v < 48:
+ return 0
+ if v < 114:
+ return 1
+ return (v - 35) // 40
+
+
+def colour_find_rgb(r, g, b):
+ # type: (int, int, int) -> int
+ """Convert an RGB triplet to the xterm(1) 256 colour palette.
+
+ xterm provides a 6x6x6 colour cube (16 - 231) and 24 greys (232 - 255).
+ We map our RGB colour to the closest in the cube, also work out the
+ closest grey, and use the nearest of the two.
+
+ Note that the xterm has much lower resolution for darker colours (they
+ are not evenly spread out), so our 6 levels are not evenly spread: 0x0,
+ 0x5f (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are
+ more evenly spread (8, 18, 28 ... 238).
+ """
+ # pylint: disable=invalid-name
+ q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
+
+ # Map RGB to 6x6x6 cube.
+ qr = colour_to_6cube(r)
+ qg = colour_to_6cube(g)
+ qb = colour_to_6cube(b)
+
+ cr = q2c[qr]
+ cg = q2c[qg]
+ cb = q2c[qb]
+
+ # If we have hit the colour exactly, return early.
+ if (cr == r and cg == g and cb == b):
+ return 16 + (36 * qr) + (6 * qg) + qb
+
+ # Work out the closest grey (average of RGB).
+ grey_avg = (r + g + b) // 3
+
+ if grey_avg > 238:
+ grey_idx = 23
+ else:
+ grey_idx = (grey_avg - 3) // 10
+
+ grey = 8 + (10 * grey_idx)
+
+ # Is grey or 6x6x6 colour closest?
+ d = colour_dist_sq(cr, cg, cb, r, g, b)
+
+ if colour_dist_sq(grey, grey, grey, r, g, b) < d:
+ idx = 232 + grey_idx
+ else:
+ idx = 16 + (36 * qr) + (6 * qg) + qb
+
+ return idx
+
+
+def color_html_to_weechat(color):
+ # type: (str) -> str
+ first_16 = {
+ (0, 0, 0): "black", # 0
+ (128, 0, 0): "red", # 1
+ (0, 128, 0): "green", # 2
+ (128, 128, 0): "brown", # 3
+ (0, 0, 128): "blue", # 4
+ (128, 0, 128): "magenta", # 5
+ (0, 128, 128): "cyan", # 6
+ (192, 192, 192): "default", # 7
+ (128, 128, 128): "gray", # 8
+ (255, 0, 0): "lightred", # 9
+ (0, 255, 0): "lightgreen", # 11
+ (255, 255, 0): "yellow", # 12
+ (0, 0, 255): "lightblue", # 13
+ (255, 0, 255): "lightmagenta", # 14
+ (0, 255, 255): "lightcyan", # 15
+ }
+
+ try:
+ rgb_color = webcolors.html5_parse_legacy_color(color)
+ except ValueError:
+ return None
+
+ if rgb_color in first_16:
+ return first_16[rgb_color]
+
+ return str(colour_find_rgb(*rgb_color))
+
+
+def color_weechat_to_html(color):
+ first_16 = {
+ "black": "black", # 0
+ "red": "maroon", # 1
+ "green": "green", # 2
+ "brown": "olive", # 3
+ "blue": "navy", # 4
+ "magenta": "purple", # 5
+ "cyan": "teal", # 6
+ "default": "silver", # 7
+ "gray": "grey", # 8
+ "lightred": "red", # 9
+ "lightgreen": "lime", # 11
+ "yellow": "yellow", # 12
+ "lightblue": "fuchsia", # 13
+ "lightmagenta": "aqua", # 14
+ "lightcyan": "white", # 15
+ }
+
+ hex_colors = {
+ "0": "#000000",
+ "1": "#800000",
+ "2": "#008000",
+ "3": "#808000",
+ "4": "#000080",
+ "5": "#800080",
+ "6": "#008080",
+ "7": "#c0c0c0",
+ "8": "#808080",
+ "9": "#ff0000",
+ "10": "#00ff00",
+ "11": "#ffff00",
+ "12": "#0000ff",
+ "13": "#ff00ff",
+ "14": "#00ffff",
+ "15": "#ffffff",
+ "16": "#000000",
+ "17": "#00005f",
+ "18": "#000087",
+ "19": "#0000af",
+ "20": "#0000d7",
+ "21": "#0000ff",
+ "22": "#005f00",
+ "23": "#005f5f",
+ "24": "#005f87",
+ "25": "#005faf",
+ "26": "#005fd7",
+ "27": "#005fff",
+ "28": "#008700",
+ "29": "#00875f",
+ "30": "#008787",
+ "31": "#0087af",
+ "32": "#0087d7",
+ "33": "#0087ff",
+ "34": "#00af00",
+ "35": "#00af5f",
+ "36": "#00af87",
+ "37": "#00afaf",
+ "38": "#00afd7",
+ "39": "#00afff",
+ "40": "#00d700",
+ "41": "#00d75f",
+ "42": "#00d787",
+ "43": "#00d7af",
+ "44": "#00d7d7",
+ "45": "#00d7ff",
+ "46": "#00ff00",
+ "47": "#00ff5f",
+ "48": "#00ff87",
+ "49": "#00ffaf",
+ "50": "#00ffd7",
+ "51": "#00ffff",
+ "52": "#5f0000",
+ "53": "#5f005f",
+ "54": "#5f0087",
+ "55": "#5f00af",
+ "56": "#5f00d7",
+ "57": "#5f00ff",
+ "58": "#5f5f00",
+ "59": "#5f5f5f",
+ "60": "#5f5f87",
+ "61": "#5f5faf",
+ "62": "#5f5fd7",
+ "63": "#5f5fff",
+ "64": "#5f8700",
+ "65": "#5f875f",
+ "66": "#5f8787",
+ "67": "#5f87af",
+ "68": "#5f87d7",
+ "69": "#5f87ff",
+ "70": "#5faf00",
+ "71": "#5faf5f",
+ "72": "#5faf87",
+ "73": "#5fafaf",
+ "74": "#5fafd7",
+ "75": "#5fafff",
+ "76": "#5fd700",
+ "77": "#5fd75f",
+ "78": "#5fd787",
+ "79": "#5fd7af",
+ "80": "#5fd7d7",
+ "81": "#5fd7ff",
+ "82": "#5fff00",
+ "83": "#5fff5f",
+ "84": "#5fff87",
+ "85": "#5fffaf",
+ "86": "#5fffd7",
+ "87": "#5fffff",
+ "88": "#870000",
+ "89": "#87005f",
+ "90": "#870087",
+ "91": "#8700af",
+ "92": "#8700d7",
+ "93": "#8700ff",
+ "94": "#875f00",
+ "95": "#875f5f",
+ "96": "#875f87",
+ "97": "#875faf",
+ "98": "#875fd7",
+ "99": "#875fff",
+ "100": "#878700",
+ "101": "#87875f",
+ "102": "#878787",
+ "103": "#8787af",
+ "104": "#8787d7",
+ "105": "#8787ff",
+ "106": "#87af00",
+ "107": "#87af5f",
+ "108": "#87af87",
+ "109": "#87afaf",
+ "110": "#87afd7",
+ "111": "#87afff",
+ "112": "#87d700",
+ "113": "#87d75f",
+ "114": "#87d787",
+ "115": "#87d7af",
+ "116": "#87d7d7",
+ "117": "#87d7ff",
+ "118": "#87ff00",
+ "119": "#87ff5f",
+ "120": "#87ff87",
+ "121": "#87ffaf",
+ "122": "#87ffd7",
+ "123": "#87ffff",
+ "124": "#af0000",
+ "125": "#af005f",
+ "126": "#af0087",
+ "127": "#af00af",
+ "128": "#af00d7",
+ "129": "#af00ff",
+ "130": "#af5f00",
+ "131": "#af5f5f",
+ "132": "#af5f87",
+ "133": "#af5faf",
+ "134": "#af5fd7",
+ "135": "#af5fff",
+ "136": "#af8700",
+ "137": "#af875f",
+ "138": "#af8787",
+ "139": "#af87af",
+ "140": "#af87d7",
+ "141": "#af87ff",
+ "142": "#afaf00",
+ "143": "#afaf5f",
+ "144": "#afaf87",
+ "145": "#afafaf",
+ "146": "#afafd7",
+ "147": "#afafff",
+ "148": "#afd700",
+ "149": "#afd75f",
+ "150": "#afd787",
+ "151": "#afd7af",
+ "152": "#afd7d7",
+ "153": "#afd7ff",
+ "154": "#afff00",
+ "155": "#afff5f",
+ "156": "#afff87",
+ "157": "#afffaf",
+ "158": "#afffd7",
+ "159": "#afffff",
+ "160": "#d70000",
+ "161": "#d7005f",
+ "162": "#d70087",
+ "163": "#d700af",
+ "164": "#d700d7",
+ "165": "#d700ff",
+ "166": "#d75f00",
+ "167": "#d75f5f",
+ "168": "#d75f87",
+ "169": "#d75faf",
+ "170": "#d75fd7",
+ "171": "#d75fff",
+ "172": "#d78700",
+ "173": "#d7875f",
+ "174": "#d78787",
+ "175": "#d787af",
+ "176": "#d787d7",
+ "177": "#d787ff",
+ "178": "#d7af00",
+ "179": "#d7af5f",
+ "180": "#d7af87",
+ "181": "#d7afaf",
+ "182": "#d7afd7",
+ "183": "#d7afff",
+ "184": "#d7d700",
+ "185": "#d7d75f",
+ "186": "#d7d787",
+ "187": "#d7d7af",
+ "188": "#d7d7d7",
+ "189": "#d7d7ff",
+ "190": "#d7ff00",
+ "191": "#d7ff5f",
+ "192": "#d7ff87",
+ "193": "#d7ffaf",
+ "194": "#d7ffd7",
+ "195": "#d7ffff",
+ "196": "#ff0000",
+ "197": "#ff005f",
+ "198": "#ff0087",
+ "199": "#ff00af",
+ "200": "#ff00d7",
+ "201": "#ff00ff",
+ "202": "#ff5f00",
+ "203": "#ff5f5f",
+ "204": "#ff5f87",
+ "205": "#ff5faf",
+ "206": "#ff5fd7",
+ "207": "#ff5fff",
+ "208": "#ff8700",
+ "209": "#ff875f",
+ "210": "#ff8787",
+ "211": "#ff87af",
+ "212": "#ff87d7",
+ "213": "#ff87ff",
+ "214": "#ffaf00",
+ "215": "#ffaf5f",
+ "216": "#ffaf87",
+ "217": "#ffafaf",
+ "218": "#ffafd7",
+ "219": "#ffafff",
+ "220": "#ffd700",
+ "221": "#ffd75f",
+ "222": "#ffd787",
+ "223": "#ffd7af",
+ "224": "#ffd7d7",
+ "225": "#ffd7ff",
+ "226": "#ffff00",
+ "227": "#ffff5f",
+ "228": "#ffff87",
+ "229": "#ffffaf",
+ "230": "#ffffd7",
+ "231": "#ffffff",
+ "232": "#080808",
+ "233": "#121212",
+ "234": "#1c1c1c",
+ "235": "#262626",
+ "236": "#303030",
+ "237": "#3a3a3a",
+ "238": "#444444",
+ "239": "#4e4e4e",
+ "240": "#585858",
+ "241": "#626262",
+ "242": "#6c6c6c",
+ "243": "#767676",
+ "244": "#808080",
+ "245": "#8a8a8a",
+ "246": "#949494",
+ "247": "#9e9e9e",
+ "248": "#a8a8a8",
+ "249": "#b2b2b2",
+ "250": "#bcbcbc",
+ "251": "#c6c6c6",
+ "252": "#d0d0d0",
+ "253": "#dadada",
+ "254": "#e4e4e4",
+ "255": "#eeeeee"
+ }
+
+ if color in first_16:
+ return first_16[color]
+
+ hex_color = hex_colors[color]
+
+ try:
+ return webcolors.hex_to_name(hex_color)
+ except ValueError:
+ return hex_color
+
+
+# TODO reverse video
+def parse_input_line(line):
+ # type: (str) -> List[FormattedString]
+ """Parses the weechat input line and produces formatted strings that can be
+ later converted to HTML or to a string for weechat's print functions
+ """
+ text = "" # type: str
+ substrings = [] # type: List[FormattedString]
+ attributes = DEFAULT_ATRIBUTES.copy()
+
+ i = 0
+ while i < len(line):
+ # Bold
+ if line[i] == "\x02":
+ if text:
+ substrings.append(FormattedString(text, attributes.copy()))
+ text = ""
+ attributes["bold"] = not attributes["bold"]
+ i = i + 1
+
+ # Color
+ elif line[i] == "\x03":
+ if text:
+ substrings.append(FormattedString(text, attributes.copy()))
+ text = ""
+ i = i + 1
+
+ # check if it's a valid color, add it to the attributes
+ if line[i].isdigit():
+ color_string = line[i]
+ i = i + 1
+
+ if line[i].isdigit():
+ if color_string == "0":
+ color_string = line[i]
+ else:
+ color_string = color_string + line[i]
+ i = i + 1
+
+ attributes["fgcolor"] = color_line_to_weechat(color_string)
+ else:
+ attributes["fgcolor"] = None
+
+ # check if we have a background color
+ if line[i] == "," and line[i+1].isdigit():
+ color_string = line[i+1]
+ i = i + 2
+
+ if line[i].isdigit():
+ if color_string == "0":
+ color_string = line[i]
+ else:
+ color_string = color_string + line[i]
+ i = i + 1
+
+ attributes["bgcolor"] = color_line_to_weechat(color_string)
+ else:
+ attributes["bgcolor"] = None
+ # Reset
+ elif line[i] == "\x0F":
+ if text:
+ substrings.append(FormattedString(text, attributes.copy()))
+ text = ""
+ # Reset all the attributes
+ attributes = DEFAULT_ATRIBUTES.copy()
+ i = i + 1
+ # Italic
+ elif line[i] == "\x1D":
+ if text:
+ substrings.append(FormattedString(text, attributes.copy()))
+ text = ""
+ attributes["italic"] = not attributes["italic"]
+ i = i + 1
+
+ # Underline
+ elif line[i] == "\x1F":
+ if text:
+ substrings.append(FormattedString(text, attributes.copy()))
+ text = ""
+ attributes["underline"] = not attributes["underline"]
+ i = i + 1
+
+ # Normal text
+ else:
+ text = text + line[i]
+ i = i + 1
+
+ substrings.append(FormattedString(text, attributes))
+ return substrings
+
+
+def formatted(strings):
+ for string in strings:
+ if string.attributes != DEFAULT_ATRIBUTES:
+ return True
+ return False
+
+
+def formatted_to_html(strings):
+ # type: (List[FormattedString]) -> str
+ # TODO BG COLOR
+ def add_attribute(string, name, value):
+ if name == "bold" and value:
+ return "{bold_on}{text}{bold_off}".format(
+ bold_on="",
+ text=string,
+ bold_off="")
+ elif name == "italic" and value:
+ return "{italic_on}{text}{italic_off}".format(
+ italic_on="",
+ text=string,
+ italic_off="")
+ elif name == "underline" and value:
+ return "{underline_on}{text}{underline_off}".format(
+ underline_on="",
+ text=string,
+ underline_off="")
+ elif name == "strikethrough" and value:
+ return "{strike_on}{text}{strike_off}".format(
+ strike_on="",
+ text=string,
+ strike_off="")
+ elif name == "quote" and value:
+ return "{quote_on}{text}{quote_off}".format(
+ quote_on="
", + text=string, + quote_off="") + elif name == "fgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on="".format( + color=color_weechat_to_html(value) + ), + text=string, + color_off="") + + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = add_attribute(text, key, value) + return text + + html_string = map(format_string, strings) + return "".join(html_string) + + +# TODO do we want at least some formating using unicode +# (strikethrough, quotes)? +def formatted_to_plain(strings): + # type: (List[FormattedString]) -> str + def strip_atribute(string, _, __): + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = strip_atribute(text, key, value) + return text + + plain_string = map(format_string, strings) + return "".join(plain_string) + + +def html_to_formatted(html): + parser = MatrixHtmlParser() + parser.feed(html) + return parser.get_substrings() + + +def string_strikethrough(string): + return "".join(["{}\u0336".format(c) for c in string]) + + +def formatted_to_weechat(W, strings): + # type: (weechat, List[colors.FormattedString]) -> str + # TODO BG COLOR + def add_attribute(string, name, value): + if name == "bold" and value: + return "{bold_on}{text}{bold_off}".format( + bold_on=W.color("bold"), + text=string, + bold_off=W.color("-bold")) + + elif name == "italic" and value: + return "{italic_on}{text}{italic_off}".format( + italic_on=W.color("italic"), + text=string, + italic_off=W.color("-italic")) + + elif name == "underline" and value: + return "{underline_on}{text}{underline_off}".format( + underline_on=W.color("underline"), + text=string, + underline_off=W.color("-underline")) + + elif name == "strikethrough" and value: + return string_strikethrough(string) + + elif name == "quote" and value: + return "“{text}”".format(text=string) + + elif name == "fgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on=W.color(value), + text=string, + color_off=W.color("resetcolor")) + + elif name == "bgcolor" and value: + return "{color_on}{text}{color_off}".format( + color_on=W.color("," + value), + text=string, + color_off=W.color("resetcolor")) + + return string + + def format_string(formatted_string): + text = formatted_string.text + attributes = formatted_string.attributes + + for key, value in attributes.items(): + text = add_attribute(text, key, value) + return text + + weechat_strings = map(format_string, strings) + return "".join(weechat_strings) diff --git a/weechat-matrix.py b/weechat-matrix.py index a296a52..1d47216 100644 --- a/weechat-matrix.py +++ b/weechat-matrix.py @@ -22,13 +22,8 @@ from functools import wraps # pylint: disable=unused-import from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any) -import webcolors from http_parser.pyparser import HttpParser - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser +from matrix import colors # pylint: disable=import-error @@ -275,14 +270,14 @@ class MatrixMessage: ): # type: (...) -> None # pylint: disable=dangerous-default-value - self.type = message_type # MessageType - self.request = None # HttpRequest - self.response = None # HttpResponse - self.extra_data = extra_data # Dict[str, Any] + self.type = message_type # type: MessageType + self.request = None # type: HttpRequest + self.response = None # type: HttpResponse + self.extra_data = extra_data # type: Dict[str, Any] - self.creation_time = time.time() - self.send_time = None - self.receive_time = None + self.creation_time = time.time() # type: float + self.send_time = None # type: float + self.receive_time = None # type: float if message_type == MessageType.LOGIN: path = ("{api}/login").format(api=MATRIX_API_PATH) @@ -596,833 +591,6 @@ class MatrixServer: "", "server_config_change_cb", self.name, "", "") -FormattedString = namedtuple( - 'FormattedString', - ['text', 'attributes'] -) - -DEFAULT_ATRIBUTES = { - "bold": False, - "italic": False, - "underline": False, - "strikethrough": False, - "quote": False, - "fgcolor": None, - "bgcolor": None -} - - -def color_line_to_weechat(color_string): - # type: (str) -> str - line_colors = { - "0": "white", - "1": "black", - "2": "blue", - "3": "green", - "4": "lightred", - "5": "red", - "6": "magenta", - "7": "brown", - "8": "yellow", - "9": "lightgreen", - "10": "cyan", - "11": "lightcyan", - "12": "lightblue", - "13": "lightmagenta", - "14": "darkgray", - "15": "gray", - "16": "52", - "17": "94", - "18": "100", - "19": "58", - "20": "22", - "21": "29", - "22": "23", - "23": "24", - "24": "17", - "25": "54", - "26": "53", - "27": "89", - "28": "88", - "29": "130", - "30": "142", - "31": "64", - "32": "28", - "33": "35", - "34": "30", - "35": "25", - "36": "18", - "37": "91", - "38": "90", - "39": "125", - "40": "124", - "41": "166", - "42": "184", - "43": "106", - "44": "34", - "45": "49", - "46": "37", - "47": "33", - "48": "19", - "49": "129", - "50": "127", - "51": "161", - "52": "196", - "53": "208", - "54": "226", - "55": "154", - "56": "46", - "57": "86", - "58": "51", - "59": "75", - "60": "21", - "61": "171", - "62": "201", - "63": "198", - "64": "203", - "65": "215", - "66": "227", - "67": "191", - "68": "83", - "69": "122", - "70": "87", - "71": "111", - "72": "63", - "73": "177", - "74": "207", - "75": "205", - "76": "217", - "77": "223", - "78": "229", - "79": "193", - "80": "157", - "81": "158", - "82": "159", - "83": "153", - "84": "147", - "85": "183", - "86": "219", - "87": "212", - "88": "16", - "89": "233", - "90": "235", - "91": "237", - "92": "239", - "93": "241", - "94": "244", - "95": "247", - "96": "250", - "97": "254", - "98": "231", - "99": "default" - } - - assert color_string in line_colors - - return line_colors[color_string] - - -# The functions colour_dist_sq(), colour_to_6cube(), and colour_find_rgb -# are python ports of the same named functions from the tmux -# source, they are under the copyright of Nicholas Marriott, and Avi Halachmi -# under the ISC license. -# More info: https://github.com/tmux/tmux/blob/master/colour.c - -def colour_dist_sq(R, G, B, r, g, b): - # pylint: disable=invalid-name,too-many-arguments - # type: (int, int, int, int, int, int) -> int - return (R - r) * (R - r) + (G - g) * (G - g) + (B - b) * (B - b) - - -def colour_to_6cube(v): - # pylint: disable=invalid-name - # type: (int) -> int - if v < 48: - return 0 - if v < 114: - return 1 - return (v - 35) // 40 - - -def colour_find_rgb(r, g, b): - """Convert an RGB triplet to the xterm(1) 256 colour palette. - - xterm provides a 6x6x6 colour cube (16 - 231) and 24 greys (232 - 255). - We map our RGB colour to the closest in the cube, also work out the - closest grey, and use the nearest of the two. - - Note that the xterm has much lower resolution for darker colours (they - are not evenly spread out), so our 6 levels are not evenly spread: 0x0, - 0x5f (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are - more evenly spread (8, 18, 28 ... 238). - """ - # pylint: disable=invalid-name - # type: (int, int, int) -> int - q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] - - # Map RGB to 6x6x6 cube. - qr = colour_to_6cube(r) - qg = colour_to_6cube(g) - qb = colour_to_6cube(b) - - cr = q2c[qr] - cg = q2c[qg] - cb = q2c[qb] - - # If we have hit the colour exactly, return early. - if (cr == r and cg == g and cb == b): - return 16 + (36 * qr) + (6 * qg) + qb - - # Work out the closest grey (average of RGB). - grey_avg = (r + g + b) // 3 - - if grey_avg > 238: - grey_idx = 23 - else: - grey_idx = (grey_avg - 3) // 10 - - grey = 8 + (10 * grey_idx) - - # Is grey or 6x6x6 colour closest? - d = colour_dist_sq(cr, cg, cb, r, g, b) - - if colour_dist_sq(grey, grey, grey, r, g, b) < d: - idx = 232 + grey_idx - else: - idx = 16 + (36 * qr) + (6 * qg) + qb - - return idx - - -def color_html_to_weechat(color): - # type: (str) -> str - first_16 = { - (0, 0, 0): "black", # 0 - (128, 0, 0): "red", # 1 - (0, 128, 0): "green", # 2 - (128, 128, 0): "brown", # 3 - (0, 0, 128): "blue", # 4 - (128, 0, 128): "magenta", # 5 - (0, 128, 128): "cyan", # 6 - (192, 192, 192): "default", # 7 - (128, 128, 128): "gray", # 8 - (255, 0, 0): "lightred", # 9 - (0, 255, 0): "lightgreen", # 11 - (255, 255, 0): "yellow", # 12 - (0, 0, 255): "lightblue", # 13 - (255, 0, 255): "lightmagenta", # 14 - (0, 255, 255): "lightcyan", # 15 - } - - try: - rgb_color = webcolors.html5_parse_legacy_color(color) - except ValueError: - return None - - if rgb_color in first_16: - return first_16[rgb_color] - - return str(colour_find_rgb(*rgb_color)) - - -def color_weechat_to_html(color): - first_16 = { - "black": "black", # 0 - "red": "maroon", # 1 - "green": "green", # 2 - "brown": "olive", # 3 - "blue": "navy", # 4 - "magenta": "purple", # 5 - "cyan": "teal", # 6 - "default": "silver", # 7 - "gray": "grey", # 8 - "lightred": "red", # 9 - "lightgreen": "lime", # 11 - "yellow": "yellow", # 12 - "lightblue": "fuchsia", # 13 - "lightmagenta": "aqua", # 14 - "lightcyan": "white", # 15 - } - - hex_colors = { - "0": "#000000", - "1": "#800000", - "2": "#008000", - "3": "#808000", - "4": "#000080", - "5": "#800080", - "6": "#008080", - "7": "#c0c0c0", - "8": "#808080", - "9": "#ff0000", - "10": "#00ff00", - "11": "#ffff00", - "12": "#0000ff", - "13": "#ff00ff", - "14": "#00ffff", - "15": "#ffffff", - "16": "#000000", - "17": "#00005f", - "18": "#000087", - "19": "#0000af", - "20": "#0000d7", - "21": "#0000ff", - "22": "#005f00", - "23": "#005f5f", - "24": "#005f87", - "25": "#005faf", - "26": "#005fd7", - "27": "#005fff", - "28": "#008700", - "29": "#00875f", - "30": "#008787", - "31": "#0087af", - "32": "#0087d7", - "33": "#0087ff", - "34": "#00af00", - "35": "#00af5f", - "36": "#00af87", - "37": "#00afaf", - "38": "#00afd7", - "39": "#00afff", - "40": "#00d700", - "41": "#00d75f", - "42": "#00d787", - "43": "#00d7af", - "44": "#00d7d7", - "45": "#00d7ff", - "46": "#00ff00", - "47": "#00ff5f", - "48": "#00ff87", - "49": "#00ffaf", - "50": "#00ffd7", - "51": "#00ffff", - "52": "#5f0000", - "53": "#5f005f", - "54": "#5f0087", - "55": "#5f00af", - "56": "#5f00d7", - "57": "#5f00ff", - "58": "#5f5f00", - "59": "#5f5f5f", - "60": "#5f5f87", - "61": "#5f5faf", - "62": "#5f5fd7", - "63": "#5f5fff", - "64": "#5f8700", - "65": "#5f875f", - "66": "#5f8787", - "67": "#5f87af", - "68": "#5f87d7", - "69": "#5f87ff", - "70": "#5faf00", - "71": "#5faf5f", - "72": "#5faf87", - "73": "#5fafaf", - "74": "#5fafd7", - "75": "#5fafff", - "76": "#5fd700", - "77": "#5fd75f", - "78": "#5fd787", - "79": "#5fd7af", - "80": "#5fd7d7", - "81": "#5fd7ff", - "82": "#5fff00", - "83": "#5fff5f", - "84": "#5fff87", - "85": "#5fffaf", - "86": "#5fffd7", - "87": "#5fffff", - "88": "#870000", - "89": "#87005f", - "90": "#870087", - "91": "#8700af", - "92": "#8700d7", - "93": "#8700ff", - "94": "#875f00", - "95": "#875f5f", - "96": "#875f87", - "97": "#875faf", - "98": "#875fd7", - "99": "#875fff", - "100": "#878700", - "101": "#87875f", - "102": "#878787", - "103": "#8787af", - "104": "#8787d7", - "105": "#8787ff", - "106": "#87af00", - "107": "#87af5f", - "108": "#87af87", - "109": "#87afaf", - "110": "#87afd7", - "111": "#87afff", - "112": "#87d700", - "113": "#87d75f", - "114": "#87d787", - "115": "#87d7af", - "116": "#87d7d7", - "117": "#87d7ff", - "118": "#87ff00", - "119": "#87ff5f", - "120": "#87ff87", - "121": "#87ffaf", - "122": "#87ffd7", - "123": "#87ffff", - "124": "#af0000", - "125": "#af005f", - "126": "#af0087", - "127": "#af00af", - "128": "#af00d7", - "129": "#af00ff", - "130": "#af5f00", - "131": "#af5f5f", - "132": "#af5f87", - "133": "#af5faf", - "134": "#af5fd7", - "135": "#af5fff", - "136": "#af8700", - "137": "#af875f", - "138": "#af8787", - "139": "#af87af", - "140": "#af87d7", - "141": "#af87ff", - "142": "#afaf00", - "143": "#afaf5f", - "144": "#afaf87", - "145": "#afafaf", - "146": "#afafd7", - "147": "#afafff", - "148": "#afd700", - "149": "#afd75f", - "150": "#afd787", - "151": "#afd7af", - "152": "#afd7d7", - "153": "#afd7ff", - "154": "#afff00", - "155": "#afff5f", - "156": "#afff87", - "157": "#afffaf", - "158": "#afffd7", - "159": "#afffff", - "160": "#d70000", - "161": "#d7005f", - "162": "#d70087", - "163": "#d700af", - "164": "#d700d7", - "165": "#d700ff", - "166": "#d75f00", - "167": "#d75f5f", - "168": "#d75f87", - "169": "#d75faf", - "170": "#d75fd7", - "171": "#d75fff", - "172": "#d78700", - "173": "#d7875f", - "174": "#d78787", - "175": "#d787af", - "176": "#d787d7", - "177": "#d787ff", - "178": "#d7af00", - "179": "#d7af5f", - "180": "#d7af87", - "181": "#d7afaf", - "182": "#d7afd7", - "183": "#d7afff", - "184": "#d7d700", - "185": "#d7d75f", - "186": "#d7d787", - "187": "#d7d7af", - "188": "#d7d7d7", - "189": "#d7d7ff", - "190": "#d7ff00", - "191": "#d7ff5f", - "192": "#d7ff87", - "193": "#d7ffaf", - "194": "#d7ffd7", - "195": "#d7ffff", - "196": "#ff0000", - "197": "#ff005f", - "198": "#ff0087", - "199": "#ff00af", - "200": "#ff00d7", - "201": "#ff00ff", - "202": "#ff5f00", - "203": "#ff5f5f", - "204": "#ff5f87", - "205": "#ff5faf", - "206": "#ff5fd7", - "207": "#ff5fff", - "208": "#ff8700", - "209": "#ff875f", - "210": "#ff8787", - "211": "#ff87af", - "212": "#ff87d7", - "213": "#ff87ff", - "214": "#ffaf00", - "215": "#ffaf5f", - "216": "#ffaf87", - "217": "#ffafaf", - "218": "#ffafd7", - "219": "#ffafff", - "220": "#ffd700", - "221": "#ffd75f", - "222": "#ffd787", - "223": "#ffd7af", - "224": "#ffd7d7", - "225": "#ffd7ff", - "226": "#ffff00", - "227": "#ffff5f", - "228": "#ffff87", - "229": "#ffffaf", - "230": "#ffffd7", - "231": "#ffffff", - "232": "#080808", - "233": "#121212", - "234": "#1c1c1c", - "235": "#262626", - "236": "#303030", - "237": "#3a3a3a", - "238": "#444444", - "239": "#4e4e4e", - "240": "#585858", - "241": "#626262", - "242": "#6c6c6c", - "243": "#767676", - "244": "#808080", - "245": "#8a8a8a", - "246": "#949494", - "247": "#9e9e9e", - "248": "#a8a8a8", - "249": "#b2b2b2", - "250": "#bcbcbc", - "251": "#c6c6c6", - "252": "#d0d0d0", - "253": "#dadada", - "254": "#e4e4e4", - "255": "#eeeeee" - } - - if color in first_16: - return first_16[color] - - hex_color = hex_colors[color] - - try: - return webcolors.hex_to_name(hex_color) - except ValueError: - return hex_color - - -# TODO reverse video -def parse_input_line(line): - """Parses the weechat input line and produces formatted strings that can be - later converted to HTML or to a string for weechat's print functions - """ - # type: (str) -> List[FormattedString] - text = "" # type: str - substrings = [] # type: List[FormattedString] - attributes = DEFAULT_ATRIBUTES.copy() - - i = 0 - while i < len(line): - # Bold - if line[i] == "\x02": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["bold"] = not attributes["bold"] - i = i + 1 - - # Color - elif line[i] == "\x03": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - i = i + 1 - - # check if it's a valid color, add it to the attributes - if line[i].isdigit(): - color_string = line[i] - i = i + 1 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["fgcolor"] = color_line_to_weechat(color_string) - else: - attributes["fgcolor"] = None - - # check if we have a background color - if line[i] == "," and line[i+1].isdigit(): - color_string = line[i+1] - i = i + 2 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["bgcolor"] = color_line_to_weechat(color_string) - else: - attributes["bgcolor"] = None - # Reset - elif line[i] == "\x0F": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - # Reset all the attributes - attributes = DEFAULT_ATRIBUTES.copy() - i = i + 1 - # Italic - elif line[i] == "\x1D": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["italic"] = not attributes["italic"] - i = i + 1 - - # Underline - elif line[i] == "\x1F": - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - attributes["underline"] = not attributes["underline"] - i = i + 1 - - # Normal text - else: - text = text + line[i] - i = i + 1 - - substrings.append(FormattedString(text, attributes)) - return substrings - - -def formatted(strings): - for string in strings: - if string.attributes != DEFAULT_ATRIBUTES: - return True - return False - - -def formatted_to_weechat(strings): - # type: (List[FormattedString]) -> str - # TODO BG COLOR - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on=W.color("bold"), - text=string, - bold_off=W.color("-bold")) - - elif name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on=W.color("italic"), - text=string, - italic_off=W.color("-italic")) - - elif name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on=W.color("underline"), - text=string, - underline_off=W.color("-underline")) - - elif name == "strikethrough" and value: - return string_strikethrough(string) - - elif name == "quote" and value: - return "“{text}”".format(text=string) - - elif name == "fgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on=W.color(value), - text=string, - color_off=W.color("resetcolor")) - - elif name == "bgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on=W.color("," + value), - text=string, - color_off=W.color("resetcolor")) - - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - return text - - weechat_strings = map(format_string, strings) - return "".join(weechat_strings) - - -def formatted_to_html(strings): - # type: (List[FormattedString]) -> str - # TODO BG COLOR - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on="", - text=string, - bold_off="") - elif name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on="", - text=string, - italic_off="") - elif name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on="", - text=string, - underline_off="") - elif name == "strikethrough" and value: - return "{strike_on}{text}{strike_off}".format( - strike_on="
", - text=string, - quote_off="") - elif name == "fgcolor" and value: - return "{color_on}{text}{color_off}".format( - color_on="".format( - color=color_weechat_to_html(value) - ), - text=string, - color_off="") - - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - return text - - html_string = map(format_string, strings) - return "".join(html_string) - - -# TODO do we want at least some formating using unicode -# (strikethrough, quotes)? -def formatted_to_plain(strings): - # type: (List[FormattedString]) -> str - def strip_atribute(string, _, __): - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = strip_atribute(text, key, value) - return text - - plain_string = map(format_string, strings) - return "".join(plain_string) - - -class MatrixHtmlParser(HTMLParser): - # TODO bg color - # TODO bullets - def __init__(self): - HTMLParser.__init__(self) - self.text = "" # type: str - self.substrings = [] # type: List[FormattedString] - self.attributes = DEFAULT_ATRIBUTES.copy() - - def _toggle_attribute(self, attribute): - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes[attribute] = not self.attributes[attribute] - - def handle_starttag(self, tag, attrs): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "font": - for key, value in attrs: - if key == "color": - color = color_html_to_weechat(value) - - if not color: - continue - - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes["fgcolor"] = color - else: - # W.prnt("", "Unhandled tag {t}".format(t=tag)) - pass - - def handle_endtag(self, tag): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "font": - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - self.text = "" - self.attributes["fgcolor"] = None - else: - pass - - def handle_data(self, data): - self.text = self.text + data - - def get_substrings(self): - if self.text: - self.substrings.append( - FormattedString(self.text, self.attributes.copy()) - ) - - return self.substrings - - -def html_to_formatted(html): - parser = MatrixHtmlParser() - parser.feed(html) - return parser.get_substrings() - - def wrap_socket(server, file_descriptor): # type: (MatrixServer, int) -> socket.socket sock = None # type: socket.socket @@ -1501,10 +669,10 @@ def handle_http_response(server, message): reason = ("." if not response or not response["error"] else ": {r}.".format(r=response["error"])) - message = ("{prefix}Login error{reason}").format( + error_message = ("{prefix}Login error{reason}").format( prefix=W.prefix("error"), reason=reason) - server_buffer_prnt(server, message) + server_buffer_prnt(server, error_message) W.unhook(server.timer_hook) server.timer_hook = None @@ -1516,16 +684,16 @@ def handle_http_response(server, message): reason = ("." if not response or not response["error"] else ": {r}.".format(r=response["error"])) - message = ("{prefix}Can't set state{reason}").format( + error_message = ("{prefix}Can't set state{reason}").format( prefix=W.prefix("network"), reason=reason) - server_buffer_prnt(server, message) + server_buffer_prnt(server, error_message) else: - message = ("{prefix}Unhandled 403 error, please inform the " - "developers about this: {error}").format( - prefix=W.prefix("error"), - error=message.response.body) - server_buffer_prnt(server, message) + error_message = ("{prefix}Unhandled 403 error, please inform the " + "developers about this: {error}").format( + prefix=W.prefix("error"), + error=message.response.body) + server_buffer_prnt(server, error_message) else: server_buffer_prnt( @@ -1541,19 +709,19 @@ def handle_http_response(server, message): creation_date = datetime.datetime.fromtimestamp(message.creation_time) done_time = time.time() - message = ("Message of type {t} created at {c}." - "\nMessage lifetime information:" - "\n Send delay: {s} ms" - "\n Receive delay: {r} ms" - "\n Handling time: {h} ms" - "\n Total time: {total} ms").format( - t=message.type, - c=creation_date, - s=(message.send_time - message.creation_time) * 1000, - r=(message.receive_time - message.send_time) * 1000, - h=(done_time - message.receive_time) * 1000, - total=(done_time - message.creation_time) * 1000,) - prnt_debug(DebugType.TIMING, server, message) + info_message = ("Message of type {t} created at {c}." + "\nMessage lifetime information:" + "\n Send delay: {s} ms" + "\n Receive delay: {r} ms" + "\n Handling time: {h} ms" + "\n Total time: {total} ms").format( + t=message.type, + c=creation_date, + s=(message.send_time - message.creation_time) * 1000, + r=(message.receive_time - message.send_time) * 1000, + h=(done_time - message.receive_time) * 1000, + total=(done_time - message.creation_time) * 1000,) + prnt_debug(DebugType.TIMING, server, info_message) return @@ -1709,9 +877,9 @@ def matrix_handle_room_text_message(server, room_id, event, old=False): if 'format' in event['content'] and 'formatted_body' in event['content']: if event['content']['format'] == "org.matrix.custom.html": - formatted_data = html_to_formatted( + formatted_data = colors.html_to_formatted( event['content']['formatted_body']) - msg = formatted_to_weechat(formatted_data) + msg = colors.formatted_to_weechat(W, formatted_data) if event['sender'] in room.users: user = room.users[event['sender']] @@ -2626,17 +1794,20 @@ def room_input_cb(server_name, buffer, input_data): if room.encrypted: return W.WEECHAT_RC_OK - formatted_data = parse_input_line(input_data) + formatted_data = colors.parse_input_line(input_data) - body = {"msgtype": "m.text", "body": formatted_to_plain(formatted_data)} + body = { + "msgtype": "m.text", + "body": colors.formatted_to_plain(formatted_data) + } - if formatted(formatted_data): + if colors.formatted(formatted_data): body["format"] = "org.matrix.custom.html" - body["formatted_body"] = formatted_to_html(formatted_data) + body["formatted_body"] = colors.formatted_to_html(formatted_data) extra_data = { "author": server.user, - "message": formatted_to_weechat(formatted_data), + "message": colors.formatted_to_weechat(W, formatted_data), "room_id": room_id }