from __future__ import annotations import re from html import escape _ESCAPE_CHARS = {"&": "&", "<": "<", ">": ">"} _TAG_PATTERN = r"<(?!/?(?:b|strong|i|em|u|ins|s|strike|del|code|pre|a|tg-spoiler|tg-emoji)\b)[^>]*>" _TAG_PLACEHOLDER_PATTERN = re.compile(_TAG_PATTERN) _CODE_BLOCK_RE = re.compile(r"```(?:\w+)?\n.+?```", re.DOTALL) _INLINE_CODE_RE = re.compile(r"`([^`\n]+?)`") _PLACEHOLDER_PREFIX = "\x00MDPLACEHOLDER" def markdown_to_html(text: str) -> str: code_blocks: dict[str, str] = {} text = _protect_code_spans(text, _CODE_BLOCK_RE, code_blocks) text = _protect_code_spans(text, _INLINE_CODE_RE, code_blocks) text = _convert_bold(text) text = _convert_italic(text) text = _convert_strikethrough(text) text = _convert_underline(text) text = _convert_links(text) text = _convert_spoiler(text) text = _restore_code_spans(text, code_blocks) text = _sanitize_html(text) return text def strip_all_tags(text: str) -> str: return re.sub(r"<[^>]+>", "", text) def strip_html_tags(text: str) -> str: return strip_all_tags(text) def _protect_code_spans(text: str, pattern: re.Pattern, storage: dict[str, str]) -> str: result = [] idx = 0 for m in pattern.finditer(text): result.append(text[idx : m.start()]) placeholder = f"{_PLACEHOLDER_PREFIX}{len(storage)}" storage[placeholder] = m.group(0) result.append(placeholder) idx = m.end() result.append(text[idx:]) return "".join(result) def _restore_code_spans(text: str, storage: dict[str, str]) -> str: result = text for placeholder, code_text in storage.items(): if placeholder.startswith(f"{_PLACEHOLDER_PREFIX}") and "```" in code_text: inner = code_text[3:].rstrip("`").lstrip("\n") lang_end = inner.find("\n") inner = inner[lang_end + 1 :] if lang_end >= 0 else inner safe = escape(inner, quote=False) result = result.replace(placeholder, f"
{safe}")
elif placeholder in result:
inner = code_text.strip("`")
safe = escape(inner, quote=False)
result = result.replace(placeholder, f"{safe}")
return result
def _convert_bold(text: str) -> str:
return re.sub(r"\*\*(.+?)\*\*", r"\1", text)
def _convert_italic(text: str) -> str:
text = re.sub(r"(?\1", text)
text = re.sub(r"(?\1", text)
return text
def _convert_strikethrough(text: str) -> str:
return re.sub(r"~~(.+?)~~", r"