summaryrefslogtreecommitdiffstats
path: root/nikola/plugins/compile/markdown
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/plugins/compile/markdown')
-rw-r--r--nikola/plugins/compile/markdown/__init__.py88
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.py241
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.py58
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.py87
4 files changed, 474 insertions, 0 deletions
diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py
new file mode 100644
index 0000000..b41c6b5
--- /dev/null
+++ b/nikola/plugins/compile/markdown/__init__.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on markdown."""
+
+from __future__ import unicode_literals
+
+import codecs
+import os
+import re
+
+try:
+ from markdown import markdown
+
+ from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension
+ nikola_extension = NikolaExtension()
+
+ from nikola.plugins.compile.markdown.mdx_gist import GistExtension
+ gist_extension = GistExtension()
+
+ from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension
+ podcast_extension = PodcastExtension()
+
+except ImportError:
+ markdown = None # NOQA
+ nikola_extension = None
+ gist_extension = None
+ podcast_extension = None
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
+
+
+class CompileMarkdown(PageCompiler):
+ """Compile markdown into HTML."""
+
+ name = "markdown"
+ extensions = [gist_extension, nikola_extension, podcast_extension]
+ site = None
+
+ def compile_html(self, source, dest, is_two_file=True):
+ if markdown is None:
+ req_missing(['markdown'], 'build this site (compile Markdown)')
+ makedirs(os.path.dirname(dest))
+ self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS")
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
+ output = markdown(data, self.extensions)
+ out_file.write(output)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py
new file mode 100644
index 0000000..3c3bef9
--- /dev/null
+++ b/nikola/plugins/compile/markdown/mdx_gist.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Warning: URL formats of "raw" gists are undocummented and subject to change.
+# See also: http://developer.github.com/v3/gists/
+#
+# Inspired by "[Python] reStructuredText GitHub Gist directive"
+# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
+'''
+Extension to Python Markdown for Embedded Gists (gist.github.com)
+
+Basic Example:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example with filename:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 zen.py]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example using reStructuredText syntax:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... .. gist:: 4747847 zen.py
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Error Case: non-existent Gist ID:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 0]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/0.js"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/0 --></noscript>
+ </div>
+ </p>
+
+Error Case: non-existent file:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 doesntexist.py]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/4747847/doesntexist.py --></noscript>
+ </div>
+ </p>
+
+'''
+from __future__ import unicode_literals, print_function
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import AtomicString
+from markdown.util import etree
+from nikola.utils import get_logger, req_missing, STDERR_HANDLER
+
+LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+GIST_JS_URL = "https://gist.github.com/{0}.js"
+GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
+GIST_RAW_URL = "https://gist.github.com/raw/{0}"
+GIST_FILE_RAW_URL = "https://gist.github.com/raw/{0}/{1}"
+
+GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\s*\]'
+GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$'
+
+
+class GistFetchException(Exception):
+ '''Raised when attempt to fetch content of a Gist from github.com fails.'''
+ def __init__(self, url, status_code):
+ Exception.__init__(self)
+ self.message = 'Received a {0} response from Gist URL: {1}'.format(
+ status_code, url)
+
+
+class GistPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, pattern)
+
+ def get_raw_gist_with_filename(self, gist_id, filename):
+ url = GIST_FILE_RAW_URL.format(gist_id, filename)
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
+
+ def get_raw_gist(self, gist_id):
+ url = GIST_RAW_URL.format(gist_id)
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
+
+ def handleMatch(self, m):
+ gist_id = m.group('gist_id')
+ gist_file = m.group('filename')
+
+ gist_elem = etree.Element('div')
+ gist_elem.set('class', 'gist')
+ script_elem = etree.SubElement(gist_elem, 'script')
+
+ if requests:
+ noscript_elem = etree.SubElement(gist_elem, 'noscript')
+
+ try:
+ if gist_file:
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+ raw_gist = (self.get_raw_gist_with_filename(
+ gist_id, gist_file))
+
+ else:
+ script_elem.set('src', GIST_JS_URL.format(
+ gist_id))
+ raw_gist = (self.get_raw_gist(gist_id))
+
+ # Insert source as <pre/> within <noscript>
+ pre_elem = etree.SubElement(noscript_elem, 'pre')
+ pre_elem.text = AtomicString(raw_gist)
+
+ except GistFetchException as e:
+ LOGGER.warn(e.message)
+ warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
+ noscript_elem.append(warning_comment)
+
+ else:
+ req_missing('requests', 'have inline gist source', optional=True)
+
+ return gist_elem
+
+
+class GistExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs())
+ gist_md_pattern.md = md
+ md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong")
+
+ gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs())
+ gist_rst_pattern.md = md
+ md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist")
+
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return GistExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+
+ # Silence user warnings thrown by tests:
+ doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
+ doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py
new file mode 100644
index 0000000..b0ad2f7
--- /dev/null
+++ b/nikola/plugins/compile/markdown/mdx_nikola.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Markdown Extension for Nikola-specific post-processing"""
+from __future__ import unicode_literals
+import re
+from markdown.postprocessors import Postprocessor
+from markdown.extensions import Extension
+
+
+class NikolaPostProcessor(Postprocessor):
+ def run(self, text):
+ output = text
+ # h1 is reserved for the title so increment all header levels
+ for n in reversed(range(1, 9)):
+ output = re.sub('<h%i>' % n, '<h%i>' % (n + 1), output)
+ output = re.sub('</h%i>' % n, '</h%i>' % (n + 1), output)
+
+ # python-markdown's highlighter uses the class 'codehilite' to wrap
+ # code, instead of the standard 'code'. None of the standard
+ # pygments stylesheets use this class, so swap it to be 'code'
+ output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
+ r'\1code\2', output)
+ return output
+
+
+class NikolaExtension(Extension):
+ def extendMarkdown(self, md, md_globals):
+ pp = NikolaPostProcessor()
+ md.postprocessors.add('nikola_post_processor', pp, '_end')
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return NikolaExtension(configs)
diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py
new file mode 100644
index 0000000..be8bb6b
--- /dev/null
+++ b/nikola/plugins/compile/markdown/mdx_podcast.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt, Roberto Alsina
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Inspired by "[Python] reStructuredText GitHub Podcast directive"
+# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
+
+from __future__ import print_function, unicode_literals
+
+
+'''
+Extension to Python Markdown for Embedded Audio
+
+Basic Example:
+
+>>> import markdown
+>>> text = """[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]"""
+>>> html = markdown.markdown(text, [PodcastExtension()])
+>>> print(html)
+<p><audio src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3"></audio></p>
+'''
+
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import etree
+
+PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]'
+
+
+class PodcastPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, pattern)
+
+ def handleMatch(self, m):
+ url = m.group('url').strip()
+ audio_elem = etree.Element('audio')
+ audio_elem.set('controls', '')
+ source_elem = etree.SubElement(audio_elem, 'source')
+ source_elem.set('src', url)
+ source_elem.set('type', 'audio/mpeg')
+ return audio_elem
+
+
+class PodcastExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs())
+ podcast_md_pattern.md = md
+ md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong")
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return PodcastExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
+ doctest.REPORT_NDIFF))