|
|
@@ -0,0 +1,182 @@ |
|
|
|
""" |
|
|
|
Fenced Code Extension for Python Markdown |
|
|
|
========================================= |
|
|
|
|
|
|
|
This extension adds Fenced Code Blocks to Python-Markdown. |
|
|
|
|
|
|
|
See <https://Python-Markdown.github.io/extensions/fenced_code_blocks> |
|
|
|
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<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence |
|
|
|
((\{(?P<attrs>[^\}\n]*)\})?| # (optional {attrs} or |
|
|
|
(\.?(?P<lang>[\w#.+-]*))?[ ]* # optional (.)lang |
|
|
|
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?) # optional hl_lines) |
|
|
|
[ ]*\n # newline (end of opening fence) |
|
|
|
(?P<code>.*?)(?<=\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('<code{lang}{kv}>{line}</code>'.format( |
|
|
|
kv=kv_pairs, |
|
|
|
lang=lang_attr, |
|
|
|
line=self._escape(line) |
|
|
|
) for line in m.group('code').rstrip().split('\n')) |
|
|
|
code = '<pre{id}{cls}>{codeblk}</pre>'.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) |