diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index ad52c9d7..77dc0ff3 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -1,33 +1,23 @@ from __future__ import annotations + import html import re -import struct -from dataclasses import dataclass -from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( "TextDecoration", + "HtmlDecoration", + "MarkdownDecoration", "html_decoration", "markdown_decoration", - "add_surrogate", - "remove_surrogate", ) -@dataclass -class TextDecoration: - link: str - bold: str - italic: str - code: str - pre: str - underline: str - strikethrough: str - quote: Callable[[AnyStr], AnyStr] - +class TextDecoration(ABC): def apply_entity(self, entity: MessageEntity, text: str) -> str: """ Apply single entity to text @@ -36,24 +26,28 @@ class TextDecoration: :param text: :return: """ - if entity.type in ( - "bold", - "italic", - "code", - "pre", - "underline", - "strikethrough", - ): - return getattr(self, entity.type).format(value=text) - elif entity.type == "text_mention": - return self.link.format(value=text, link=f"tg://user?id={entity.user.id}") - elif entity.type == "text_link": - return self.link.format(value=text, link=entity.url) - elif entity.type == "url": + if entity.type in {"bot_command", "url", "mention", "phone_number"}: + # This entities should not be changed return text + if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}: + return cast(str, getattr(self, entity.type)(value=text)) + if entity.type == "pre": + return ( + self.pre_language(value=text, language=entity.language) + if entity.language + else self.pre(value=text) + ) + if entity.type == "text_mention": + from aiogram.types import User + + user = cast(User, entity.user) + return self.link(value=text, link=f"tg://user?id={user.id}") + if entity.type == "text_link": + return self.link(value=text, link=cast(str, entity.url)) + return self.quote(text) - def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str: + def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str: """ Unparse message entities @@ -61,22 +55,22 @@ class TextDecoration: :param entities: Array of MessageEntities :return: """ - text = add_surrogate(text) result = "".join( self._unparse_entities( text, sorted(entities, key=lambda item: item.offset) if entities else [] ) ) - return remove_surrogate(result) + return result def _unparse_entities( self, text: str, - entities: Iterable[MessageEntity], + entities: List[MessageEntity], offset: Optional[int] = None, length: Optional[int] = None, ) -> Generator[str, None, None]: - offset = offset or 0 + if offset is None: + offset = 0 length = length or len(text) for index, entity in enumerate(entities): @@ -88,7 +82,7 @@ class TextDecoration: offset = entity.offset + entity.length sub_entities = list( - filter(lambda e: e.offset < offset, entities[index + 1 :]) + filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]) ) yield self.apply_entity( entity, @@ -102,42 +96,102 @@ class TextDecoration: if offset < length: yield self.quote(text[offset:length]) + @abstractmethod + def link(self, value: str, link: str) -> str: # pragma: no cover + pass -html_decoration = TextDecoration( - link='{value}', - bold="{value}", - italic="{value}", - code="{value}", - pre="
{value}
", - underline="{value}", - strikethrough="{value}", - quote=html.escape, -) + @abstractmethod + def bold(self, value: str) -> str: # pragma: no cover + pass -MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])") + @abstractmethod + def italic(self, value: str) -> str: # pragma: no cover + pass -markdown_decoration = TextDecoration( - link="[{value}]({link})", - bold="*{value}*", - italic="_{value}_\r", - code="`{value}`", - pre="```{value}```", - underline="__{value}__", - strikethrough="~{value}~", - quote=lambda text: re.sub( - pattern=MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=text - ), -) + @abstractmethod + def code(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def pre(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def pre_language(self, value: str, language: str) -> str: # pragma: no cover + pass + + @abstractmethod + def underline(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def strikethrough(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def quote(self, value: str) -> str: # pragma: no cover + pass -def add_surrogate(text: str) -> str: - return "".join( - "".join(chr(d) for d in struct.unpack(" str: + return f'{value}' + + def bold(self, value: str) -> str: + return f"{value}" + + def italic(self, value: str) -> str: + return f"{value}" + + def code(self, value: str) -> str: + return f"{value}" + + def pre(self, value: str) -> str: + return f"
{value}
" + + def pre_language(self, value: str, language: str) -> str: + return f'
{value}
' + + def underline(self, value: str) -> str: + return f"{value}" + + def strikethrough(self, value: str) -> str: + return f"{value}" + + def quote(self, value: str) -> str: + return html.escape(value) -def remove_surrogate(text: str) -> str: - return text.encode("utf-16", "surrogatepass").decode("utf-16") +class MarkdownDecoration(TextDecoration): + MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])") + + def link(self, value: str, link: str) -> str: + return f"[{value}]({link})" + + def bold(self, value: str) -> str: + return f"*{value}*" + + def italic(self, value: str) -> str: + return f"_{value}_\r" + + def code(self, value: str) -> str: + return f"`{value}`" + + def pre(self, value: str) -> str: + return f"```{value}```" + + def pre_language(self, value: str, language: str) -> str: + return f"```{language}\n{value}\n```" + + def underline(self, value: str) -> str: + return f"__{value}__" + + def strikethrough(self, value: str) -> str: + return f"~{value}~" + + def quote(self, value: str) -> str: + return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value) + + +html_decoration = HtmlDecoration() +markdown_decoration = MarkdownDecoration()