2020-01-01 16:39:31 +02:00
|
|
|
from __future__ import annotations
|
2020-06-08 20:42:38 +03:00
|
|
|
|
2020-01-01 16:39:31 +02:00
|
|
|
import html
|
|
|
|
|
import re
|
2020-06-08 20:42:38 +03:00
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
if TYPE_CHECKING: # pragma: no cover
|
2020-01-01 16:39:31 +02:00
|
|
|
from aiogram.types import MessageEntity
|
|
|
|
|
|
|
|
|
|
__all__ = (
|
2020-09-13 22:14:33 +03:00
|
|
|
'HtmlDecoration',
|
|
|
|
|
'MarkdownDecoration',
|
|
|
|
|
'TextDecoration',
|
|
|
|
|
'html_decoration',
|
|
|
|
|
'markdown_decoration',
|
2020-01-01 16:39:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
class TextDecoration(ABC):
|
2020-01-01 16:39:31 +02:00
|
|
|
def apply_entity(self, entity: MessageEntity, text: str) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Apply single entity to text
|
|
|
|
|
|
|
|
|
|
:param entity:
|
|
|
|
|
:param text:
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
2020-06-08 20:42:38 +03:00
|
|
|
if entity.type in {"bot_command", "url", "mention", "phone_number"}:
|
|
|
|
|
# This entities should not be changed
|
2020-01-01 16:39:31 +02:00
|
|
|
return text
|
2020-06-08 20:42:38 +03:00
|
|
|
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))
|
|
|
|
|
|
2020-01-01 16:39:31 +02:00
|
|
|
return self.quote(text)
|
|
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:
|
2020-01-01 16:39:31 +02:00
|
|
|
"""
|
|
|
|
|
Unparse message entities
|
|
|
|
|
|
|
|
|
|
:param text: raw text
|
|
|
|
|
:param entities: Array of MessageEntities
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
2021-03-21 02:51:32 +09:00
|
|
|
return "".join(
|
2020-01-01 16:39:31 +02:00
|
|
|
self._unparse_entities(
|
2020-09-04 18:08:15 +03:00
|
|
|
self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else []
|
2020-01-01 16:39:31 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _unparse_entities(
|
|
|
|
|
self,
|
2020-09-04 18:08:15 +03:00
|
|
|
text: bytes,
|
2020-06-08 20:42:38 +03:00
|
|
|
entities: List[MessageEntity],
|
2020-01-01 16:39:31 +02:00
|
|
|
offset: Optional[int] = None,
|
|
|
|
|
length: Optional[int] = None,
|
|
|
|
|
) -> Generator[str, None, None]:
|
2020-06-08 20:42:38 +03:00
|
|
|
if offset is None:
|
|
|
|
|
offset = 0
|
2020-01-01 16:39:31 +02:00
|
|
|
length = length or len(text)
|
|
|
|
|
|
|
|
|
|
for index, entity in enumerate(entities):
|
2020-09-04 18:08:15 +03:00
|
|
|
if entity.offset * 2 < offset:
|
2020-01-01 16:39:31 +02:00
|
|
|
continue
|
2020-09-04 18:08:15 +03:00
|
|
|
if entity.offset * 2 > offset:
|
|
|
|
|
yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2]))
|
|
|
|
|
start = entity.offset * 2
|
|
|
|
|
offset = entity.offset * 2 + entity.length * 2
|
2020-01-01 16:39:31 +02:00
|
|
|
|
|
|
|
|
sub_entities = list(
|
2020-09-04 18:08:15 +03:00
|
|
|
filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :])
|
2020-01-01 16:39:31 +02:00
|
|
|
)
|
|
|
|
|
yield self.apply_entity(
|
|
|
|
|
entity,
|
|
|
|
|
"".join(
|
|
|
|
|
self._unparse_entities(
|
|
|
|
|
text, sub_entities, offset=start, length=offset
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if offset < length:
|
2020-09-04 18:08:15 +03:00
|
|
|
yield self.quote(self._remove_surrogates(text[offset:length]))
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _add_surrogates(text: str):
|
|
|
|
|
return text.encode('utf-16-le')
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _remove_surrogates(text: bytes):
|
|
|
|
|
return text.decode('utf-16-le')
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
@abstractmethod
|
|
|
|
|
def link(self, value: str, link: str) -> str: # pragma: no cover
|
|
|
|
|
pass
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
@abstractmethod
|
|
|
|
|
def bold(self, value: str) -> str: # pragma: no cover
|
|
|
|
|
pass
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
@abstractmethod
|
|
|
|
|
def italic(self, value: str) -> str: # pragma: no cover
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HtmlDecoration(TextDecoration):
|
|
|
|
|
def link(self, value: str, link: str) -> str:
|
|
|
|
|
return f'<a href="{link}">{value}</a>'
|
|
|
|
|
|
|
|
|
|
def bold(self, value: str) -> str:
|
|
|
|
|
return f"<b>{value}</b>"
|
|
|
|
|
|
|
|
|
|
def italic(self, value: str) -> str:
|
|
|
|
|
return f"<i>{value}</i>"
|
|
|
|
|
|
|
|
|
|
def code(self, value: str) -> str:
|
|
|
|
|
return f"<code>{value}</code>"
|
|
|
|
|
|
|
|
|
|
def pre(self, value: str) -> str:
|
|
|
|
|
return f"<pre>{value}</pre>"
|
|
|
|
|
|
|
|
|
|
def pre_language(self, value: str, language: str) -> str:
|
|
|
|
|
return f'<pre><code class="language-{language}">{value}</code></pre>'
|
|
|
|
|
|
|
|
|
|
def underline(self, value: str) -> str:
|
|
|
|
|
return f"<u>{value}</u>"
|
|
|
|
|
|
|
|
|
|
def strikethrough(self, value: str) -> str:
|
|
|
|
|
return f"<s>{value}</s>"
|
|
|
|
|
|
|
|
|
|
def quote(self, value: str) -> str:
|
2020-08-30 05:06:48 +07:00
|
|
|
return html.escape(value, quote=False)
|
2020-06-08 20:42:38 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MarkdownDecoration(TextDecoration):
|
2020-06-27 16:17:38 +03:00
|
|
|
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
|
2020-06-08 20:42:38 +03:00
|
|
|
|
|
|
|
|
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:
|
2020-09-13 22:56:32 +03:00
|
|
|
return f"_\r{value}_\r"
|
2020-06-08 20:42:38 +03:00
|
|
|
|
|
|
|
|
def code(self, value: str) -> str:
|
|
|
|
|
return f"`{value}`"
|
|
|
|
|
|
|
|
|
|
def pre(self, value: str) -> str:
|
2021-06-04 18:53:59 +03:00
|
|
|
return f"```\n{value}\n```"
|
2020-06-08 20:42:38 +03:00
|
|
|
|
|
|
|
|
def pre_language(self, value: str, language: str) -> str:
|
|
|
|
|
return f"```{language}\n{value}\n```"
|
|
|
|
|
|
|
|
|
|
def underline(self, value: str) -> str:
|
2020-09-13 22:56:32 +03:00
|
|
|
return f"__\r{value}__\r"
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
def strikethrough(self, value: str) -> str:
|
|
|
|
|
return f"~{value}~"
|
2020-01-01 16:39:31 +02:00
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
def quote(self, value: str) -> str:
|
|
|
|
|
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
|
2020-01-01 16:39:31 +02:00
|
|
|
|
|
|
|
|
|
2020-06-08 20:42:38 +03:00
|
|
|
html_decoration = HtmlDecoration()
|
|
|
|
|
markdown_decoration = MarkdownDecoration()
|