font color → span mx color for outgoing HTML

Also remove HTML_PROCESSOR `filter_inline` and `from_markdown_inline`
methods. `filter` and `from_markdown` now take an `inline` argument.
This commit is contained in:
miruka 2019-12-20 15:36:01 -04:00
parent 335d931b0a
commit 44e5de02f8
4 changed files with 58 additions and 35 deletions

View File

@ -92,14 +92,16 @@ class HTMLProcessor:
- Wrap text lines starting with a `>` in `<span>` with a `quote` class. - Wrap text lines starting with a `>` in `<span>` with a `quote` class.
This allows them to be styled appropriately from QML. This allows them to be styled appropriately from QML.
Some methods have `inline` counterparts, which return text appropriate Some methods take an `inline` argument, which return text appropriate
for UI elements restricted to display a single line, e.g. the room for UI elements restricted to display a single line, e.g. the room
last message subtitles in QML or notifications. last message subtitles in QML or notifications.
In inline filtered HTML, block tags are stripped or substituted and In inline filtered HTML, block tags are stripped or substituted and
newlines are turned into symbols (U+23CE). newlines are turned into symbols (U+23CE).
""" """
inline_tags = {"font", "a", "sup", "sub", "b", "i", "s", "u", "code"} inline_tags = {
"span", "font", "a", "sup", "sub", "b", "i", "s", "u", "code",
}
block_tags = { block_tags = {
"h1", "h2", "h3", "h4", "h5", "h6","blockquote", "h1", "h2", "h3", "h4", "h5", "h6","blockquote",
@ -126,8 +128,16 @@ class HTMLProcessor:
def __init__(self) -> None: def __init__(self) -> None:
self._sanitizer = Sanitizer(self.sanitize_settings()) self._sanitizers = {
(False, False): Sanitizer(self.sanitize_settings(False, False)),
(True, False): Sanitizer(self.sanitize_settings(True, False)),
(False, True): Sanitizer(self.sanitize_settings(False, True)),
(True, True): Sanitizer(self.sanitize_settings(True, True)),
}
self._inline_sanitizer = Sanitizer(self.sanitize_settings(inline=True)) self._inline_sanitizer = Sanitizer(self.sanitize_settings(inline=True))
self._inline_outgoing_sanitizer = \
Sanitizer(self.sanitize_settings(inline=True))
# The whitespace remover doesn't take <pre> into account # The whitespace remover doesn't take <pre> into account
sanitizer.normalize_overall_whitespace = lambda html, *args, **kw: html sanitizer.normalize_overall_whitespace = lambda html, *args, **kw: html
@ -149,44 +159,36 @@ class HTMLProcessor:
] ]
def from_markdown(self, text: str, outgoing: bool = False) -> str: def from_markdown(
self, text: str, inline: bool = False, outgoing: bool = False,
) -> str:
"""Return filtered HTML from Markdown text.""" """Return filtered HTML from Markdown text."""
return self.filter(self._markdown_to_html(text), outgoing) return self.filter(self._markdown_to_html(text), inline, outgoing)
def from_markdown_inline(self, text: str, outgoing: bool = False) -> str: def filter(
"""Return single-line filtered HTML from Markdown text.""" self, html: str, inline: bool = False, outgoing: bool = False,
) -> str:
"""Filter and return HTML."""
return self.filter_inline(self._markdown_to_html(text), outgoing) html = self._sanitizers[inline, outgoing].sanitize(html).rstrip("\n")
def filter_inline(self, html: str, outgoing: bool = False) -> str:
"""Filter and return HTML with block tags stripped or substituted."""
html = self._inline_sanitizer.sanitize(html)
if outgoing: if outgoing:
return html return html
# Client-side modifications # Client-side modifications
return self.inline_quote_regex.sub( if inline:
r'\1<span class="quote">\2</span>', html, return self.inline_quote_regex.sub(
) r'\1<span class="quote">\2</span>', html,
)
def filter(self, html: str, outgoing: bool = False) -> str:
"""Filter and return HTML."""
html = self._sanitizer.sanitize(html).rstrip("\n")
if outgoing:
return html
return self.quote_regex.sub(r'\1<span class="quote">\2</span>\3', html) return self.quote_regex.sub(r'\1<span class="quote">\2</span>\3', html)
def sanitize_settings(self, inline: bool = False) -> dict: def sanitize_settings(
self, inline: bool = False, outgoing: bool = False,
) -> dict:
"""Return an html_sanitizer configuration.""" """Return an html_sanitizer configuration."""
# https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes # https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
@ -203,6 +205,7 @@ class HTMLProcessor:
attributes = {**inlines_attributes, **{ attributes = {**inlines_attributes, **{
"ol": {"start"}, "ol": {"start"},
"hr": {"width"}, "hr": {"width"},
"span": {"data-mx-color"},
}} }}
return { return {
@ -231,18 +234,22 @@ class HTMLProcessor:
sanitizer.tag_replacer("div", "p"), sanitizer.tag_replacer("div", "p"),
sanitizer.tag_replacer("caption", "p"), sanitizer.tag_replacer("caption", "p"),
sanitizer.target_blank_noopener, sanitizer.target_blank_noopener,
self._process_span_font,
self._span_color_to_font if not outgoing else lambda el: el,
self._img_to_a, self._img_to_a,
self._remove_extra_newlines, self._remove_extra_newlines,
self._newlines_to_return_symbol if inline else lambda el: el, self._newlines_to_return_symbol if inline else lambda el: el,
], ],
"element_postprocessors": [], "element_postprocessors": [
self._font_color_to_span if outgoing else lambda el: el,
],
"is_mergeable": lambda e1, e2: e1.attrib == e2.attrib, "is_mergeable": lambda e1, e2: e1.attrib == e2.attrib,
} }
@staticmethod @staticmethod
def _process_span_font(el: HtmlElement) -> HtmlElement: def _span_color_to_font(el: HtmlElement) -> HtmlElement:
"""Convert HTML `<span data-mx-color=...` to `<font color=...>`.""" """Convert HTML `<span data-mx-color=...` to `<font color=...>`."""
if el.tag not in ("span", "font"): if el.tag not in ("span", "font"):
@ -256,6 +263,21 @@ class HTMLProcessor:
return el return el
@staticmethod
def _font_color_to_span(el: HtmlElement) -> HtmlElement:
"""Convert HTML `<font color=...>` to `<span data-mx-color=...`."""
if el.tag not in ("span", "font"):
return el
color = el.attrib.pop("color", None)
if color:
el.tag = "span"
el.attrib["data-mx-color"] = color
return el
@staticmethod @staticmethod
def _img_to_a(el: HtmlElement) -> HtmlElement: def _img_to_a(el: HtmlElement) -> HtmlElement:
"""Linkify images by wrapping `<img>` tags in `<a>`.""" """Linkify images by wrapping `<img>` tags in `<a>`."""

