Coverage for webapp/markdown.py: 98%

44 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 22:43 +0000

1import re 

2import html 

3 

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 

10 

11# All the overrides were discussed here: 

12# https://forum.snapcraft.io/t/use-of-markdown-in-snap-metadata-summary-description/2128 

13 

14_INDENT_CODE_TRIM = re.compile(r"^ {1,3}", flags=re.M) 

15 

16 

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 } 

26 

27 # We removed many block rules 

28 DEFAULT_RULES = ( 

29 "blank_line", 

30 "thematic_break", 

31 "indent_code", 

32 "list", 

33 ) 

34 

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() 

47 

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) 

60 

61 

62class SnapcraftInlineParser(InlineParser): 

63 SPECIFICATION = { 

64 **InlineParser.SPECIFICATION, 

65 # Only match a single backtick so triple-backtick fences stay literal 

66 "codespan": r"(?<!`)`(?!`)", 

67 } 

68 

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 ) 

81 

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

88 

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() 

99 

100 

101renderer = HTMLRenderer() 

102parser = Markdown( 

103 renderer=renderer, 

104 block=SnapcraftBlockParser(), 

105 inline=SnapcraftInlineParser(), 

106 plugins=[strikethrough, url], 

107) 

108 

109 

110def parse_markdown_description(content): 

111 unescaped_content = html.unescape(content) 

112 return parser(unescaped_content)