blob: 409166ad8a9e16b192125e8850e9eef267497edc [file] [log] [blame]
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](
All changes Copyright 2008-2014 The Python Markdown Project
License: [BSD](
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
from ..serializers import _escape_attrib_html
import re
class FencedCodeExtension(Extension):
def __init__(self, **kwargs):
self.config = {
'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
def extendMarkdown(self, md):
""" Add FencedBlockPreprocessor to the Markdown instance. """
md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
class FencedBlockPreprocessor(Preprocessor):
FENCED_BLOCK_RE = re.compile(
(?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
def __init__(self, md, config):
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 = [
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
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 =
if m:
lang, id, classes, config = None, '', [], {}
id, classes, config = self.handle_attrs(get_attrs('attrs')))
if len(classes):
lang = classes.pop(0)
lang ='lang')
# Support hl_lines outside of attrs for backward-compatibility
config['hl_lines'] = parse_hl_lines('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()
# 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),
highliter = CodeHilite('code'),
style=local_config.pop('pygments_style', 'default'),
code = highliter.hilite(shebang=False)
id_attr = lang_attr = class_attr = kv_pairs = ''
if lang:
prefix = self.config.get('lang_prefix', 'language-')
lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"'
if classes:
class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"'
if id:
id_attr = f' id="{_escape_attrib_html(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(
f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
code = self._escape('code'))
code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
placeholder =
text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
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 == '.':
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)
configs[k] = v
return id, classes, configs
def _escape(self, txt):
""" basic html escaping """
txt = txt.replace('&', '&amp;')
txt = txt.replace('<', '&lt;')
txt = txt.replace('>', '&gt;')
txt = txt.replace('"', '&quot;')
return txt
def makeExtension(**kwargs): # pragma: no cover
return FencedCodeExtension(**kwargs)