• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Fenced Code Extension for Python Markdown
3=========================================
4
5This extension adds Fenced Code Blocks to Python-Markdown.
6
7See <https://Python-Markdown.github.io/extensions/fenced_code_blocks>
8for documentation.
9
10Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
11
12
13All changes Copyright 2008-2014 The Python Markdown Project
14
15License: [BSD](https://opensource.org/licenses/bsd-license.php)
16"""
17
18
19from textwrap import dedent
20from . import Extension
21from ..preprocessors import Preprocessor
22from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
23from .attr_list import get_attrs, AttrListExtension
24from ..util import parseBoolValue
25from ..serializers import _escape_attrib_html
26import re
27
28
29class FencedCodeExtension(Extension):
30    def __init__(self, **kwargs):
31        self.config = {
32            'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
33        }
34        super().__init__(**kwargs)
35
36    def extendMarkdown(self, md):
37        """ Add FencedBlockPreprocessor to the Markdown instance. """
38        md.registerExtension(self)
39
40        md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
41
42
43class FencedBlockPreprocessor(Preprocessor):
44    FENCED_BLOCK_RE = re.compile(
45        dedent(r'''
46            (?P<fence>^(?:~{3,}|`{3,}))[ ]*                          # opening fence
47            ((\{(?P<attrs>[^\}\n]*)\})|                              # (optional {attrs} or
48            (\.?(?P<lang>[\w#.+-]*)[ ]*)?                            # optional (.)lang
49            (hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
50            \n                                                       # newline (end of opening fence)
51            (?P<code>.*?)(?<=\n)                                     # the code block
52            (?P=fence)[ ]*$                                          # closing fence
53        '''),
54        re.MULTILINE | re.DOTALL | re.VERBOSE
55    )
56
57    def __init__(self, md, config):
58        super().__init__(md)
59        self.config = config
60        self.checked_for_deps = False
61        self.codehilite_conf = {}
62        self.use_attr_list = False
63        # List of options to convert to bool values
64        self.bool_options = [
65            'linenums',
66            'guess_lang',
67            'noclasses',
68            'use_pygments'
69        ]
70
71    def run(self, lines):
72        """ Match and store Fenced Code Blocks in the HtmlStash. """
73
74        # Check for dependent extensions
75        if not self.checked_for_deps:
76            for ext in self.md.registeredExtensions:
77                if isinstance(ext, CodeHiliteExtension):
78                    self.codehilite_conf = ext.getConfigs()
79                if isinstance(ext, AttrListExtension):
80                    self.use_attr_list = True
81
82            self.checked_for_deps = True
83
84        text = "\n".join(lines)
85        while 1:
86            m = self.FENCED_BLOCK_RE.search(text)
87            if m:
88                lang, id, classes, config = None, '', [], {}
89                if m.group('attrs'):
90                    id, classes, config = self.handle_attrs(get_attrs(m.group('attrs')))
91                    if len(classes):
92                        lang = classes.pop(0)
93                else:
94                    if m.group('lang'):
95                        lang = m.group('lang')
96                    if m.group('hl_lines'):
97                        # Support hl_lines outside of attrs for backward-compatibility
98                        config['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
99
100                # If config is not empty, then the codehighlite extension
101                # is enabled, so we call it to highlight the code
102                if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True):
103                    local_config = self.codehilite_conf.copy()
104                    local_config.update(config)
105                    # Combine classes with cssclass. Ensure cssclass is at end
106                    # as pygments appends a suffix under certain circumstances.
107                    # Ignore ID as Pygments does not offer an option to set it.
108                    if classes:
109                        local_config['css_class'] = '{} {}'.format(
110                            ' '.join(classes),
111                            local_config['css_class']
112                        )
113                    highliter = CodeHilite(
114                        m.group('code'),
115                        lang=lang,
116                        style=local_config.pop('pygments_style', 'default'),
117                        **local_config
118                    )
119
120                    code = highliter.hilite(shebang=False)
121                else:
122                    id_attr = lang_attr = class_attr = kv_pairs = ''
123                    if lang:
124                        prefix = self.config.get('lang_prefix', 'language-')
125                        lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"'
126                    if classes:
127                        class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"'
128                    if id:
129                        id_attr = f' id="{_escape_attrib_html(id)}"'
130                    if self.use_attr_list and config and not config.get('use_pygments', False):
131                        # Only assign key/value pairs to code element if attr_list ext is enabled, key/value pairs
132                        # were defined on the code block, and the `use_pygments` key was not set to True. The
133                        # `use_pygments` key could be either set to False or not defined. It is omitted from output.
134                        kv_pairs = ''.join(
135                            f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
136                        )
137                    code = self._escape(m.group('code'))
138                    code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
139
140                placeholder = self.md.htmlStash.store(code)
141                text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
142            else:
143                break
144        return text.split("\n")
145
146    def handle_attrs(self, attrs):
147        """ Return tuple: (id, [list, of, classes], {configs}) """
148        id = ''
149        classes = []
150        configs = {}
151        for k, v in attrs:
152            if k == 'id':
153                id = v
154            elif k == '.':
155                classes.append(v)
156            elif k == 'hl_lines':
157                configs[k] = parse_hl_lines(v)
158            elif k in self.bool_options:
159                configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True)
160            else:
161                configs[k] = v
162        return id, classes, configs
163
164    def _escape(self, txt):
165        """ basic html escaping """
166        txt = txt.replace('&', '&amp;')
167        txt = txt.replace('<', '&lt;')
168        txt = txt.replace('>', '&gt;')
169        txt = txt.replace('"', '&quot;')
170        return txt
171
172
173def makeExtension(**kwargs):  # pragma: no cover
174    return FencedCodeExtension(**kwargs)
175