Coverage for app/backend/src/couchers/markup.py: 100%
33 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 21:52 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 21:52 +0000
1from html.parser import HTMLParser
2from typing import Any
3from urllib.parse import urlencode
5from markdown_it import MarkdownIt
6from markupsafe import Markup, escape
8# Markdown config should match frontend's MarkdownNoSSR component.
9_markdown = MarkdownIt(
10 "zero", # Base configuration disables all features
11 options_update={
12 "typographer": True, # Enable some language-neutral replacement + quotes beautification
13 "breaks": True, # Convert '\n' in paragraphs into <br>
14 },
15).enable(
16 [
17 "emphasis", # Process *this* and _that_
18 "heading", # Headings (#, ##, ...)
19 "hr", # Horizontal rule
20 "link", # Process [link](<to> "stuff")
21 "list", # Lists
22 "newline", # Process '\n'
23 "smartquotes", # Convert straight quotation marks to typographic ones
24 ]
25)
28def markdown_to_html(text: str) -> Markup:
29 return Markup(_markdown.render(text))
32def markdown_to_plaintext(text: str) -> str:
33 return html_to_plaintext(markdown_to_html(text))
36def html_to_plaintext(html: str | Markup) -> str:
37 """
38 Renders a plaintext version of HTML by extracting inner HTML and converting entities+newlines.
39 Do not use for sanitization. The resulting string may not be markup-safe.
40 """
42 if isinstance(html, Markup):
43 html = str(html)
45 converter = _HTMLToPlaintext()
46 converter.feed(html)
47 return converter.plaintext
50class _HTMLToPlaintext(HTMLParser):
51 plaintext: str
53 def __init__(self) -> None:
54 super().__init__()
55 self.plaintext = ""
57 def handle_starttag(self, tag: str, attrs: Any) -> None:
58 if tag == "br":
59 self.plaintext += "\n"
61 def handle_data(self, data: str) -> None:
62 # Escapes have already been unescaped
63 self.plaintext += data.replace("\n", "") # Newlines in html are meaningless
66def html_mailto_link(email: str, subject: str | None = None) -> Markup:
67 url = f"mailto:{email}"
68 if subject:
69 query = {"subject": subject}
70 url += f"?{urlencode(query)}"
71 return html_link(url, text=email)
74def html_link(url: str, *, text: str | None = None) -> Markup:
75 return Markup(f'<a href="{escape(url)}">{escape(text or url)}</a>')