| 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"><p>block two</p>\\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('&', '&') |
| 144 |
txt = txt.replace('<', '<') |
| 145 |
txt = txt.replace('>', '>') |
| 146 |
txt = txt.replace('"', '"') |
| 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() |