1""" 2Fenced Code Extension for Python Markdown 3========================================= 4 5This extension adds Fenced Code Blocks to Python-Markdown. 6 7See <https://Python-Markdown.github.io/extensions/fenced_code_blocks> 8for documentation. 9 10Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). 11 12 13All changes Copyright 2008-2014 The Python Markdown Project 14 15License: [BSD](https://opensource.org/licenses/bsd-license.php) 16""" 17 18 19from textwrap import dedent 20from . import Extension 21from ..preprocessors import Preprocessor 22from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines 23from .attr_list import get_attrs, AttrListExtension 24from ..util import parseBoolValue 25from ..serializers import _escape_attrib_html 26import re 27 28 29class FencedCodeExtension(Extension): 30 def __init__(self, **kwargs): 31 self.config = { 32 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] 33 } 34 super().__init__(**kwargs) 35 36 def extendMarkdown(self, md): 37 """ Add FencedBlockPreprocessor to the Markdown instance. """ 38 md.registerExtension(self) 39 40 md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25) 41 42 43class FencedBlockPreprocessor(Preprocessor): 44 FENCED_BLOCK_RE = re.compile( 45 dedent(r''' 46 (?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence 47 ((\{(?P<attrs>[^\}\n]*)\})| # (optional {attrs} or 48 (\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang 49 (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines) 50 \n # newline (end of opening fence) 51 (?P<code>.*?)(?<=\n) # the code block 52 (?P=fence)[ ]*$ # closing fence 53 '''), 54 re.MULTILINE | re.DOTALL | re.VERBOSE 55 ) 56 57 def __init__(self, md, config): 58 super().__init__(md) 59 self.config = config 60 self.checked_for_deps = False 61 self.codehilite_conf = {} 62 self.use_attr_list = False 63 # List of options to convert to bool values 64 self.bool_options = [ 65 'linenums', 66 'guess_lang', 67 'noclasses', 68 'use_pygments' 69 ] 70 71 def run(self, lines): 72 """ Match and store Fenced Code Blocks in the HtmlStash. """ 73 74 # Check for dependent extensions 75 if not self.checked_for_deps: 76 for ext in self.md.registeredExtensions: 77 if isinstance(ext, CodeHiliteExtension): 78 self.codehilite_conf = ext.getConfigs() 79 if isinstance(ext, AttrListExtension): 80 self.use_attr_list = True 81 82 self.checked_for_deps = True 83 84 text = "\n".join(lines) 85 while 1: 86 m = self.FENCED_BLOCK_RE.search(text) 87 if m: 88 lang, id, classes, config = None, '', [], {} 89 if m.group('attrs'): 90 id, classes, config = self.handle_attrs(get_attrs(m.group('attrs'))) 91 if len(classes): 92 lang = classes.pop(0) 93 else: 94 if m.group('lang'): 95 lang = m.group('lang') 96 if m.group('hl_lines'): 97 # Support hl_lines outside of attrs for backward-compatibility 98 config['hl_lines'] = parse_hl_lines(m.group('hl_lines')) 99 100 # If config is not empty, then the codehighlite extension 101 # is enabled, so we call it to highlight the code 102 if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True): 103 local_config = self.codehilite_conf.copy() 104 local_config.update(config) 105 # Combine classes with cssclass. Ensure cssclass is at end 106 # as pygments appends a suffix under certain circumstances. 107 # Ignore ID as Pygments does not offer an option to set it. 108 if classes: 109 local_config['css_class'] = '{} {}'.format( 110 ' '.join(classes), 111 local_config['css_class'] 112 ) 113 highliter = CodeHilite( 114 m.group('code'), 115 lang=lang, 116 style=local_config.pop('pygments_style', 'default'), 117 **local_config 118 ) 119 120 code = highliter.hilite(shebang=False) 121 else: 122 id_attr = lang_attr = class_attr = kv_pairs = '' 123 if lang: 124 prefix = self.config.get('lang_prefix', 'language-') 125 lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"' 126 if classes: 127 class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"' 128 if id: 129 id_attr = f' id="{_escape_attrib_html(id)}"' 130 if self.use_attr_list and config and not config.get('use_pygments', False): 131 # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs 132 # were defined on the code block, and the `use_pygments` key was not set to True. The 133 # `use_pygments` key could be either set to False or not defined. It is omitted from output. 134 kv_pairs = ''.join( 135 f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments' 136 ) 137 code = self._escape(m.group('code')) 138 code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>' 139 140 placeholder = self.md.htmlStash.store(code) 141 text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}' 142 else: 143 break 144 return text.split("\n") 145 146 def handle_attrs(self, attrs): 147 """ Return tuple: (id, [list, of, classes], {configs}) """ 148 id = '' 149 classes = [] 150 configs = {} 151 for k, v in attrs: 152 if k == 'id': 153 id = v 154 elif k == '.': 155 classes.append(v) 156 elif k == 'hl_lines': 157 configs[k] = parse_hl_lines(v) 158 elif k in self.bool_options: 159 configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True) 160 else: 161 configs[k] = v 162 return id, classes, configs 163 164 def _escape(self, txt): 165 """ basic html escaping """ 166 txt = txt.replace('&', '&') 167 txt = txt.replace('<', '<') 168 txt = txt.replace('>', '>') 169 txt = txt.replace('"', '"') 170 return txt 171 172 173def makeExtension(**kwargs): # pragma: no cover 174 return FencedCodeExtension(**kwargs) 175