74 lines
2.2 KiB
Python
74 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import html
|
|
import re
|
|
|
|
_MD_TO_HTML_MAP = [
|
|
(r"\*\*(.+?)\*\*", r"<b>\1</b>"),
|
|
(r"__(.+?)__", r"<b>\1</b>"),
|
|
(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"<i>\1</i>"),
|
|
(r"(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", r"<i>\1</i>"),
|
|
(r"~~(.+?)~~", r"<del>\1</del>"),
|
|
(r"`([^`\n]+?)`", r"<code>\1</code>"),
|
|
]
|
|
|
|
_HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
|
|
_LINK_PATTERN = re.compile(r"\[(.+?)\]\((.+?)\)")
|
|
_CODE_BLOCK_PATTERN = re.compile(r"```(?:\w+)?\n(.+?)```", re.DOTALL)
|
|
_BLOCKQUOTE_PATTERN = re.compile(r"^>\s?(.*)$", re.MULTILINE)
|
|
_HR_PATTERN = re.compile(r"^(?:[-*_]\s*){3,}$", re.MULTILINE)
|
|
_UNORDERED_LIST_PATTERN = re.compile(r"^[\-\*\+]\s+(.+)$", re.MULTILINE)
|
|
_ORDERED_LIST_PATTERN = re.compile(r"^\d+\.\s+(.+)$", re.MULTILINE)
|
|
|
|
_UNSAFE_TAGS = re.compile(
|
|
r"<\s*(script|iframe|object|embed|form|input|svg|style|link|meta|base|applet)[^>]*>",
|
|
re.IGNORECASE,
|
|
)
|
|
_UNSAFE_ATTRS = re.compile(
|
|
r'\s(on\w+|href\s*=\s*["\']javascript:|style\s*=\s*["\']\s*expression)',
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def markdown_to_matrix_html(text: str) -> str:
|
|
result = text
|
|
|
|
result = _CODE_BLOCK_PATTERN.sub(_code_block_replacement, result)
|
|
|
|
result = _HEADING_PATTERN.sub(_heading_replacement, result)
|
|
|
|
for pattern, replacement in _MD_TO_HTML_MAP:
|
|
result = re.sub(pattern, replacement, result)
|
|
|
|
result = _LINK_PATTERN.sub(r'<a href="\2">\1</a>', result)
|
|
|
|
result = _UNORDERED_LIST_PATTERN.sub(r"<li>\1</li>", result)
|
|
result = _ORDERED_LIST_PATTERN.sub(r"<li>\1</li>", result)
|
|
|
|
result = _BLOCKQUOTE_PATTERN.sub(r"<blockquote>\1</blockquote>", result)
|
|
|
|
result = _HR_PATTERN.sub(r"<hr>", result)
|
|
|
|
result = result.replace("\n", "<br>")
|
|
result = _sanitize_html(result)
|
|
|
|
return result
|
|
|
|
|
|
def _code_block_replacement(match: re.Match) -> str:
|
|
content = match.group(1)
|
|
safe_content = html.escape(content)
|
|
return f"<pre><code>{safe_content}</code></pre>"
|
|
|
|
|
|
def _heading_replacement(match: re.Match) -> str:
|
|
level = len(match.group(1))
|
|
text_content = match.group(2)
|
|
return f"<h{level}>{text_content}</h{level}>"
|
|
|
|
|
|
def _sanitize_html(text: str) -> str:
|
|
text = _UNSAFE_TAGS.sub("", text)
|
|
text = _UNSAFE_ATTRS.sub("", text)
|
|
return text
|