Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown/extensions/fenced_code.py: 89%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

100 statements  

1# Fenced Code Extension for Python Markdown 

2# ========================================= 

3 

4# This extension adds Fenced Code Blocks to Python-Markdown. 

5 

6# See https://Python-Markdown.github.io/extensions/fenced_code_blocks 

7# for documentation. 

8 

9# Original code Copyright 2007-2008 [Waylan Limberg](https://github.com/waylan). 

10 

11# All changes Copyright 2008-2014 The Python Markdown Project 

12 

13# License: [BSD](https://opensource.org/licenses/bsd-license.php) 

14 

15""" 

16This extension adds Fenced Code Blocks to Python-Markdown. 

17 

18See the [documentation](https://Python-Markdown.github.io/extensions/fenced_code_blocks) 

19for details. 

20""" 

21 

22from __future__ import annotations 

23 

24from textwrap import dedent 

25from . import Extension 

26from ..preprocessors import Preprocessor 

27from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines 

28from .attr_list import get_attrs_and_remainder, AttrListExtension 

29from ..util import parseBoolValue 

30from ..serializers import _escape_attrib_html 

31import re 

32from typing import TYPE_CHECKING, Any, Iterable 

33 

34if TYPE_CHECKING: # pragma: no cover 

35 from markdown import Markdown 

36 

37 

38class FencedCodeExtension(Extension): 

39 def __init__(self, **kwargs): 

40 self.config = { 

41 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] 

42 } 

43 """ Default configuration options. """ 

44 super().__init__(**kwargs) 

45 

46 def extendMarkdown(self, md): 

47 """ 

48 Register the processor. 

49 

50 | Class Instance | Registry | Name | Priority | 

51 | --------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------ | :------: | 

52 | [`FencedBlockPreprocessor`][markdown.extensions.fenced_code.FencedBlockPreprocessor] | [`blockprocessors`][markdown.blockprocessors.build_block_parser] | `fenced_code_block` | `25` | 

53 

54 """ 

55 # flake8: noqa: E501 50-52 

56 md.registerExtension(self) 

57 

58 md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25) 

59 

60 

61class FencedBlockPreprocessor(Preprocessor): 

62 """ Find and extract fenced code blocks. """ 

63 

64 FENCED_BLOCK_RE = re.compile( 

65 dedent(r''' 

66 (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence 

67 ((\{(?P<attrs>[^\n]*)\})| # (optional {attrs} or 

68 (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang 

69 (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines) 

70 \n # newline (end of opening fence) 

71 (?P<code>.*?)(?<=\n) # the code block 

72 (?P=fence)[ ]*$ # closing fence 

73 '''), 

74 re.MULTILINE | re.DOTALL | re.VERBOSE 

75 ) 

76 

77 def __init__(self, md: Markdown, config: dict[str, Any]): 

78 super().__init__(md) 

79 self.config = config 

80 self.checked_for_deps = False 

81 self.codehilite_conf: dict[str, Any] = {} 

82 self.use_attr_list = False 

83 # List of options to convert to boolean values 

84 self.bool_options = [ 

85 'linenums', 

86 'guess_lang', 

87 'noclasses', 

88 'use_pygments' 

89 ] 

90 

91 def run(self, lines: list[str]) -> list[str]: 

92 """ Match and store Fenced Code Blocks in the `HtmlStash`. """ 

93 

94 # Check for dependent extensions 

95 if not self.checked_for_deps: 

96 for ext in self.md.registeredExtensions: 

97 if isinstance(ext, CodeHiliteExtension): 

98 self.codehilite_conf = ext.getConfigs() 

99 if isinstance(ext, AttrListExtension): 

100 self.use_attr_list = True 

101 

102 self.checked_for_deps = True 

103 

104 text = "\n".join(lines) 

105 index = 0 

106 while 1: 

107 m = self.FENCED_BLOCK_RE.search(text, index) 

108 if m: 

109 lang, id, classes, config = None, '', [], {} 

110 if m.group('attrs'): 

111 attrs, remainder = get_attrs_and_remainder(m.group('attrs')) 

112 if remainder: # Does not have correctly matching curly braces, so the syntax is invalid. 

113 index = m.end('attrs') # Explicitly skip over this, to prevent an infinite loop. 

114 continue 

115 id, classes, config = self.handle_attrs(attrs) 

116 if len(classes): 

117 lang = classes.pop(0) 

118 else: 

119 if m.group('lang'): 

120 lang = m.group('lang') 

121 if m.group('hl_lines'): 

122 # Support `hl_lines` outside of `attrs` for backward-compatibility 

123 config['hl_lines'] = parse_hl_lines(m.group('hl_lines')) 

124 

125 # If `config` is not empty, then the `codehighlite` extension 

126 # is enabled, so we call it to highlight the code 

127 if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True): 

128 local_config = self.codehilite_conf.copy() 

129 local_config.update(config) 

130 # Combine classes with `cssclass`. Ensure `cssclass` is at end 

131 # as Pygments appends a suffix under certain circumstances. 

132 # Ignore ID as Pygments does not offer an option to set it. 

133 if classes: 

134 local_config['css_class'] = '{} {}'.format( 

135 ' '.join(classes), 

136 local_config['css_class'] 

137 ) 

138 highliter = CodeHilite( 

139 m.group('code'), 

140 lang=lang, 

141 style=local_config.pop('pygments_style', 'default'), 

142 **local_config 

143 ) 

144 

145 code = highliter.hilite(shebang=False) 

146 else: 

147 id_attr = lang_attr = class_attr = kv_pairs = '' 

148 if lang: 

149 prefix = self.config.get('lang_prefix', 'language-') 

150 lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"' 

151 if classes: 

152 class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"' 

153 if id: 

154 id_attr = f' id="{_escape_attrib_html(id)}"' 

155 if self.use_attr_list and config and not config.get('use_pygments', False): 

156 # Only assign key/value pairs to code element if `attr_list` extension is enabled, key/value 

157 # pairs were defined on the code block, and the `use_pygments` key was not set to `True`. The 

158 # `use_pygments` key could be either set to `False` or not defined. It is omitted from output. 

159 kv_pairs = ''.join( 

160 f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments' 

161 ) 

162 code = self._escape(m.group('code')) 

163 code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>' 

164 

165 placeholder = self.md.htmlStash.store(code) 

166 text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}' 

167 # Continue from after the replaced text in the next iteration. 

168 index = m.start() + 1 + len(placeholder) 

169 else: 

170 break 

171 return text.split("\n") 

172 

173 def handle_attrs(self, attrs: Iterable[tuple[str, str]]) -> tuple[str, list[str], dict[str, Any]]: 

174 """ Return tuple: `(id, [list, of, classes], {configs})` """ 

175 id = '' 

176 classes = [] 

177 configs = {} 

178 for k, v in attrs: 

179 if k == 'id': 

180 id = v 

181 elif k == '.': 

182 classes.append(v) 

183 elif k == 'hl_lines': 

184 configs[k] = parse_hl_lines(v) 

185 elif k in self.bool_options: 

186 configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True) 

187 else: 

188 configs[k] = v 

189 return id, classes, configs 

190 

191 def _escape(self, txt: str) -> str: 

192 """ basic html escaping """ 

193 txt = txt.replace('&', '&amp;') 

194 txt = txt.replace('<', '&lt;') 

195 txt = txt.replace('>', '&gt;') 

196 txt = txt.replace('"', '&quot;') 

197 return txt 

198 

199 

200def makeExtension(**kwargs): # pragma: no cover 

201 return FencedCodeExtension(**kwargs)