View File

@ -244,8 +244,8 @@ class MatrixClient(nio.AsyncClient):
event_type = nio.RoomMessageEmote event_type = nio.RoomMessageEmote
text = text[len("/me "): ] text = text[len("/me "): ]
content = {"body": text, "msgtype": "m.emote"} content = {"body": text, "msgtype": "m.emote"}
to_html = HTML.from_markdown_inline(text, outgoing=True) to_html = HTML.from_markdown(text, inline=True, outgoing=True)
echo_body = HTML.from_markdown_inline(text) echo_body = HTML.from_markdown(text, inline=True)
else: else:
event_type = nio.RoomMessageText event_type = nio.RoomMessageText
content = {"body": text, "msgtype": "m.text"} content = {"body": text, "msgtype": "m.text"}
@ -965,7 +965,7 @@ class MatrixClient(nio.AsyncClient):
display_name = room.display_name or "", display_name = room.display_name or "",
avatar_url = room.gen_avatar_url or "", avatar_url = room.gen_avatar_url or "",
plain_topic = room.topic or "", plain_topic = room.topic or "",
topic = HTML.filter_inline(room.topic or ""), topic = HTML.filter(room.topic or "", inline=True),
inviter_id = inviter, inviter_id = inviter,
inviter_name = room.user_name(inviter) if inviter else "", inviter_name = room.user_name(inviter) if inviter else "",
inviter_avatar = inviter_avatar =

View File

@ -239,7 +239,8 @@ class Event(ModelItem):
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self.inline_content: if not self.inline_content:
self.inline_content = HTML_PROCESSOR.filter_inline(self.content) self.inline_content = \
HTML_PROCESSOR.filter(self.content, inline=True)
def __lt__(self, other: "Event") -> bool: def __lt__(self, other: "Event") -> bool:

View File

@ -340,7 +340,7 @@ class NioCallbacks:
async def onRoomTopicEvent(self, room, ev) -> None: async def onRoomTopicEvent(self, room, ev) -> None:
if ev.topic: if ev.topic:
topic = HTML_PROCESSOR.filter_inline(ev.topic) topic = HTML_PROCESSOR.filter(ev.topic, inline=True)
co = f"%1 changed the room's topic to \"{topic}\"" co = f"%1 changed the room's topic to \"{topic}\""
else: else:
co = "%1 removed the room's topic" co = "%1 removed the room's topic"