126 lines
3.6 KiB
Python
126 lines
3.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
from html import escape
|
||
|
|
|
||
|
|
_HTML_TAGS = [
|
||
|
|
("<b>", "</b>"),
|
||
|
|
("<i>", "</i>"),
|
||
|
|
("<s>", "</s>"),
|
||
|
|
("<u>", "</u>"),
|
||
|
|
("<code>", "</code>"),
|
||
|
|
("<pre>", "</pre>"),
|
||
|
|
("<tg-spoiler>", "</tg-spoiler>"),
|
||
|
|
("<blockquote>", "</blockquote>"),
|
||
|
|
("<tg-emoji", "</tg-emoji>"),
|
||
|
|
]
|
||
|
|
|
||
|
|
_RE_MARKDOWN_TO_HTML = [
|
||
|
|
(re.compile(r"\*\*\*(.+?)\*\*\*"), r"<b><i>\1</i></b>"),
|
||
|
|
(re.compile(r"\*\*_(.+?)_\*\*"), r"<b><i>\1</i></b>"),
|
||
|
|
(re.compile(r"_\*\*(.+?)\*\*_"), r"<i><b>\1</b></i>"),
|
||
|
|
(re.compile(r"\*\*(.+?)\*\*"), r"<b>\1</b>"),
|
||
|
|
(re.compile(r"__(.+?)__"), r"<u>\1</u>"),
|
||
|
|
(re.compile(r"~~(.+?)~~"), r"<s>\1</s>"),
|
||
|
|
(re.compile(r"\|\|(.+?)\|\|"), r"<tg-spoiler>\1</tg-spoiler>"),
|
||
|
|
(re.compile(r"(?<!\w)_(.+?)_(?!\w)"), r"<i>\1</i>"),
|
||
|
|
(re.compile(r"(?<!\w)\*(.+?)\*(?!\w)"), r"<i>\1</i>"),
|
||
|
|
(re.compile(r"(?<!\w)`([^`]+)`(?!\w)"), r"<code>\1</code>"),
|
||
|
|
(re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'<a href="\2">\1</a>'),
|
||
|
|
]
|
||
|
|
|
||
|
|
_BULLET_LINE_RE = re.compile(r"^(\s*)\*\s", re.MULTILINE)
|
||
|
|
_BLOCKQUOTE_RE = re.compile(r"^> (.+)$", re.MULTILINE)
|
||
|
|
|
||
|
|
_CODE_BLOCK_RE = re.compile(r"```(\w+)?\s*\n?(.+?)```", re.DOTALL)
|
||
|
|
|
||
|
|
|
||
|
|
def markdown_to_telegram_html(md_text: str) -> str:
|
||
|
|
text = md_text
|
||
|
|
|
||
|
|
code_blocks = []
|
||
|
|
|
||
|
|
def _save_code_block(m: re.Match) -> str:
|
||
|
|
lang = m.group(1) or ""
|
||
|
|
code = m.group(2)
|
||
|
|
code_blocks.append(f"<pre><code class=\"language-{lang}\">{escape(code)}</code></pre>")
|
||
|
|
return f"\x00CODEBLOCK{len(code_blocks) - 1}\x00"
|
||
|
|
|
||
|
|
text = _CODE_BLOCK_RE.sub(_save_code_block, text)
|
||
|
|
|
||
|
|
text = escape(text)
|
||
|
|
|
||
|
|
for pattern, replacement in _RE_MARKDOWN_TO_HTML:
|
||
|
|
try:
|
||
|
|
text = pattern.sub(replacement, text)
|
||
|
|
except re.error:
|
||
|
|
pass
|
||
|
|
|
||
|
|
text = _BULLET_LINE_RE.sub(r"\1• ", text)
|
||
|
|
text = _BLOCKQUOTE_RE.sub(r"<blockquote>\1</blockquote>", text)
|
||
|
|
|
||
|
|
text = text.replace("*", "").replace("_", "")
|
||
|
|
|
||
|
|
for i, block in enumerate(code_blocks):
|
||
|
|
text = text.replace(f"\x00CODEBLOCK{i}\x00", block)
|
||
|
|
|
||
|
|
return text.strip()
|
||
|
|
|
||
|
|
|
||
|
|
def split_telegram_html_chunks(html: str, limit: int = 4096) -> list[str]:
|
||
|
|
if len(html) <= limit:
|
||
|
|
return [html]
|
||
|
|
|
||
|
|
chunks: list[str] = []
|
||
|
|
tag_stack: list[str] = []
|
||
|
|
start = 0
|
||
|
|
|
||
|
|
i = 0
|
||
|
|
while i < len(html):
|
||
|
|
if i - start >= limit:
|
||
|
|
boundary = _find_safe_boundary(html, i, tag_stack)
|
||
|
|
chunk = html[start:boundary]
|
||
|
|
chunk = _close_open_tags(chunk, tag_stack)
|
||
|
|
chunks.append(chunk)
|
||
|
|
start = _skip_whitespace(html, boundary)
|
||
|
|
i = start
|
||
|
|
continue
|
||
|
|
i += 1
|
||
|
|
|
||
|
|
if start < len(html):
|
||
|
|
chunk = html[start:]
|
||
|
|
chunk = _close_open_tags(chunk, tag_stack)
|
||
|
|
chunks.append(chunk)
|
||
|
|
|
||
|
|
return chunks
|
||
|
|
|
||
|
|
|
||
|
|
def _find_safe_boundary(html: str, pos: int, tag_stack: list[str]) -> int:
|
||
|
|
end = min(pos, len(html))
|
||
|
|
|
||
|
|
for i in range(end - 1, max(pos - 200, 0), -1):
|
||
|
|
if html[i] == "\n":
|
||
|
|
return i
|
||
|
|
|
||
|
|
return end
|
||
|
|
|
||
|
|
|
||
|
|
def _close_open_tags(chunk: str, tag_stack: list[str]) -> str:
|
||
|
|
result = chunk
|
||
|
|
for tag in reversed(tag_stack):
|
||
|
|
close_tag = {"<b>": "</b>", "<i>": "</i>", "<s>": "</s>", "<u>": "</u>",
|
||
|
|
"<code>": "</code>", "<pre>": "</pre>",
|
||
|
|
'<a': '</a>',
|
||
|
|
"<tg-spoiler>": "</tg-spoiler>",
|
||
|
|
"<blockquote>": "</blockquote>",
|
||
|
|
"<tg-emoji": "</tg-emoji>"}.get(tag, "")
|
||
|
|
if close_tag:
|
||
|
|
result += close_tag
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def _skip_whitespace(html: str, pos: int) -> int:
|
||
|
|
while pos < len(html) and html[pos] in (" ", "\n", "\r", "\t"):
|
||
|
|
pos += 1
|
||
|
|
return pos
|