""" Fenced Code Extension for Python Markdown ========================================= This extension adds Fenced Code Blocks to Python-Markdown. See for documentation. Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). All changes Copyright 2008-2014 The Python Markdown Project License: [BSD](https://opensource.org/licenses/bsd-license.php) """ from textwrap import dedent from . import Extension from ..preprocessors import Preprocessor from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines from .attr_list import get_attrs, AttrListExtension from ..util import parseBoolValue import re class FencedCodeExtension(Extension): def __init__(self, **kwargs): self.config = { 'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"'] } super().__init__(**kwargs) def extendMarkdown(self, md): """ Add FencedBlockPreprocessor to the Markdown instance. """ md.registerExtension(self) md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25) class FencedBlockPreprocessor(Preprocessor): FENCED_BLOCK_RE = re.compile( dedent(r''' (?P^(?:~{3,}|`{3,}))[ ]* # opening fence ((\{(?P[^\}\n]*)\})?| # (optional {attrs} or (\.?(?P[\w#.+-]*))?[ ]* # optional (.)lang (hl_lines=(?P"|')(?P.*?)(?P=quot))?) # optional hl_lines) [ ]*\n # newline (end of opening fence) (?P.*?)(?<=\n) # the code block (?P=fence)[ ]*$ # closing fence '''), re.MULTILINE | re.DOTALL | re.VERBOSE ) def __init__(self, md, config): super().__init__(md) self.config = config self.checked_for_deps = False self.codehilite_conf = {} self.use_attr_list = False # List of options to convert to bool values self.bool_options = [ 'linenums', 'guess_lang', 'noclasses', 'use_pygments' ] def run(self, lines): """ Match and store Fenced Code Blocks in the HtmlStash. """ # Check for dependent extensions if not self.checked_for_deps: for ext in self.md.registeredExtensions: if isinstance(ext, CodeHiliteExtension): self.codehilite_conf = ext.getConfigs() if isinstance(ext, AttrListExtension): self.use_attr_list = True self.checked_for_deps = True text = "\n".join(lines) while 1: m = self.FENCED_BLOCK_RE.search(text) if m: lang, id, classes, config = None, '', [], {} if m.group('attrs'): id, classes, config = self.handle_attrs(get_attrs(m.group('attrs'))) if len(classes): lang = classes.pop(0) else: if m.group('lang'): lang = m.group('lang') if m.group('hl_lines'): # Support hl_lines outside of attrs for backward-compatibility config['hl_lines'] = parse_hl_lines(m.group('hl_lines')) # If config is not empty, then the codehighlite extension # is enabled, so we call it to highlight the code if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True): local_config = self.codehilite_conf.copy() local_config.update(config) # Combine classes with cssclass. Ensure cssclass is at end # as pygments appends a suffix under certain circumstances. # Ignore ID as Pygments does not offer an option to set it. if classes: local_config['css_class'] = '{} {}'.format( ' '.join(classes), local_config['css_class'] ) highliter = CodeHilite( m.group('code'), lang=lang, style=local_config.pop('pygments_style', 'default'), **local_config ) code = highliter.hilite() else: id_attr = lang_attr = class_attr = kv_pairs = '' if lang: lang_attr = ' class="{}{}"'.format(self.config.get('lang_prefix', 'language-'), lang) if classes: class_attr = ' class="{}"'.format(' '.join(classes)) if id: id_attr = ' id="{}"'.format(id) if self.use_attr_list and config and not config.get('use_pygments', False): # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs # were defined on the code block, and the `use_pygments` key was not set to True. The # `use_pygments` key could be either set to False or not defined. It is omitted from output. kv_pairs = ' ' + ' '.join( '{k}="{v}"'.format(k=k, v=v) for k, v in config.items() if k != 'use_pygments' ) codeblk = ''.join('{line}'.format( kv=kv_pairs, lang=lang_attr, line=self._escape(line) ) for line in m.group('code').rstrip().split('\n')) code = '{codeblk}'.format( id=id_attr, codeblk=codeblk, cls=class_attr, ) placeholder = self.md.htmlStash.store(code) text = '{}\n{}\n{}'.format(text[:m.start()], placeholder, text[m.end():]) else: break return text.split("\n") def handle_attrs(self, attrs): """ Return tuple: (id, [list, of, classes], {configs}) """ id = '' classes = [] configs = {} for k, v in attrs: if k == 'id': id = v elif k == '.': classes.append(v) elif k == 'hl_lines': configs[k] = parse_hl_lines(v) elif k in self.bool_options: configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True) else: configs[k] = v return id, classes, configs def _escape(self, txt): """ basic html escaping """ txt = txt.replace('&', '&') txt = txt.replace('<', '<') txt = txt.replace('>', '>') txt = txt.replace('"', '"') return txt def makeExtension(**kwargs): # pragma: no cover return FencedCodeExtension(**kwargs)