1#!/usr/bin/env python
2
3"""
4Fenced Code Extension for Python Markdown
5=========================================
6
7This extension adds Fenced Code Blocks to Python-Markdown.
8
9    >>> import markdown
10    >>> text = '''
11    ... A paragraph before a fenced code block:
12    ...
13    ... ~~~
14    ... Fenced code block
15    ... ~~~
16    ... '''
17    >>> html = markdown.markdown(text, extensions=['fenced_code'])
18    >>> html
19    u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>'
20
21Works with safe_mode also (we check this because we are using the HtmlStash):
22
23    >>> markdown.markdown(text, extensions=['fenced_code'], safe_mode='replace')
24    u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>'
25
26Include tilde's in a code block and wrap with blank lines:
27
28    >>> text = '''
29    ... ~~~~~~~~
30    ...
31    ... ~~~~
32    ...
33    ... ~~~~~~~~'''
34    >>> markdown.markdown(text, extensions=['fenced_code'])
35    u'<pre><code>\\n~~~~\\n\\n</code></pre>'
36
37Multiple blocks and language tags:
38
39    >>> text = '''
40    ... ~~~~{.python}
41    ... block one
42    ... ~~~~
43    ...
44    ... ~~~~.html
45    ... <p>block two</p>
46    ... ~~~~'''
47    >>> markdown.markdown(text, extensions=['fenced_code'])
48    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>'
49
50Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
51
52Project website: <http://www.freewisdom.org/project/python-markdown/Fenced__Code__Blocks>
53Contact: markdown@freewisdom.org
54
55License: BSD (see ../docs/LICENSE for details)
56
57Dependencies:
58* [Python 2.3+](http://python.org)
59* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/)
60
61"""
62
63import markdown, re
64
65# Global vars
66FENCED_BLOCK_RE = re.compile( \
67    r'(?P<fence>^~{3,})[ ]*(\{?\.(?P<lang>[a-zA-Z0-9_-]*)\}?)?[ ]*\n(?P<code>.*?)(?P=fence)[ ]*$',
68    re.MULTILINE|re.DOTALL
69    )
70CODE_WRAP = '<pre><code%s>%s</code></pre>'
71LANG_TAG = ' class="%s"'
72
73
74class FencedCodeExtension(markdown.Extension):
75
76    def extendMarkdown(self, md, md_globals):
77        """ Add FencedBlockPreprocessor to the Markdown instance. """
78
79        md.preprocessors.add('fenced_code_block',
80                                 FencedBlockPreprocessor(md),
81                                 "_begin")
82
83
84class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
85
86    def run(self, lines):
87        """ Match and store Fenced Code Blocks in the HtmlStash. """
88        text = "\n".join(lines)
89        while 1:
90            m = FENCED_BLOCK_RE.search(text)
91            if m:
92                lang = ''
93                if m.group('lang'):
94                    lang = LANG_TAG % m.group('lang')
95                code = CODE_WRAP % (lang, self._escape(m.group('code')))
96                placeholder = self.markdown.htmlStash.store(code, safe=True)
97                text = '%s\n%s\n%s'% (text[:m.start()], placeholder, text[m.end():])
98            else:
99                break
100        return text.split("\n")
101
102    def _escape(self, txt):
103        """ basic html escaping """
104        txt = txt.replace('&', '&amp;')
105        txt = txt.replace('<', '&lt;')
106        txt = txt.replace('>', '&gt;')
107        txt = txt.replace('"', '&quot;')
108        return txt
109
110
111def makeExtension(configs=None):
112    return FencedCodeExtension()
113
114
115if __name__ == "__main__":
116    import doctest
117    doctest.testmod()
118