Files
notifications-admin/notifications_utils/markdown.py

285 lines
7.9 KiB
Python
Raw Normal View History

2025-04-09 16:57:42 -07:00
import html
import re
import mistune
2025-04-09 16:57:42 -07:00
from flask import current_app
from notifications_utils.formatters import create_sanitised_html_for_url
LINK_STYLE = "word-wrap: break-word; color: #1D70B8;"
2025-03-26 13:34:23 -07:00
def escape_plus_lists(markdown_text):
return re.sub(r"(?m)^(\+)(?=\s)", r"\\\1", markdown_text)
2025-03-26 13:34:23 -07:00
def autolinkify(text):
# url_pattern = re.compile(r"""(?<!\]\()(?<!["'])\b(https?://[^\s<>()]+)""")
url_pattern = re.compile(
r"""(?<!\]\()
(?<!href=["'])
\b(https?://[^\s<>"')\]]+)""",
re.VERBOSE,
)
def replacer(match):
url = match.group(0)
return f"[{url}]({url})"
return url_pattern.sub(replacer, text)
2025-03-26 09:16:07 -07:00
class EmailRenderer(mistune.HTMLRenderer):
2025-03-26 13:34:23 -07:00
def table(self, header, body):
return ""
def table_row(self, content):
return ""
def table_cell(self, content, **kwargs):
return ""
2025-03-26 09:16:07 -07:00
def heading(self, text, level):
if level == 1:
2025-03-26 09:16:07 -07:00
return (
'<h2 style="Margin: 0 0 20px 0; padding: 0; '
2025-03-26 13:34:23 -07:00
'font-size: 27px; line-height: 35px; font-weight: bold; color: #0B0C0C;">'
2025-03-26 09:16:07 -07:00
f"{text}</h2>"
)
return self.paragraph(text)
def paragraph(self, text):
if text.strip():
2025-03-26 13:34:23 -07:00
text = html.unescape(text)
2025-03-26 09:16:07 -07:00
return (
'<p style="Margin: 0 0 20px 0; font-size: 19px; '
2025-03-26 13:34:23 -07:00
'line-height: 25px; color: #0B0C0C;">' + text + "</p>"
2025-03-26 09:16:07 -07:00
)
2025-03-26 09:16:07 -07:00
def emphasis(self, text):
return f"*{text}*"
2025-03-26 13:34:23 -07:00
def strong(self, text):
return f"**{text}**"
def block_code(self, code, info=None):
return code.strip()
2025-03-26 09:16:07 -07:00
def block_quote(self, text):
return (
'<blockquote style="Margin: 0 0 20px 0; border-left: 10px solid #B1B4B6; '
'padding: 15 px 0 0.1px 15 px; font-size: 19px; line-height: 25px;">'
f"{text}</blockquote>"
)
2025-03-26 09:16:07 -07:00
def thematic_break(self):
return '<hr style="border: 0; height: 1px; background: #B1B4B6; Margin: 30px 0 30px 0;">'
2025-03-26 09:16:07 -07:00
def codespan(self, text):
2025-03-26 13:34:23 -07:00
return f"`{text}`"
def linebreak(self):
2025-03-26 09:16:07 -07:00
return "<br />"
2025-03-26 13:34:23 -07:00
def newline(self):
return self.linebreak()
def list(self, text, ordered, level=None, **kwargs):
2025-03-26 09:16:07 -07:00
tag = "ol" if ordered else "ul"
2025-03-26 13:34:23 -07:00
style = "list-style-type: decimal;" if ordered else "list-style-type: disc;"
2025-03-26 09:16:07 -07:00
return (
2025-03-26 13:34:23 -07:00
'<table role="presentation" style="padding 0 0 20px 0;">'
'<tr><td style="font-family: Helvetica, Arial, sans-serif;">'
2025-03-26 09:16:07 -07:00
f'<{tag} style="Margin: 0 0 0 20px; padding: 0; {style}">{text}</{tag}>'
2025-03-26 13:34:23 -07:00
"</td></tr></table>"
2025-03-26 09:16:07 -07:00
)
def list_item(self, text, level=None):
return (
'<li style="Margin: 5px 0 5px; padding: 0 0 0 5px; font-size: 19px;'
2025-03-26 13:34:23 -07:00
'line-height: 25px; color: #0B0C0C;">' + text.strip() + "</li>"
2025-03-26 09:16:07 -07:00
)
2025-03-26 09:16:07 -07:00
def link(self, link=None, text=None, title=None, url=None, **kwargs):
2025-03-26 13:34:23 -07:00
href = html.escape(
url or (link if link and link.startswith("http://", "https://") else "")
)
2025-03-26 09:16:07 -07:00
display_text = text or link or href or ""
title_attr = f' title="{title}"' if title else ""
return f'<a style="{LINK_STYLE}" href="{href}"{title_attr}>{display_text}</a>'
2025-03-27 12:58:05 -07:00
def autolink(self, link, is_email=False): # noqa
2025-03-26 14:21:45 -07:00
2025-03-26 09:16:07 -07:00
return create_sanitised_html_for_url(link, style=LINK_STYLE)
2025-03-27 12:58:05 -07:00
def image(self, src, alt="", title=None, url=None): # noqa
2025-03-27 12:53:10 -07:00
current_app.logger.debug(f"src={src} alt={alt} title={title} url={url}")
return ""
2025-03-26 09:16:07 -07:00
def strikethrough(self, text):
return (
'<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">'
f"~~{text}~~"
2025-03-26 13:34:23 -07:00
"</p>"
2025-03-26 09:16:07 -07:00
)
2025-03-26 13:34:23 -07:00
2025-03-26 09:16:07 -07:00
class PlainTextRenderer(mistune.HTMLRenderer):
COLUMN_WIDTH = 65
def heading(self, text, level):
if level == 1:
return f"\n\n\n{text}\n{'-' * self.COLUMN_WIDTH}"
return self.paragraph(text)
def paragraph(self, text):
if text.strip():
return f"\n\n{text}"
return ""
2025-03-26 09:16:07 -07:00
def thematic_break(self):
2025-03-26 13:34:23 -07:00
return f"\n\n{'=' * self.COLUMN_WIDTH}"
2025-03-26 09:16:07 -07:00
def block_quote(self, text):
return text
2025-03-26 13:34:23 -07:00
def block_code(self, code, info=None):
return code.strip()
def linebreak(self):
2025-03-26 09:16:07 -07:00
return "\n"
2025-03-26 09:16:07 -07:00
def list(self, text, ordered, level=None, **kwargs):
2025-03-27 14:47:43 -07:00
if ordered is True:
text = text.replace("", "1.", 1)
text = text.replace("", "2.", 1)
text = text.replace("", "3.", 1)
# print(f"LIST ordered={ordered} text={text}")
2025-03-26 09:16:07 -07:00
return f"\n{text}"
2025-03-27 14:47:43 -07:00
def list_item(self, text, ordered=None, level=None):
# print(f"LIST ITEM = {text} ordered={ordered} level {level}")
2025-03-26 09:16:07 -07:00
return f"\n{text.strip()}"
2025-03-26 09:16:07 -07:00
def link(self, link=None, text=None, title=None, url=None, **kwargs):
display_text = text or link or url or ""
href = url or link or ""
output = display_text
if title:
output += f" ({title})"
if href:
output += f": {href}"
return output
def autolink(self, link, is_email=False):
return link
def image(self, src, alt="", title=None, url=None):
return ""
2025-03-26 09:16:07 -07:00
def emphasis(self, text):
return f"*{text}*"
2025-03-26 09:16:07 -07:00
def strong(self, text):
return f"**{text}**"
2025-03-26 09:16:07 -07:00
def codespan(self, text):
return f"`{text}`"
2025-03-26 09:16:07 -07:00
def strikethrough(self, text):
return f"~~{text}~~"
2025-03-26 13:34:23 -07:00
2025-03-26 09:16:07 -07:00
class PreheaderRenderer(PlainTextRenderer):
2025-03-27 14:47:43 -07:00
2025-03-26 09:16:07 -07:00
def heading(self, text, level):
2025-03-27 14:47:43 -07:00
return html.unescape(self.paragraph(text))
2025-03-26 09:16:07 -07:00
def thematic_break(self):
return ""
2025-03-27 14:47:43 -07:00
def link(self, link, text=None, title=None, url=None):
2025-03-26 09:16:07 -07:00
return text or link
2025-03-26 09:16:07 -07:00
def image(self, src, alt="", title=None, url=None):
2025-03-27 12:53:10 -07:00
current_app.logger.debug("src={src} alt={alt} title={title} url={url}")
2025-03-26 09:16:07 -07:00
return ""
class LetterPreviewRenderer(mistune.HTMLRenderer):
def heading(self, text, level):
if level == 1:
return super().heading(text, 2)
return self.paragraph(text)
def paragraph(self, text):
if text.strip():
2025-03-26 09:16:07 -07:00
return f"<p>{text}</p>"
return ""
2025-03-26 13:34:23 -07:00
def block_code(self, code, info=None):
return code.strip()
2025-03-26 09:16:07 -07:00
def link(self, link, text=None, title=None, url=None):
2025-03-27 12:53:10 -07:00
current_app.logger(f"title={title}")
2025-03-26 09:16:07 -07:00
href = url
display_text = text or link
return f"{display_text}: <strong>{href.replace('http://', '').replace('https://', '')}</strong>"
def autolink(self, link, is_email=False):
2025-03-27 12:53:10 -07:00
current_app.logger.debug(f"is_email={is_email}")
2025-03-26 09:16:07 -07:00
return f"<strong>{link.replace('http://', '')}.replace(https://', '')</strong>"
def thematic_break(self):
return '<div class="page-break">&nbsp;</div>'
def image(self, src, alt="", title=None, **kwargs):
return ""
def block_quote(self, text):
return text
2025-03-26 09:16:07 -07:00
def list_item(self, text, level=None):
return f"<li>{text.strip()}</li>\n"
2025-03-26 09:16:07 -07:00
def emphasis(self, text):
return f"*{text}*"
2025-03-26 09:16:07 -07:00
def strong(self, text):
return f"**{text}**"
2025-03-26 09:16:07 -07:00
def codespan(self, text):
return f"`{text}`"
2025-03-26 09:16:07 -07:00
def linebreak(self):
return "<br>"
def newline(self):
return "<br>"
2025-03-26 13:34:23 -07:00
_notify_email_markdown = mistune.create_markdown(
renderer=EmailRenderer(), hard_wrap=True
)
notify_letter_preview_markdown = mistune.create_markdown(
renderer=LetterPreviewRenderer()
)
2025-03-26 09:16:07 -07:00
notify_email_preheader_markdown = mistune.create_markdown(renderer=PreheaderRenderer())
2025-03-26 13:34:23 -07:00
_notify_plain_text_email_markdown = mistune.create_markdown(
renderer=PlainTextRenderer()
)
def notify_email_markdown(text):
2025-03-27 14:47:43 -07:00
text = escape_plus_lists(text)
2025-03-26 13:34:23 -07:00
return _notify_email_markdown(autolinkify(text))
def notify_plain_text_email_markdown(text):
text = escape_plus_lists(text)
return _notify_plain_text_email_markdown(text)