1
#!/usr/bin/env python
2
3
"""
4
Fenced Code Extension for Python Markdown
5
=========================================
6
7
This 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
21
Works 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
26
Include 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
37
Multiple 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
50
Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
51
52
Project website: <http://www.freewisdom.org/project/python-markdown/Fenced__Code__Blocks>
53
Contact: markdown@freewisdom.org
54
55
License: BSD (see ../docs/LICENSE for details)
56
57
Dependencies:
58
* [Python 2.4+](http://python.org)
59
* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/)
60
* [Pygments (optional)](http://pygments.org)
61
62
"""
63
64
import re
65
import markdown
66
from markdown.extensions.codehilite import CodeHilite, CodeHiliteExtension
67
68
# Global vars
69
FENCED_BLOCK_RE = re.compile( \
70
    r'(?P<fence>^~{3,})[ ]*(\{?\.(?P<lang>[a-zA-Z0-9_-]*)\}?)?[ ]*\n(?P<code>.*?)(?P=fence)[ ]*$',
71
    re.MULTILINE|re.DOTALL
72
    )
73
CODE_WRAP = '<pre><code%s>%s</code></pre>'
74
LANG_TAG = ' class="%s"'
75
76
class FencedCodeExtension(markdown.Extension):
77
78
    def extendMarkdown(self, md, md_globals):
79
        """ Add FencedBlockPreprocessor to the Markdown instance. """
80
        md.registerExtension(self)
81
82
        md.preprocessors.add('fenced_code_block',
83
                                 FencedBlockPreprocessor(md),
84
                                 "_begin")
85
86
87
class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
88
89
    def __init__(self, md):
90
        markdown.preprocessors.Preprocessor.__init__(self, md)
91
92
        self.checked_for_codehilite = False
93
        self.codehilite_conf = {}
94
95
    def getConfig(self, key):
96
        if key in self.config:
97
            return self.config[key][0]
98
        else:
99
            return None
100
101
    def run(self, lines):
102
        """ Match and store Fenced Code Blocks in the HtmlStash. """
103
104
        # Check for code hilite extension
105
        if not self.checked_for_codehilite:
106
            for ext in self.markdown.registeredExtensions:
107
                if isinstance(ext, CodeHiliteExtension):
108
                    self.codehilite_conf = ext.config
109
                    break
110
111
            self.checked_for_codehilite = True
112
113
        text = "\n".join(lines)
114
        while 1:
115
            m = FENCED_BLOCK_RE.search(text)
116
            if m:
117
                lang = ''
118
                if m.group('lang'):
119
                    lang = LANG_TAG % m.group('lang')
120
121
                # If config is not empty, then the codehighlite extension
122
                # is enabled, so we call it to highlite the code
123
                if self.codehilite_conf:
124
                    highliter = CodeHilite(m.group('code'),
125
                            linenos=self.codehilite_conf['force_linenos'][0],
126
                            css_class=self.codehilite_conf['css_class'][0],
127
                            style=self.codehilite_conf['pygments_style'][0],
128
                            lang=(m.group('lang') or None),
129
                            noclasses=self.codehilite_conf['noclasses'][0])
130
131
                    code = highliter.hilite()
132
                else:
133
                    code = CODE_WRAP % (lang, self._escape(m.group('code')))
134
135
                placeholder = self.markdown.htmlStash.store(code, safe=True)
136
                text = '%s\n%s\n%s'% (text[:m.start()], placeholder, text[m.end():])
137
            else:
138
                break
139
        return text.split("\n")
140
141
    def _escape(self, txt):
142
        """ basic html escaping """
143
        txt = txt.replace('&', '&amp;')
144
        txt = txt.replace('<', '&lt;')
145
        txt = txt.replace('>', '&gt;')
146
        txt = txt.replace('"', '&quot;')
147
        return txt
148
149
150
def makeExtension(configs=None):
151
    return FencedCodeExtension(configs=configs)
152
153
154
if __name__ == "__main__":
155
    import doctest
156
    doctest.testmod()