1# markdown is released under the BSD license
2# Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
3# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
4# Copyright 2004 Manfred Stienstra (the original version)
5#
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11# *   Redistributions of source code must retain the above copyright
12#     notice, this list of conditions and the following disclaimer.
13# *   Redistributions in binary form must reproduce the above copyright
14#     notice, this list of conditions and the following disclaimer in the
15#     documentation and/or other materials provided with the distribution.
16# *   Neither the name of the <organization> nor the
17#     names of its contributors may be used to endorse or promote products
18#     derived from this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY
21# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23# DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT
24# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31
32
33"""
34Fenced Code Extension for Python Markdown
35=========================================
36
37This extension adds Fenced Code Blocks to Python-Markdown.
38
39    >>> import markdown
40    >>> text = '''
41    ... A paragraph before a fenced code block:
42    ...
43    ... ~~~
44    ... Fenced code block
45    ... ~~~
46    ... '''
47    >>> html = markdown.markdown(text, extensions=['fenced_code'])
48    >>> print html
49    <p>A paragraph before a fenced code block:</p>
50    <pre><code>Fenced code block
51    </code></pre>
52
53Works with safe_mode also (we check this because we are using the HtmlStash):
54
55    >>> print markdown.markdown(text, extensions=['fenced_code'], safe_mode='replace')
56    <p>A paragraph before a fenced code block:</p>
57    <pre><code>Fenced code block
58    </code></pre>
59
60Include tilde's in a code block and wrap with blank lines:
61
62    >>> text = '''
63    ... ~~~~~~~~
64    ...
65    ... ~~~~
66    ... ~~~~~~~~'''
67    >>> print markdown.markdown(text, extensions=['fenced_code'])
68    <pre><code>
69    ~~~~
70    </code></pre>
71
72Language tags:
73
74    >>> text = '''
75    ... ~~~~{.python}
76    ... # Some python code
77    ... ~~~~'''
78    >>> print markdown.markdown(text, extensions=['fenced_code'])
79    <pre><code class="python"># Some python code
80    </code></pre>
81
82Optionally backticks instead of tildes as per how github's code block markdown is identified:
83
84    >>> text = '''
85    ... `````
86    ... # Arbitrary code
87    ... ~~~~~ # these tildes will not close the block
88    ... `````'''
89    >>> print markdown.markdown(text, extensions=['fenced_code'])
90    <pre><code># Arbitrary code
91    ~~~~~ # these tildes will not close the block
92    </code></pre>
93
94Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
95
96Project website: <http://packages.python.org/Markdown/extensions/fenced_code_blocks.html>
97Contact: markdown@freewisdom.org
98
99License: BSD (see ../docs/LICENSE for details)
100
101Dependencies:
102* [Python 2.4+](http://python.org)
103* [Markdown 2.0+](http://packages.python.org/Markdown/)
104* [Pygments (optional)](http://pygments.org)
105
106"""
107
108from __future__ import absolute_import
109from __future__ import unicode_literals
110from . import Extension
111from ..preprocessors import Preprocessor
112from .codehilite import CodeHilite, CodeHiliteExtension
113import re
114
115# Global vars
116FENCED_BLOCK_RE = re.compile( \
117    r'(?P<fence>^(?:~{3,}|`{3,}))[ ]*(\{?\.?(?P<lang>[a-zA-Z0-9_+-]*)\}?)?[ ]*\n(?P<code>.*?)(?<=\n)(?P=fence)[ ]*$',
118    re.MULTILINE|re.DOTALL
119    )
120CODE_WRAP = '<pre><code%s>%s</code></pre>'
121LANG_TAG = ' class="%s"'
122
123class FencedCodeExtension(Extension):
124
125    def extendMarkdown(self, md, md_globals):
126        """ Add FencedBlockPreprocessor to the Markdown instance. """
127        md.registerExtension(self)
128
129        md.preprocessors.add('fenced_code_block',
130                                 FencedBlockPreprocessor(md),
131                                 ">normalize_whitespace")
132
133
134class FencedBlockPreprocessor(Preprocessor):
135
136    def __init__(self, md):
137        super(FencedBlockPreprocessor, self).__init__(md)
138
139        self.checked_for_codehilite = False
140        self.codehilite_conf = {}
141
142    def run(self, lines):
143        """ Match and store Fenced Code Blocks in the HtmlStash. """
144
145        # Check for code hilite extension
146        if not self.checked_for_codehilite:
147            for ext in self.markdown.registeredExtensions:
148                if isinstance(ext, CodeHiliteExtension):
149                    self.codehilite_conf = ext.config
150                    break
151
152            self.checked_for_codehilite = True
153
154        text = "\n".join(lines)
155        while 1:
156            m = FENCED_BLOCK_RE.search(text)
157            if m:
158                lang = ''
159                if m.group('lang'):
160                    lang = LANG_TAG % m.group('lang')
161
162                # If config is not empty, then the codehighlite extension
163                # is enabled, so we call it to highlite the code
164                if self.codehilite_conf:
165                    highliter = CodeHilite(m.group('code'),
166                            linenums=self.codehilite_conf['linenums'][0],
167                            guess_lang=self.codehilite_conf['guess_lang'][0],
168                            css_class=self.codehilite_conf['css_class'][0],
169                            style=self.codehilite_conf['pygments_style'][0],
170                            lang=(m.group('lang') or None),
171                            noclasses=self.codehilite_conf['noclasses'][0])
172
173                    code = highliter.hilite()
174                else:
175                    code = CODE_WRAP % (lang, self._escape(m.group('code')))
176
177                placeholder = self.markdown.htmlStash.store(code, safe=True)
178                text = '%s\n%s\n%s'% (text[:m.start()], placeholder, text[m.end():])
179            else:
180                break
181        return text.split("\n")
182
183    def _escape(self, txt):
184        """ basic html escaping """
185        txt = txt.replace('&', '&amp;')
186        txt = txt.replace('<', '&lt;')
187        txt = txt.replace('>', '&gt;')
188        txt = txt.replace('"', '&quot;')
189        return txt
190
191
192def makeExtension(configs=None):
193    return FencedCodeExtension(configs=configs)
194