From 155d554f67af1d4d76d93a7885d54f4d881a21f0 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Thu, 15 Apr 2021 16:25:19 -0700 Subject: [PATCH] code update for markdown plugin. This is needed to implement the line numbering system used for a soon to be added post.. --- fenced_code.py | 182 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 fenced_code.py diff --git a/fenced_code.py b/fenced_code.py new file mode 100644 index 0000000..d3b5b15 --- /dev/null +++ b/fenced_code.py @@ -0,0 +1,182 @@ +""" +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)