Coverage for webapp/markdown.py: 98%
44 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +0000
1import re
2import html
4from mistune import HTMLRenderer, Markdown
5from mistune.block_parser import BlockParser
6from mistune.inline_parser import InlineParser
7from mistune.plugins.formatting import strikethrough
8from mistune.plugins.url import url
9from mistune.util import expand_leading_tab
11# All the overrides were discussed here:
12# https://forum.snapcraft.io/t/use-of-markdown-in-snap-metadata-summary-description/2128
14_INDENT_CODE_TRIM = re.compile(r"^ {1,3}", flags=re.M)
17class SnapcraftBlockParser(BlockParser):
18 SPECIFICATION = {
19 **BlockParser.SPECIFICATION,
20 # Indent code is 3 spaces instead of 4
21 "indent_code": (
22 r"^(?: {3}| *\t)[^\n]+(?:\n+|$)"
23 r"((?:(?: {3}| *\t)[^\n]+(?:\n+|$))|\s)*"
24 ),
25 }
27 # We removed many block rules
28 DEFAULT_RULES = (
29 "blank_line",
30 "thematic_break",
31 "indent_code",
32 "list",
33 )
35 def parse_indent_code(self, m, state):
36 end_pos = state.append_paragraph()
37 if end_pos:
38 return end_pos
39 code = m.group(0)
40 code = expand_leading_tab(code)
41 code = _INDENT_CODE_TRIM.sub("", code)
42 code = code.lstrip("\n")
43 state.append_token(
44 {"type": "block_code", "raw": code, "style": "indent"}
45 )
46 return m.end()
48 def parse_method(self, m, state):
49 # mistune's list parser invokes parse_method for rules outside
50 # DEFAULT_RULES (atx_heading, block_quote, ...) render those as
51 # paragraph text instead of letting the inherited methods run.
52 if m.lastgroup not in self.DEFAULT_RULES:
53 end_pos = state.find_line_end()
54 state.append_token(
55 {"type": "paragraph", "text": state.get_text(end_pos)}
56 )
57 state.cursor = end_pos
58 return end_pos
59 return super().parse_method(m, state)
62class SnapcraftInlineParser(InlineParser):
63 SPECIFICATION = {
64 **InlineParser.SPECIFICATION,
65 # Only match a single backtick so triple-backtick fences stay literal
66 "codespan": r"(?<!`)`(?!`)",
67 }
69 # We removed many inline rules
70 DEFAULT_RULES = (
71 "escape",
72 "auto_link",
73 "auto_email",
74 "emphasis",
75 "codespan",
76 "linebreak",
77 # Keep rule enabled for mistune precedence compatibility;
78 # parse_link below turns markdown [text](url) syntax into literal text.
79 "link",
80 )
82 def __init__(self):
83 super().__init__()
84 # Parent __init__ appends "softbreak" drop it so newlines stay as
85 # literal characters in the output
86 if "softbreak" in self.rules:
87 self.rules.remove("softbreak")
89 def parse_link(self, m, state):
90 # Keep markdown link syntax as literal text instead of creating links.
91 #
92 # Supported Mistune v3 customization point:
93 # - Inline parser methods can be overridden (see InlineParser API).
94 # https://mistune.lepture.com/en/latest/api.html#mistune.InlineParser
95 # - Default link behavior lives in InlineParser.parse_link.
96 # https://raw.githubusercontent.com/lepture/mistune/v3.2.1/src/mistune/inline_parser.py
97 state.append_token({"type": "text", "raw": m.group(0)})
98 return m.end()
101renderer = HTMLRenderer()
102parser = Markdown(
103 renderer=renderer,
104 block=SnapcraftBlockParser(),
105 inline=SnapcraftInlineParser(),
106 plugins=[strikethrough, url],
107)
110def parse_markdown_description(content):
111 unescaped_content = html.unescape(content)
112 return parser(unescaped_content)