aboutsummaryrefslogtreecommitdiff
path: root/markdown/extensions/fenced_code.py
diff options
context:
space:
mode:
Diffstat (limited to 'markdown/extensions/fenced_code.py')
-rw-r--r--markdown/extensions/fenced_code.py223
1 files changed, 140 insertions, 83 deletions
diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py
index 307b1dc..409166a 100644
--- a/markdown/extensions/fenced_code.py
+++ b/markdown/extensions/fenced_code.py
@@ -1,104 +1,166 @@
-#!/usr/bin/env python
-
"""
Fenced Code Extension for Python Markdown
=========================================
This extension adds Fenced Code Blocks to Python-Markdown.
- >>> import markdown
- >>> text = '''
- ... A paragraph before a fenced code block:
- ...
- ... ~~~
- ... Fenced code block
- ... ~~~
- ... '''
- >>> html = markdown.markdown(text, extensions=['fenced_code'])
- >>> html
- u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>'
-
-Works with safe_mode also (we check this because we are using the HtmlStash):
-
- >>> markdown.markdown(text, extensions=['fenced_code'], safe_mode='replace')
- u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>'
-
-Include tilde's in a code block and wrap with blank lines:
-
- >>> text = '''
- ... ~~~~~~~~
- ...
- ... ~~~~
- ...
- ... ~~~~~~~~'''
- >>> markdown.markdown(text, extensions=['fenced_code'])
- u'<pre><code>\\n~~~~\\n\\n</code></pre>'
-
-Multiple blocks and language tags:
-
- >>> text = '''
- ... ~~~~{.python}
- ... block one
- ... ~~~~
- ...
- ... ~~~~.html
- ... <p>block two</p>
- ... ~~~~'''
- >>> markdown.markdown(text, extensions=['fenced_code'])
- u'<pre><code class="python">block one\\n</code></pre>\\n\\n<pre><code class="html">&lt;p&gt;block two&lt;/p&gt;\\n</code></pre>'
-
-Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
-
-Project website: <http://www.freewisdom.org/project/python-markdown/Fenced__Code__Blocks>
-Contact: markdown@freewisdom.org
-
-License: BSD (see ../docs/LICENSE for details)
-
-Dependencies:
-* [Python 2.3+](http://python.org)
-* [Markdown 2.0+](http://www.freewisdom.org/projects/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)
"""
-import markdown, re
-# Global vars
-FENCED_BLOCK_RE = re.compile( \
- r'(?P<fence>^~{3,})[ ]*(\{?\.(?P<lang>[a-zA-Z0-9_-]*)\}?)?[ ]*\n(?P<code>.*?)(?P=fence)[ ]*$',
- re.MULTILINE|re.DOTALL
- )
-CODE_WRAP = '<pre><code%s>%s</code></pre>'
-LANG_TAG = ' class="%s"'
+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(markdown.Extension):
+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, md_globals):
+ 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
+ )
- md.preprocessors.add('fenced_code_block',
- FencedBlockPreprocessor(md),
- "_begin")
-
+ 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'
+ ]
-class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
-
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 = FENCED_BLOCK_RE.search(text)
+ m = self.FENCED_BLOCK_RE.search(text)
if m:
- lang = ''
- if m.group('lang'):
- lang = LANG_TAG % m.group('lang')
- code = CODE_WRAP % (lang, self._escape(m.group('code')))
- placeholder = self.markdown.htmlStash.store(code, safe=True)
- text = '%s\n%s\n%s'% (text[:m.start()], placeholder, text[m.end():])
+ 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(shebang=False)
+ else:
+ 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(m.group('code'))
+ code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
+
+ placeholder = self.md.htmlStash.store(code)
+ text = f'{text[:m.start()]}\n{placeholder}\n{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('&', '&amp;')
@@ -108,10 +170,5 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
return txt
-def makeExtension(configs=None):
- return FencedCodeExtension()
-
-
-if __name__ == "__main__":
- import doctest
- doctest.testmod()
+def makeExtension(**kwargs): # pragma: no cover
+ return FencedCodeExtension(**kwargs)