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

1from html.parser import HTMLParser 

2from typing import Any 

3from urllib.parse import urlencode 

4 

5from markdown_it import MarkdownIt 

6from markupsafe import Markup, escape 

7 

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) 

26 

27 

28def markdown_to_html(text: str) -> Markup: 

29 return Markup(_markdown.render(text)) 

30 

31 

32def markdown_to_plaintext(text: str) -> str: 

33 return html_to_plaintext(markdown_to_html(text)) 

34 

35 

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 """ 

41 

42 if isinstance(html, Markup): 

43 html = str(html) 

44 

45 converter = _HTMLToPlaintext() 

46 converter.feed(html) 

47 return converter.plaintext 

48 

49 

50class _HTMLToPlaintext(HTMLParser): 

51 plaintext: str 

52 

53 def __init__(self) -> None: 

54 super().__init__() 

55 self.plaintext = "" 

56 

57 def handle_starttag(self, tag: str, attrs: Any) -> None: 

58 if tag == "br": 

59 self.plaintext += "\n" 

60 

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 

64 

65 

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) 

72 

73 

74def html_link(url: str, *, text: str | None = None) -> Markup: 

75 return Markup(f'<a href="{escape(url)}">{escape(text or url)}</a>')