ソースを参照

attempt to merge master with collapsible code

Jörg Dietrich 10 年 前
コミット
42f90c94fb
共有42 個のファイルを変更した2556 個の追加152 個の削除を含む
  1. 35 0
      creole_reader/Readme.md
  2. 1 0
      creole_reader/__init__.py
  3. 56 0
      creole_reader/creole_reader.py
  4. 5 0
      github_activity/Readme.rst
  5. 2 1
      github_activity/github_activity.py
  6. 1 0
      latex
  7. 0 75
      latex/Readme.md
  8. 0 1
      latex/__init__.py
  9. 0 55
      latex/latex.py
  10. 11 2
      liquid_tags/Readme.md
  11. 38 10
      liquid_tags/notebook.py
  12. 54 0
      liquid_tags/vimeo.py
  13. 8 7
      neighbors/neighbors.py
  14. 31 0
      pelican_comment_system/Readme.md
  15. 1 0
      pelican_comment_system/__init__.py
  16. 94 0
      pelican_comment_system/avatars.py
  17. 45 0
      pelican_comment_system/comment.py
  18. 35 0
      pelican_comment_system/doc/avatars.md
  19. 28 0
      pelican_comment_system/doc/feed.md
  20. 83 0
      pelican_comment_system/doc/form.md
  21. 106 0
      pelican_comment_system/doc/installation.md
  22. 11 0
      pelican_comment_system/identicon/LICENSE
  23. 17 0
      pelican_comment_system/identicon/README.md
  24. 0 0
      pelican_comment_system/identicon/__init__.py
  25. 256 0
      pelican_comment_system/identicon/identicon.py
  26. 121 0
      pelican_comment_system/pelican_comment_system.py
  27. 173 0
      render_math/Readme.md
  28. 1 0
      render_math/__init__.py
  29. 379 0
      render_math/math.py
  30. 28 0
      render_math/mathjax_script.txt
  31. 26 0
      simple_footnotes/README.md
  32. 1 0
      simple_footnotes/__init__.py
  33. 91 0
      simple_footnotes/simple_footnotes.py
  34. 33 0
      simple_footnotes/test_simple_footnotes.py
  35. 26 0
      static_comments/Readme.md
  36. 1 0
      static_comments/__init__.py
  37. 46 0
      static_comments/static_comments.py
  38. 1 1
      subcategory/README.md
  39. 118 0
      twitter_bootstrap_rst_directives/Demo.rst
  40. 46 0
      twitter_bootstrap_rst_directives/Readme.rst
  41. 4 0
      twitter_bootstrap_rst_directives/__init__.py
  42. 543 0
      twitter_bootstrap_rst_directives/bootstrap_rst_directives.py

+ 35 - 0
creole_reader/Readme.md

@@ -0,0 +1,35 @@
+# Creole Reader
+
+This plugins allows you to write your posts using the wikicreole syntax. Give to
+these files the creole extension. The medata are between `<<header>> <</header>>`
+tags.
+
+## Dependency
+This plugin relies on [python-creole](https://pypi.python.org/pypi/python-creole/) to work. Install it with:
+`pip install python-creole`
+
+## Syntax
+Use ** for strong, // for emphasis, one = for 1st level titles.
+
+For the complete syntax, look at: http://www.wikicreole.org/
+
+## Basic example
+```
+<<header>>
+title: Créole
+tags: creole, python, pelican_open
+date: 2013-12-12
+<</header>>
+
+= Title 1
+== Title 2
+
+Some nice texte with **strong** and //emphasis//.
+
+* A nice list
+** With subelements
+* Python
+
+# An ordered list
+# A second item
+```

+ 1 - 0
creole_reader/__init__.py

@@ -0,0 +1 @@
+from .creole_reader import *

+ 56 - 0
creole_reader/creole_reader.py

@@ -0,0 +1,56 @@
+#-*- conding: utf-8 -*-
+
+'''
+Creole Reader
+-------------
+
+This plugins allows you to write your posts using the wikicreole syntax. Give to
+these files the creole extension.
+For the syntax, look at: http://www.wikicreole.org/
+'''
+
+from pelican import readers
+from pelican import signals
+from pelican import settings
+
+from pelican.utils import pelican_open
+
+try:
+    from creole import creole2html
+    creole = True
+except ImportError:
+    creole = False
+
+class CreoleReader(readers.BaseReader):
+    enabled = creole
+
+    file_extensions = ['creole']
+
+    def __init__(self, settings):
+        super(CreoleReader, self).__init__(settings)
+
+    def _parse_header_macro(self, text):
+        for line in text.split('\n'):
+            name, value = line.split(':')
+            name, value = name.strip(), value.strip()
+            if name == 'title':
+                self._metadata[name] = value
+            else:
+                self._metadata[name] = self.process_metadata(name, value)
+        return u''
+
+    # You need to have a read method, which takes a filename and returns
+    # some content and the associated metadata.
+    def read(self, source_path):
+        """Parse content and metadata of creole files"""
+
+        self._metadata = {}
+        with pelican_open(source_path) as text:
+            content = creole2html(text, macros={'header': self._parse_header_macro})
+        return content, self._metadata
+
+def add_reader(readers):
+    readers.reader_classes['creole'] = CreoleReader
+
+def register():
+    signals.readers_init.connect(add_reader)

+ 5 - 0
github_activity/Readme.rst

@@ -9,6 +9,11 @@ For example, to track Pelican project activity, the setting would be::
 
 
      GITHUB_ACTIVITY_FEED = 'https://github.com/getpelican.atom'
      GITHUB_ACTIVITY_FEED = 'https://github.com/getpelican.atom'
 
 
+If you want to limit the amount of entries to a certain maximum set the
+``GITHUB_ACTIVITY_MAX_ENTRIES`` parameter.
+
+     GITHUB_ACTIVITY_MAX_ENTRIES = 10
+
 On the template side, you just have to iterate over the ``github_activity``
 On the template side, you just have to iterate over the ``github_activity``
 variable, as in this example::
 variable, as in this example::
 
 

+ 2 - 1
github_activity/github_activity.py

@@ -25,6 +25,7 @@ class GitHubActivity():
         import feedparser
         import feedparser
         self.activities = feedparser.parse(
         self.activities = feedparser.parse(
             generator.settings['GITHUB_ACTIVITY_FEED'])
             generator.settings['GITHUB_ACTIVITY_FEED'])
+        self.max_entries = generator.settings['GITHUB_ACTIVITY_MAX_ENTRIES'] 
 
 
     def fetch(self):
     def fetch(self):
         """
         """
@@ -37,7 +38,7 @@ class GitHubActivity():
                     [element for element in [activity['title'],
                     [element for element in [activity['title'],
                         activity['content'][0]['value']]])
                         activity['content'][0]['value']]])
 
 
-        return entries
+        return entries[0:self.max_entries]
 
 
 
 
 def fetch_github_activity(gen, metadata):
 def fetch_github_activity(gen, metadata):

+ 1 - 0
latex

@@ -0,0 +1 @@
+render_math/

+ 0 - 75
latex/Readme.md

@@ -1,75 +0,0 @@
-Latex Plugin For Pelican
-========================
-
-This plugin allows you to write mathematical equations in your articles using Latex.
-It uses the MathJax Latex JavaScript library to render latex that is embedded in
-between `$..$` for inline math and `$$..$$` for displayed math. It also allows for 
-writing equations in by using `\begin{equation}`...`\end{equation}`.
-
-Installation
-------------
-
-To enable, ensure that `latex.py` is put somewhere that is accessible.
-Then use as follows by adding the following to your settings.py:
-
-    PLUGINS = ["latex"]
-
-Be careful: Not loading the plugin is easy to do, and difficult to detect. To
-make life easier, find where pelican is installed, and then copy the plugin
-there. An easy way to find where pelican is installed is to verbose list the
-available themes by typing `pelican-themes -l -v`. 
-
-Once the pelican folder is found, copy `latex.py` to the `plugins` folder. Then 
-add to settings.py like this:
-
-    PLUGINS = ["pelican.plugins.latex"]
-
-Now all that is left to do is to embed the following to your template file 
-between the `<head>` parameters (for the NotMyIdea template, this file is base.html)
-
-    {% if article and article.latex %}
-        {{ article.latex }}
-    {% endif %}
-    {% if page and page.latex %}
-        {{ page.latex }}
-    {% endif %}
-
-Usage
------
-Latex will be embedded in every article. If however you want latex only for
-selected articles, then in settings.py, add
-
-    LATEX = 'article'
-
-And in each article, add the metadata key `latex:`. For example, with the above
-settings, creating an article that I want to render latex math, I would just 
-include 'Latex' as part of the metadata without any value:
-
-    Date: 1 sep 2012
-    Status: draft
-    Latex:
-
-Latex Examples
---------------
-###Inline
-Latex between `$`..`$`, for example, `$`x^2`$`, will be rendered inline 
-with respect to the current html block.
-
-###Displayed Math
-Latex between `$$`..`$$`, for example, `$$`x^2`$$`, will be rendered centered in a 
-new paragraph.
-
-###Equations
-Latex between `\begin` and `\end`, for example, `begin{equation}` x^2 `\end{equation}`, 
-will be rendered centered in a new paragraph with a right justified equation number 
-at the top of the paragraph. This equation number can be referenced in the document. 
-To do this, use a `label` inside of the equation format and then refer to that label 
-using `ref`. For example: `begin{equation}` `\label{eq}` X^2 `\end{equation}`. Now 
-refer to that equation number by `$`\ref{eq}`$`.
-   
-Template And Article Examples
------------------------------
-To see an example of this plugin in action, look at 
-[this article](http://doctrina.org/How-RSA-Works-With-Examples.html). To see how 
-this plugin works with a template, look at 
-[this template](https://github.com/barrysteyn/pelican_theme-personal_blog).

+ 0 - 1
latex/__init__.py

@@ -1 +0,0 @@
-from .latex import *

+ 0 - 55
latex/latex.py

@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Latex Plugin For Pelican
-========================
-
-This plugin allows you to write mathematical equations in your articles using Latex.
-It uses the MathJax Latex JavaScript library to render latex that is embedded in
-between `$..$` for inline math and `$$..$$` for displayed math. It also allows for 
-writing equations in by using `\begin{equation}`...`\end{equation}`.
-"""
-
-from pelican import signals
-
-# Reference about dynamic loading of MathJax can be found at http://docs.mathjax.org/en/latest/dynamic.html
-# The https cdn address can be found at http://www.mathjax.org/resources/faqs/#problem-https
-latexScript = """
-    <script type= "text/javascript">
-        var s = document.createElement('script');
-        s.type = 'text/javascript';
-        s.src = 'https:' == document.location.protocol ? 'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js' : 'http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'; 
-        s[(window.opera ? "innerHTML" : "text")] =
-            "MathJax.Hub.Config({" + 
-            "    config: ['MMLorHTML.js']," + 
-            "    jax: ['input/TeX','input/MathML','output/HTML-CSS','output/NativeMML']," +
-            "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," + 
-            "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
-            "    tex2jax: { " +
-            "        inlineMath: [ [\'$\',\'$\'] ], " +
-            "        displayMath: [ [\'$$\',\'$$\'] ]," +
-            "        processEscapes: true }, " +
-            "    'HTML-CSS': { " +
-            "        styles: { '.MathJax .mo, .MathJax .mi': {color: 'black ! important'}} " +
-            "    } " +
-            "}); ";
-        (document.body || document.getElementsByTagName('head')[0]).appendChild(s);
-    </script>
-"""
-
-def addLatex(gen, metadata):
-    """
-        The registered handler for the latex plugin. It will add 
-        the latex script to the article metadata
-    """
-    if 'LATEX' in gen.settings.keys() and gen.settings['LATEX'] == 'article':
-        if 'latex' in metadata.keys():
-            metadata['latex'] = latexScript
-    else:
-        metadata['latex'] = latexScript
-
-def register():
-    """
-        Plugin registration
-    """
-    signals.article_generator_context.connect(addLatex)
-    signals.page_generator_context.connect(addLatex)

+ 11 - 2
liquid_tags/Readme.md

@@ -14,8 +14,8 @@ First, in your pelicanconf.py file, add the plugins you want to  use:
 
 
     PLUGIN_PATH = '/path/to/pelican-plugins'
     PLUGIN_PATH = '/path/to/pelican-plugins'
     PLUGINS = ['liquid_tags.img', 'liquid_tags.video',
     PLUGINS = ['liquid_tags.img', 'liquid_tags.video',
-               'liquid_tags.youtube', 'liquid_tags.include_code',
-               'liquid_tags.notebook']
+               'liquid_tags.youtube', 'liquid_tags.vimeo',
+               'liquid_tags.include_code', 'liquid_tags.notebook']
 
 
 There are several options available
 There are several options available
 
 
@@ -34,6 +34,15 @@ To insert youtube video into a post, enable the
 The width and height are in pixels, and can be optionally specified.  If they
 The width and height are in pixels, and can be optionally specified.  If they
 are not, then the dimensions will be 640 (wide) by 390 (tall).
 are not, then the dimensions will be 640 (wide) by 390 (tall).
 
 
+## Vimeo Tag
+To insert a Vimeo video into a post, enable the
+``liquid_tags.vimeo`` plugin, and add to your document:
+
+    {% vimeo vimeo_id [width] [height] %}
+
+The width and height are in pixels, and can be optionally specified.  If they
+are not, then the dimensions will be 640 (wide) by 390 (tall).
+
 ## Video Tag
 ## Video Tag
 To insert flash/HTML5-friendly video into a post, enable the
 To insert flash/HTML5-friendly video into a post, enable the
 ``liquid_tags.video`` plugin, and add to your document:
 ``liquid_tags.video`` plugin, and add to your document:

+ 38 - 10
liquid_tags/notebook.py

@@ -55,7 +55,12 @@ if not LooseVersion(IPython.__version__) >= '1.0':
 
 
 from IPython import nbconvert
 from IPython import nbconvert
 
 
-from IPython.nbconvert.filters.highlight import _pygment_highlight
+try:
+    from IPython.nbconvert.filters.highlight import _pygments_highlight
+except ImportError:
+    # IPython < 2.0
+    from IPython.nbconvert.filters.highlight import _pygment_highlight as _pygments_highlight
+
 from pygments.formatters import HtmlFormatter
 from pygments.formatters import HtmlFormatter
 
 
 from IPython.nbconvert.exporters import HTMLExporter
 from IPython.nbconvert.exporters import HTMLExporter
@@ -64,9 +69,10 @@ from IPython.config import Config
 from IPython.nbformat import current as nbformat
 from IPython.nbformat import current as nbformat
 
 
 try:
 try:
-    from IPython.nbconvert.transformers import Transformer
+    from IPython.nbconvert.preprocessors import Preprocessor
 except ImportError:
 except ImportError:
-    raise ValueError("IPython version 2.0 is not yet supported")
+    # IPython < 2.0
+    from IPython.nbconvert.transformers import Transformer as Preprocessor
 
 
 from IPython.utils.traitlets import Integer
 from IPython.utils.traitlets import Integer
 from copy import deepcopy
 from copy import deepcopy
@@ -111,6 +117,17 @@ pre.ipynb {
   font-size: 13px;
   font-size: 13px;
 }
 }
 
 
+/* remove the prompt div from text cells */
+div.text_cell .prompt {
+    display: none;
+}
+
+/* remove horizontal padding from text cells, */
+/* so it aligns with outer body text */
+div.text_cell_render {
+    padding: 0.5em 0em;
+}
+
 img.anim_icon{padding:0; border:0; vertical-align:middle; -webkit-box-shadow:none; -box-shadow:none}
 img.anim_icon{padding:0; border:0; vertical-align:middle; -webkit-box-shadow:none; -box-shadow:none}
 
 
 div.collapseheader {
 div.collapseheader {
@@ -169,7 +186,7 @@ CSS_WRAPPER = """
 
 
 
 
 #----------------------------------------------------------------------
 #----------------------------------------------------------------------
-# Create a custom transformer
+# Create a custom preprocessor
 class SliceIndex(Integer):
 class SliceIndex(Integer):
     """An integer trait that accepts None"""
     """An integer trait that accepts None"""
     default_value = None
     default_value = None
@@ -181,28 +198,32 @@ class SliceIndex(Integer):
             return super(SliceIndex, self).validate(obj, value)
             return super(SliceIndex, self).validate(obj, value)
 
 
 
 
-class SubCell(Transformer):
+class SubCell(Preprocessor):
     """A transformer to select a slice of the cells of a notebook"""
     """A transformer to select a slice of the cells of a notebook"""
     start = SliceIndex(0, config=True,
     start = SliceIndex(0, config=True,
                        help="first cell of notebook to be converted")
                        help="first cell of notebook to be converted")
     end = SliceIndex(None, config=True,
     end = SliceIndex(None, config=True,
                      help="last cell of notebook to be converted")
                      help="last cell of notebook to be converted")
 
 
-    def call(self, nb, resources):
+    def preprocess(self, nb, resources):
         nbc = deepcopy(nb)
         nbc = deepcopy(nb)
-        for worksheet in nbc.worksheets :
+        for worksheet in nbc.worksheets:
             cells = worksheet.cells[:]
             cells = worksheet.cells[:]
             worksheet.cells = cells[self.start:self.end]
             worksheet.cells = cells[self.start:self.end]
         return nbc, resources
         return nbc, resources
 
 
+    call = preprocess # IPython < 2.0
+
 
 
 
 
 #----------------------------------------------------------------------
 #----------------------------------------------------------------------
 # Custom highlighter:
 # Custom highlighter:
 #  instead of using class='highlight', use class='highlight-ipynb'
 #  instead of using class='highlight', use class='highlight-ipynb'
-def custom_highlighter(source, language='ipython'):
+def custom_highlighter(source, language='ipython', metadata=None):
     formatter = HtmlFormatter(cssclass='highlight-ipynb')
     formatter = HtmlFormatter(cssclass='highlight-ipynb')
-    output = _pygment_highlight(source, formatter, language)
+    if not language:
+        language = 'ipython'
+    output = _pygments_highlight(source, formatter, language)
     return output.replace('<pre>', '<pre class="ipynb">')
     return output.replace('<pre>', '<pre class="ipynb">')
 
 
 
 
@@ -252,10 +273,17 @@ def notebook(preprocessor, tag, markup):
         template_file = 'pelicanhtml'
         template_file = 'pelicanhtml'
     else:
     else:
         template_file = 'basic'
         template_file = 'basic'
+    
+    if LooseVersion(IPython.__version__) >= '2.0':
+        subcell_kwarg = dict(preprocessors=[SubCell])
+    else:
+        subcell_kwarg = dict(transformers=[SubCell])
+    
     exporter = HTMLExporter(config=c,
     exporter = HTMLExporter(config=c,
                             template_file=template_file,
                             template_file=template_file,
                             filters={'highlight2html': custom_highlighter},
                             filters={'highlight2html': custom_highlighter},
-                            transformers=[SubCell])
+                            extra_loaders=[pelican_loader],
+                            **subcell_kwarg)
 
 
     # read and parse the notebook
     # read and parse the notebook
     with open(nb_path) as f:
     with open(nb_path) as f:

+ 54 - 0
liquid_tags/vimeo.py

@@ -0,0 +1,54 @@
+"""
+Vimeo Tag
+---------
+This implements a Liquid-style vimeo tag for Pelican,
+based on the youtube tag which is in turn based on
+the jekyll / octopress youtube tag [1]_
+
+Syntax
+------
+{% vimeo id [width height] %}
+
+Example
+-------
+{% vimeo 10739054 640 480 %}
+
+Output
+------
+<div style="width:640px; height:480px;"><iframe src="//player.vimeo.com/video/10739054?title=0&amp;byline=0&amp;portrait=0" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>
+
+[1] https://gist.github.com/jamieowen/2063748
+"""
+import re
+from .mdx_liquid_tags import LiquidTags
+
+SYNTAX = "{% vimeo id [width height] %}"
+
+VIMEO = re.compile(r'(\w+)(\s+(\d+)\s(\d+))?')
+
+
+@LiquidTags.register('vimeo')
+def vimeo(preprocessor, tag, markup):
+    width = 640
+    height = 390
+    vimeo_id = None
+
+    match = VIMEO.search(markup)
+    if match:
+        groups = match.groups()
+        vimeo_id = groups[0]
+        width = groups[2] or width
+        height = groups[3] or height
+
+    if vimeo_id:
+        vimeo_out = '<div style="width:{width}px; height:{height}px;"><iframe src="//player.vimeo.com/video/{vimeo_id}?title=0&amp;byline=0&amp;portrait=0" width="{width}" height="{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe></div>'.format(width=width, height=height, vimeo_id=vimeo_id)
+    else:
+        raise ValueError("Error processing input, "
+                         "expected syntax: {0}".format(SYNTAX))
+
+    return vimeo_out
+
+
+#----------------------------------------------------------------------
+# This import allows vimeo tag to be a Pelican plugin
+from liquid_tags import register

+ 8 - 7
neighbors/neighbors.py

@@ -45,13 +45,14 @@ def neighbors(generator):
         articles.sort(key=(lambda x: x.date), reverse=(True))
         articles.sort(key=(lambda x: x.date), reverse=(True))
         set_neighbors(
         set_neighbors(
             articles, 'next_article_in_category', 'prev_article_in_category')
             articles, 'next_article_in_category', 'prev_article_in_category')
-    
-    for subcategory, articles in generator.subcategories:
-        articles.sort(key=(lambda x: x.date), reverse=(True))
-        index = subcategory.name.count('/')
-        next_name = 'next_article_in_subcategory{}'.format(index)
-        prev_name = 'prev_article_in_subcategory{}'.format(index)
-        set_neighbors(articles, next_name, prev_name)
+
+    if hasattr(generator, 'subcategories'):
+        for subcategory, articles in generator.subcategories:
+            articles.sort(key=(lambda x: x.date), reverse=(True))
+            index = subcategory.name.count('/')
+            next_name = 'next_article_in_subcategory{}'.format(index)
+            prev_name = 'prev_article_in_subcategory{}'.format(index)
+            set_neighbors(articles, next_name, prev_name)
 
 
 def register():
 def register():
     signals.article_generator_finalized.connect(neighbors)
     signals.article_generator_finalized.connect(neighbors)

+ 31 - 0
pelican_comment_system/Readme.md

@@ -0,0 +1,31 @@
+# Pelican comment system
+The pelican comment system allows you to add static comments to your articles.
+The comments are stored in Markdown files. Each comment in it own file.
+
+#### Features
+ - Static comments for each article
+ - Replies to comments
+ - Avatars and [Identicons](https://en.wikipedia.org/wiki/Identicon)
+ - Comment Atom Feed for each article
+ - Easy styleable via the themes
+
+
+See it in action here: [blog.scheirle.de](http://blog.scheirle.de/posts/2014/March/29/static-comments-via-email/)
+
+Author             | Website                   | Github
+-------------------|---------------------------|------------------------------
+Bernhard Scheirle  | <http://blog.scheirle.de> | <https://github.com/Scheirle>
+
+## Instructions
+ - [Installation and basic usage](doc/installation.md)
+ - [Avatars and Identicons](doc/avatars.md)
+ - [Comment Atom Feed](doc/feed.md)
+ - [Comment Form (aka: never gather Metadata)](doc/form.md)
+ 
+## Requirements
+To create identicons the Python Image Library is needed. Therefore you either need PIL **or** Pillow (recommended).
+
+##### Install Pillow
+	easy_install Pillow
+	
+If you don't use avatars or identicons this plugin works fine without PIL/Pillow. You will however get a warning that identicons are deactivated (as expected).

+ 1 - 0
pelican_comment_system/__init__.py

@@ -0,0 +1 @@
+from .pelican_comment_system import *

+ 94 - 0
pelican_comment_system/avatars.py

@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+"""
+
+"""
+
+from __future__ import unicode_literals
+
+import logging
+import os
+
+import hashlib
+
+
+logger = logging.getLogger(__name__)
+_log = "pelican_comment_system: avatars: "
+try:
+	from . identicon import identicon
+	_identiconImported = True
+except ImportError as e:
+	logger.warning(_log + "identicon deactivated: " + str(e))
+	_identiconImported = False
+
+# Global Variables
+_identicon_save_path = None
+_identicon_output_path = None
+_identicon_data = None
+_identicon_size = None
+_initialized = False
+_authors = None
+_missingAvatars = []
+
+def _ready():
+	if not _initialized:
+		logger.warning(_log + "Module not initialized. use init")
+	if not _identicon_data:
+		logger.debug(_log + "No identicon data set")
+	return _identiconImported and _initialized and _identicon_data
+
+
+def init(pelican_output_path, identicon_output_path, identicon_data, identicon_size, authors):
+	global _identicon_save_path
+	global _identicon_output_path
+	global _identicon_data
+	global _identicon_size
+	global _initialized
+	global _authors
+	_identicon_save_path = os.path.join(pelican_output_path, identicon_output_path)
+	_identicon_output_path = identicon_output_path
+	_identicon_data = identicon_data
+	_identicon_size = identicon_size
+	_authors = authors
+	_initialized = True
+
+def _createIdenticonOutputFolder():
+	if not _ready():
+		return
+
+	if not os.path.exists(_identicon_save_path):
+		os.makedirs(_identicon_save_path)
+
+
+def getAvatarPath(comment_id, metadata):
+	if not _ready():
+		return ''
+
+	md5 = hashlib.md5()
+	author = tuple()
+	for data in _identicon_data:
+		if data in metadata:
+			string = str(metadata[data])
+			md5.update(string.encode('utf-8'))
+			author += tuple([string])
+		else:
+			logger.warning(_log + data + " is missing in comment: " + comment_id)
+
+	if author in _authors:
+		return _authors[author]
+
+	global _missingAvatars
+
+	code = md5.hexdigest()
+
+	if not code in _missingAvatars:
+		_missingAvatars.append(code)
+
+	return os.path.join(_identicon_output_path, '%s.png' % code)
+
+def generateAndSaveMissingAvatars():
+	_createIdenticonOutputFolder()
+	for code in _missingAvatars:
+		avatar_path = '%s.png' % code
+		avatar = identicon.render_identicon(int(code, 16), _identicon_size)
+		avatar_save_path = os.path.join(_identicon_save_path, avatar_path)
+		avatar.save(avatar_save_path, 'PNG')

+ 45 - 0
pelican_comment_system/comment.py

@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""
+
+"""
+from __future__ import unicode_literals
+from pelican import contents
+from pelican.contents import Content
+
+class Comment(Content):
+	mandatory_properties = ('author', 'date')
+	default_template = 'None'
+
+	def __init__(self, id, avatar, content, metadata, settings, source_path, context):
+		super(Comment,self).__init__( content, metadata, settings, source_path, context )
+		self.id = id
+		self.replies = []
+		self.avatar = avatar
+		self.title = "Posted by:  " + str(metadata['author'])
+
+	def addReply(self, comment):
+		self.replies.append(comment)
+
+	def getReply(self, id):
+		for reply in self.replies:
+			if reply.id == id:
+				return reply
+			else:
+				deepReply = reply.getReply(id)
+				if deepReply != None:
+					return deepReply
+		return None
+
+	def __lt__(self, other):
+		return self.metadata['date'] < other.metadata['date']
+
+	def sortReplies(self):
+		for r in self.replies:
+			r.sortReplies()
+		self.replies = sorted(self.replies)
+
+	def countReplies(self):
+		amount = 0
+		for r in self.replies:
+			amount += r.countReplies()
+		return amount + len(self.replies)

+ 35 - 0
pelican_comment_system/doc/avatars.md

@@ -0,0 +1,35 @@
+# Avatars and Identicons
+To activate the avatars and [identicons](https://en.wikipedia.org/wiki/Identicon) you have to set `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`.
+
+##### Example
+```python
+PELICAN_COMMENT_SYSTEM_IDENTICON_DATA = ('author')
+```
+Now every comment with the same author tag will be treated as if written from the same person. And therefore have the same avatar/identicon. Of cause you can modify this tuple so other metadata are checked.
+
+## Specific Avatars
+To set a specific avatar for a author you have to add them to the `PELICAN_COMMENT_SYSTEM_AUTHORS` dictionary.
+
+The `key` of the dictionary has to be a tuple of the form of `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`, so in our case only the author's name.
+
+The `value` of the dictionary is the path to the specific avatar.
+
+##### Example
+```python
+PELICAN_COMMENT_SYSTEM_AUTHORS = {
+	('John'): "images/authors/john.png",
+	('Tom'): "images/authors/tom.png",
+}
+```
+
+## Theme
+To display the avatars and identicons simply add the following in the "comment for loop" in your theme:
+
+```html
+<img src="{{ SITEURL }}/{{ comment.avatar }}"
+		alt="Avatar"
+		height="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}"
+		width="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}">
+```
+
+Of cause the `height` and `width` are optional, but they make sure that everything has the same size (in particular  specific avatars).

+ 28 - 0
pelican_comment_system/doc/feed.md

@@ -0,0 +1,28 @@
+# Comment Atom Feed
+## Custom comment url
+Be sure that the id of the html tag containing the comment matches `COMMENT_URL`.
+
+##### pelicanconf.py
+```python
+COMMENT_URL = "#my_own_comment_id_{path}"
+```
+
+##### Theme
+```html
+{% for comment in article.comments recursive %}
+	...
+	<article id="my_own_comment_id_{{comment.id}}">{{ comment.content }}</article>
+	...
+{% endfor %}
+```
+## Theme
+#### Link
+To display a link to the article feed simply add the following to your theme:
+
+```html
+{% if article %}
+	<a href="{{ FEED_DOMAIN }}/{{ PELICAN_COMMENT_SYSTEM_FEED|format(article.slug) }}">Comment Atom Feed</a>
+{% endif %}
+```
+
+

+ 83 - 0
pelican_comment_system/doc/form.md

@@ -0,0 +1,83 @@
+# Comment Form (aka: never gather Metadata)
+Add a form, which allows your visitors to easily write comments.
+
+But more importantly, on submit the form generates a mailto-link.
+The resulting email contains a valid markdown block. Now you only have to copy this block in a new file. And therefore there is no need to gather the metadata (like date, author, replyto) yourself.
+
+#### Reply button
+Add this in the "comment for loop" in your article theme, so your visitors can reply to a comment.
+
+```html
+<button onclick="reply('{{comment.id | urlencode}}');">Reply</button>
+```
+
+#### Form
+A basic form so your visitors can write comments.
+
+```html
+<form role="form" id="commentForm" action="#">
+	<input name="Name" type="text" id="commentForm_inputName" placeholder="Enter your name or synonym">
+	<textarea name="Text" id="commentForm_inputText" rows="10" style="resize:vertical;" placeholder="Your comment"></textarea>
+	<button type="submit" id="commentForm_button">Post via email</button>
+	<input name="replyto" type="hidden" id="commentForm_replyto">
+</form>
+```
+You may want to add a button to reset the `replyto` field.
+
+#### Javascript
+To generate the mailto-Link and set the `replyto` field there is some javascript required.
+
+```javascript
+<script type="text/javascript">
+	function reply(id)
+	{
+		id = decodeURIComponent(id);
+		$('#commentForm_replyto').val(id);
+	}
+
+	$(document).ready(function() {
+		function generateMailToLink()
+		{
+			var user = 'your_user_name'; //user@domain = your email address
+			var domain = 'your_email_provider';
+			var subject = 'Comment for \'{{ article.slug }}\'' ;
+
+			var d = new Date();
+			var body = ''
+				+ 'Hey,\nI posted a new comment on ' + document.URL + '\n\nGreetings ' + $("#commentForm_inputName").val() + '\n\n\n'
+				+ 'Raw comment data:\n'
+				+ '----------------------------------------\n'
+				+ 'date: ' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + '\n'
+				+ 'author: ' + $("#commentForm_inputName").val() + '\n';
+
+			var replyto = $('#commentForm_replyto').val();
+			if (replyto.length != 0)
+			{
+				body += 'replyto: ' + replyto + '\n'
+			}
+
+			body += '\n'
+				+ $("#commentForm_inputText").val() + '\n'
+				+ '----------------------------------------\n';
+
+			var link = 'mailto:' + user + '@' + domain + '?subject='
+				+ encodeURIComponent(subject)
+				+ "&body="
+				+ encodeURIComponent(body);
+			return link;
+		}
+
+
+		$('#commentForm').on("submit",
+			function( event )
+			{
+				event.preventDefault();
+				$(location).attr('href', generateMailToLink());
+			}
+		);
+	});
+</script>
+```
+(jQuery is required for this script)
+
+Don't forget to set the Variables `user` and `domain`.

+ 106 - 0
pelican_comment_system/doc/installation.md

@@ -0,0 +1,106 @@
+# Installation
+Activate the plugin by adding it to your `pelicanconf.py`
+
+	PLUGIN_PATH = '/path/to/pelican-plugins'
+	PLUGINS = ['pelican_comment_system']
+	PELICAN_COMMENT_SYSTEM = True
+
+And modify your `article.html` theme (see below).
+
+## Settings
+Name                                           | Type      | Default                    | Description
+-----------------------------------------------|-----------|----------------------------|-------
+`PELICAN_COMMENT_SYSTEM`                       | `boolean` | `False`                    | Activates or deactivates the comment system
+`PELICAN_COMMENT_SYSTEM_DIR`                   | `string`  | `comments`                 | Folder where the comments are stored
+`PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH` | `string`  | `images/identicon`         | Relative URL to the output folder where the identicons are stored
+`PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`        | `tuple`   | `()`                       | Contains all Metadata tags, which in combination identifies a comment author (like `('author', 'email')`)
+`PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE`        | `int`     | `72`                       | Width and height of the identicons. Has to be a multiple of 3.
+`PELICAN_COMMENT_SYSTEM_AUTHORS`               | `dict`    | `{}`                       | Comment authors, which should have a specific avatar. More info [here](avatars.md)
+`PELICAN_COMMENT_SYSTEM_FEED`                  | `string`  |`feeds/comment.%s.atom.xml` | Relative URL to output the Atom feed for each article.`%s` gets replaced with the slug of the article. More info [here](http://docs.getpelican.com/en/latest/settings.html#feed-settings)
+`COMMENT_URL`                                  | `string`  | `#comment-{path}`          | `{path}` gets replaced with the id of the comment. More info [here](feed.md)
+
+## Folder structure
+Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`.
+Sub folders are named after the `slug` of the articles.
+
+So the comments to your `foo-bar` article are stored in `comments/foo-bar/`
+
+The filenames of the comment files are up to you. But the filename is the Identifier of the comment (**with** extension).
+
+##### Example folder structure
+
+	.
+	└── comments
+		└── foo-bar
+		│   ├── 1.md
+		│   └── 0.md
+		└── some-other-slug
+			├── random-Name.md
+			├── 1.md
+			└── 0.md
+
+
+## Comment file
+### Meta information
+Tag           | Required  | Description
+--------------|-----------|----------------
+`date`        | yes       | Date when the comment was posted
+`author`      | yes       | Name of the comment author
+`replyto`     | no        | Identifier of the parent comment. Identifier = Filename (**with** extension)
+
+Every other (custom) tag gets parsed as well and will be available through the theme.
+
+##### Example of a comment file
+
+	date: 2014-3-21 15:02
+	author: Author of the comment
+	website: http://authors.website.com
+	replyto: 7
+	anothermetatag: some random tag
+
+	Content of the comment.
+
+## Theme
+In the `article.html` theme file are now two more variables available.
+
+Variables                | Description
+-------------------------|--------------------------
+`article.comments_count` | Amount of total comments for this article (including replies to comments)
+`article.comments`       | Array containing the top level comments for this article (no replies to comments)
+
+### Comment object
+The comment object is a [content](https://github.com/getpelican/pelican/blob/master/pelican/contents.py#L34) object, so all common attributes are available (like author, content, date, local_date, metadata, ...).
+
+Additional following attributes are added:
+
+Attribute  | Description
+-----------|--------------------------
+`id`       | Identifier of this comment
+`replies`  | Array containing the top level replies for this comment
+`avatar`   | Path to the avatar or identicon of the comment author
+
+##### Example article.html theme
+(only the comment section)
+```html
+{% if article.comments %}
+	{% for comment in article.comments recursive %}
+		{% if loop.depth0 == 0 %}
+			{% set marginLeft = 0 %}
+		{% else %}
+			{% set marginLeft = 50 %}
+		{% endif %}
+			<article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;">
+				<a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a>
+				<h4>{{ comment.author }}</h4>
+				<p>Posted on <abbr class="published" title="{{ comment.date.isoformat() }}">{{ comment.locale_date }}</abbr></p>
+				{{ comment.metadata['my_custom_metadata'] }}
+				{{ comment.content }}
+				{% if comment.replies %}
+					{{ loop(comment.replies) }}
+				{% endif %}
+			</article>
+	{% endfor %}
+{% else %}
+	<p>There are no comments yet.<p>
+{% endif %}
+```

ファイルの差分が大きいため隠しています
+ 11 - 0
pelican_comment_system/identicon/LICENSE


+ 17 - 0
pelican_comment_system/identicon/README.md

@@ -0,0 +1,17 @@
+identicon.py: identicon python implementation.
+==============================================
+:Author:Shin Adachi <shn@glucose.jp>
+
+## usage
+
+### commandline
+
+    python identicon.py [code]
+
+### python
+
+    import identicon
+    identicon.render_identicon(code, size)
+
+Return a PIL Image class instance which have generated identicon image.
+`size` specifies patch size. Generated image size is 3 * `size`.

+ 0 - 0
pelican_comment_system/identicon/__init__.py


+ 256 - 0
pelican_comment_system/identicon/identicon.py

@@ -0,0 +1,256 @@
+#!/usr/bin/env python
+# -*- coding:utf-8 -*-
+"""
+identicon.py
+identicon python implementation.
+by Shin Adachi <shn@glucose.jp>
+
+= usage =
+
+== commandline ==
+>>> python identicon.py [code]
+
+== python ==
+>>> import identicon
+>>> identicon.render_identicon(code, size)
+
+Return a PIL Image class instance which have generated identicon image.
+```size``` specifies `patch size`. Generated image size is 3 * ```size```.
+"""
+# g
+# PIL Modules
+from PIL import Image, ImageDraw, ImagePath, ImageColor
+
+
+__all__ = ['render_identicon', 'IdenticonRendererBase']
+
+
+class Matrix2D(list):
+    """Matrix for Patch rotation"""
+    def __init__(self, initial=[0.] * 9):
+        assert isinstance(initial, list) and len(initial) == 9
+        list.__init__(self, initial)
+
+    def clear(self):
+        for i in xrange(9):
+            self[i] = 0.
+
+    def set_identity(self):
+        self.clear()
+        for i in xrange(3):
+            self[i] = 1.
+
+    def __str__(self):
+        return '[%s]' % ', '.join('%3.2f' % v for v in self)
+
+    def __mul__(self, other):
+        r = []
+        if isinstance(other, Matrix2D):
+            for y in range(3):
+                for x in range(3):
+                    v = 0.0
+                    for i in range(3):
+                        v += (self[i * 3 + x] * other[y * 3 + i])
+                    r.append(v)
+        else:
+            raise NotImplementedError
+        return Matrix2D(r)
+
+    def for_PIL(self):
+        return self[0:6]
+
+    @classmethod
+    def translate(kls, x, y):
+        return kls([1.0, 0.0, float(x),
+                    0.0, 1.0, float(y),
+                    0.0, 0.0, 1.0])
+
+    @classmethod
+    def scale(kls, x, y):
+        return kls([float(x), 0.0, 0.0,
+                    0.0, float(y), 0.0,
+                    0.0, 0.0, 1.0])
+
+    """
+    # need `import math`
+    @classmethod
+    def rotate(kls, theta, pivot=None):
+        c = math.cos(theta)
+        s = math.sin(theta)
+
+        matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
+        if not pivot:
+            return matR
+        return kls.translate(-pivot[0], -pivot[1]) * matR *
+            kls.translate(*pivot)
+    """
+    
+    @classmethod
+    def rotateSquare(kls, theta, pivot=None):
+        theta = theta % 4
+        c = [1., 0., -1., 0.][theta]
+        s = [0., 1., 0., -1.][theta]
+
+        matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
+        if not pivot:
+            return matR
+        return kls.translate(-pivot[0], -pivot[1]) * matR * \
+            kls.translate(*pivot)
+
+
+class IdenticonRendererBase(object):
+    PATH_SET = []
+    
+    def __init__(self, code):
+        """
+        @param code code for icon
+        """
+        if not isinstance(code, int):
+            code = int(code)
+        self.code = code
+    
+    def render(self, size):
+        """
+        render identicon to PIL.Image
+        
+        @param size identicon patchsize. (image size is 3 * [size])
+        @return PIL.Image
+        """
+        
+        # decode the code
+        middle, corner, side, foreColor, backColor = self.decode(self.code)
+        size = int(size)
+        # make image        
+        image = Image.new("RGB", (size * 3, size * 3))
+        draw = ImageDraw.Draw(image)
+        
+        # fill background
+        draw.rectangle((0, 0, image.size[0], image.size[1]), fill=0)
+        
+        kwds = {
+            'draw': draw,
+            'size': size,
+            'foreColor': foreColor,
+            'backColor': backColor}
+        # middle patch
+        self.drawPatch((1, 1), middle[2], middle[1], middle[0], **kwds)
+
+        # side patch
+        kwds['type'] = side[0]
+        for i in range(4):
+            pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i]
+            self.drawPatch(pos, side[2] + 1 + i, side[1], **kwds)
+        
+        # corner patch
+        kwds['type'] = corner[0]
+        for i in range(4):
+            pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i]
+            self.drawPatch(pos, corner[2] + 1 + i, corner[1], **kwds)
+        
+        return image
+                
+    def drawPatch(self, pos, turn, invert, type, draw, size, foreColor,
+            backColor):
+        """
+        @param size patch size
+        """
+        path = self.PATH_SET[type]
+        if not path:
+            # blank patch
+            invert = not invert
+            path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)]
+        patch = ImagePath.Path(path)
+        if invert:
+            foreColor, backColor = backColor, foreColor
+        
+        mat = Matrix2D.rotateSquare(turn, pivot=(0.5, 0.5)) *\
+              Matrix2D.translate(*pos) *\
+              Matrix2D.scale(size, size)
+        
+        patch.transform(mat.for_PIL())
+        draw.rectangle((pos[0] * size, pos[1] * size, (pos[0] + 1) * size,
+            (pos[1] + 1) * size), fill=backColor)
+        draw.polygon(patch, fill=foreColor, outline=foreColor)
+
+    ### virtual functions
+    def decode(self, code):
+        raise NotImplementedError
+
+
+class DonRenderer(IdenticonRendererBase):
+    """
+    Don Park's implementation of identicon
+    see : http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released
+    """
+    
+    PATH_SET = [
+        [(0, 0), (4, 0), (4, 4), (0, 4)],   # 0
+        [(0, 0), (4, 0), (0, 4)],
+        [(2, 0), (4, 4), (0, 4)],
+        [(0, 0), (2, 0), (2, 4), (0, 4)],
+        [(2, 0), (4, 2), (2, 4), (0, 2)],   # 4
+        [(0, 0), (4, 2), (4, 4), (2, 4)],
+        [(2, 0), (4, 4), (2, 4), (3, 2), (1, 2), (2, 4), (0, 4)],
+        [(0, 0), (4, 2), (2, 4)],
+        [(1, 1), (3, 1), (3, 3), (1, 3)],   # 8   
+        [(2, 0), (4, 0), (0, 4), (0, 2), (2, 2)],
+        [(0, 0), (2, 0), (2, 2), (0, 2)],
+        [(0, 2), (4, 2), (2, 4)],
+        [(2, 2), (4, 4), (0, 4)],
+        [(2, 0), (2, 2), (0, 2)],
+        [(0, 0), (2, 0), (0, 2)],
+        []]                                 # 15
+    MIDDLE_PATCH_SET = [0, 4, 8, 15]
+    
+    # modify path set
+    for idx in range(len(PATH_SET)):
+        if PATH_SET[idx]:
+            p = map(lambda vec: (vec[0] / 4.0, vec[1] / 4.0), PATH_SET[idx])
+            p = list(p)
+            PATH_SET[idx] = p + p[:1]
+    
+    def decode(self, code):
+        # decode the code        
+        middleType  = self.MIDDLE_PATCH_SET[code & 0x03]
+        middleInvert= (code >> 2) & 0x01
+        cornerType  = (code >> 3) & 0x0F
+        cornerInvert= (code >> 7) & 0x01
+        cornerTurn  = (code >> 8) & 0x03
+        sideType    = (code >> 10) & 0x0F
+        sideInvert  = (code >> 14) & 0x01
+        sideTurn    = (code >> 15) & 0x03
+        blue        = (code >> 16) & 0x1F
+        green       = (code >> 21) & 0x1F
+        red         = (code >> 27) & 0x1F
+        
+        foreColor = (red << 3, green << 3, blue << 3)
+        
+        return (middleType, middleInvert, 0),\
+               (cornerType, cornerInvert, cornerTurn),\
+               (sideType, sideInvert, sideTurn),\
+               foreColor, ImageColor.getrgb('white')
+
+
+def render_identicon(code, size, renderer=None):
+    if not renderer:
+        renderer = DonRenderer
+    return renderer(code).render(size)
+
+
+if __name__ == '__main__':
+    import sys
+    
+    if len(sys.argv) < 2:
+        print('usage: python identicon.py [CODE]....')
+        raise SystemExit
+    
+    for code in sys.argv[1:]:
+        if code.startswith('0x') or code.startswith('0X'):
+            code = int(code[2:], 16)
+        elif code.startswith('0'):
+            code = int(code[1:], 8)
+        else:
+            code = int(code)
+        
+        icon = render_identicon(code, 24)
+        icon.save('%08x.png' % code, 'PNG')

+ 121 - 0
pelican_comment_system/pelican_comment_system.py

@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+"""
+Pelican Comment System
+======================
+
+A Pelican plugin, which allows you to add comments to your articles.
+
+Author: Bernhard Scheirle
+"""
+from __future__ import unicode_literals
+import logging
+import os
+import copy
+
+logger = logging.getLogger(__name__)
+
+from itertools import chain
+from pelican import signals
+from pelican.readers import MarkdownReader
+from pelican.writers import Writer
+
+from . comment import Comment
+from . import avatars
+
+
+def pelican_initialized(pelican):
+	from pelican.settings import DEFAULT_CONFIG
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM', False)
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_DIR' 'comments')
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH' 'images/identicon')
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
+	DEFAULT_CONFIG.setdefault('COMMENT_URL', '#comment-{path}')
+	if pelican:
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments')
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH', 'images/identicon')
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
+		pelican.settings.setdefault('COMMENT_URL', '#comment-{path}')
+
+
+def initialize(article_generator):
+	avatars.init(
+		article_generator.settings['OUTPUT_PATH'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_DATA'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE']/3,
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_AUTHORS'],
+		)
+
+def add_static_comments(gen, content):
+	if gen.settings['PELICAN_COMMENT_SYSTEM'] != True:
+		return
+
+	content.comments_count = 0
+	content.comments = []
+
+	#Modify the local context, so we get proper values for the feed
+	context = copy.copy(gen.context)
+	context['SITEURL'] += "/" + content.url
+	context['SITENAME'] = "Comments for: " + content.title
+	context['SITESUBTITLE'] = ""
+	path = gen.settings['PELICAN_COMMENT_SYSTEM_FEED'] % content.slug
+	writer = Writer(gen.output_path, settings=gen.settings)
+
+	folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], content.slug)
+
+	if not os.path.isdir(folder):
+		logger.debug("No comments found for: " + content.slug)
+		writer.write_feed( [], context, path)
+		return
+
+	reader = MarkdownReader(gen.settings)
+	comments = []
+	replies = []
+
+	for file in os.listdir(folder):
+		name, extension = os.path.splitext(file)
+		if extension[1:].lower() in reader.file_extensions:
+			com_content, meta = reader.read(os.path.join(folder, file))
+			
+			avatar_path = avatars.getAvatarPath(name, meta)
+
+			com = Comment(file, avatar_path, com_content, meta, gen.settings, file, context)
+
+			if 'replyto' in meta:
+				replies.append( com )
+			else:
+				comments.append( com )
+
+	writer.write_feed( comments + replies, context, path)
+
+	#TODO: Fix this O(n²) loop
+	for reply in replies:
+		for comment in chain(comments, replies):
+			if comment.id == reply.metadata['replyto']:
+				comment.addReply(reply)
+
+	count = 0
+	for comment in comments:
+		comment.sortReplies()
+		count += comment.countReplies()
+
+	comments = sorted(comments)
+
+	content.comments_count = len(comments) + count
+	content.comments = comments
+
+def writeIdenticonsToDisk(gen, writer):
+	avatars.generateAndSaveMissingAvatars()
+
+def register():
+	signals.initialized.connect(pelican_initialized)
+	signals.article_generator_init.connect(initialize)
+	signals.article_generator_write_article.connect(add_static_comments)
+	signals.article_writer_finalized.connect(writeIdenticonsToDisk)

+ 173 - 0
render_math/Readme.md

@@ -0,0 +1,173 @@
+Math Render Plugin For Pelican
+==============================
+This plugin gives pelican the ability to render mathematics. It accomplishes
+this by using the [MathJax](http://www.mathjax.org/) javascript engine. Both
+[LaTex](http://en.wikipedia.org/wiki/LaTeX) and [MathML](http://en.wikipedia.org/wiki/MathML) 
+can be rendered within the content.
+
+The plugin also ensures that pelican and recognized math "play" nicely together, by
+ensuring [Typogrify](https://github.com/mintchaos/typogrify) does not alter math content
+and summaries that get cut off are repaired.
+
+Recognized math in the context of this plugin is either LaTex or MathML as described below.
+
+### LaTex
+Anything between `$`...`$` (inline math) and `$$`..`$$` (displayed math) will be recognized as
+LaTex. In addition, anything the `\begin` and `\end` LaTex macros will also be 
+recognized as LaTex. For example, `\begin{equation}`...`\end{equation}` would be used to 
+render math equations with numbering.
+
+Within recognized LaTex as described above, any supported LaTex macro can be used.
+
+### MathML
+Anything between `<math>` and `</math>` tags will be recognized as MathML
+
+Installation
+------------
+To enable, ensure that `render_math` plugin is accessible.
+Then add the following to settings.py:
+
+    PLUGINS = ["render_math"]
+
+Your site is now capable of rendering math math using the mathjax JavaScript
+engine. No alterations to the template is needed, just use and enjoy!
+
+### Typogrify
+In the past, using [Typgogrify](https://github.com/mintchaos/typogrify) would alter the math contents resulting
+in math that could not be rendered by MathJax. The only option was to ensure
+that Typogrify was disabled in the settings.
+
+The problem has been recitified in this plugin, but it requires [Typogrify version 2.04](https://pypi.python.org/pypi/typogrify)
+(or higher). If this version of Typogrify is not present, the plugin will inform that an incorrect
+version of Typogrify is not present and disable Typogrify for the entire site
+
+Usage
+-----
+### Backward Compatibility
+This plugin is backward compatible in the sense that it
+will render previous setups correctly. This is because those
+settings and metadata information is ignored by this version. Therefore
+you can remove them to neaten up your site
+
+### Templates
+No alteration is needed to a template for this plugin to work. Just install
+the plugin and start writing your Math. 
+
+If on the other hand, you are template designer and want total control
+over the MathJax JavaScript, you can set the `auto_insert` setting to 
+`False` which will cause no MathJax JavaScript to be added to the content.
+
+If you choose this option, you should really know what you are doing. Therefore
+only do this if you are designing your template. There is no real advantage to
+to letting template logic handle the insertion of the MathJax JavaScript other
+than it being slightly neater.
+
+By setting `auto_insert` to `False`, metadata with `key` value of `mathjax`
+will be present in all pages and articles where MathJax should be present.
+The template designer can detect this and then use the `MATHJAXSCRIPT` setting
+which will contain the user specified MathJax script to insert into the content.
+
+For example, this code could be used:
+```
+{% if not MATH['auto_insert'] %}
+    {% if page and page.mathjax or article and article.mathjax %}
+        {{ MATHJAXSCRIPT }}
+    {% endif %}
+{% endif %}
+```
+
+### Settings
+Certain MathJax rendering options can be set. These options 
+are in a dictionary variable called `MATH` in the pelican
+settings file.
+
+The dictionary can be set with the following keys:
+
+ * `auto_insert`: controls whether plugin should automatically insert
+MathJax JavaScript in content that has Math. It is only recommended
+to set this to False if you are a template designer and you want
+extra control over where the MathJax JavaScript is renderd. **Default Value**:
+True
+ * `wrap_latex`: controls the tags that LaTex math is wrapped with inside the resulting
+html. For example, setting `wrap_latex` to `mathjax` would wrap all LaTex math inside
+`<mathjax>...</mathjax>` tags. If typogrify is set to True, then math needs
+to be wrapped in tags and `wrap_latex` will therefore default to `mathjax` if not
+set. `wrap_latex` cannot be set to `'math'` because this tag is reserved for 
+mathml notation. **Default Value**: None unless Typogrify is enabled in which case, 
+it defaults to `mathjax`
+ * `align`: controls how displayed math will be aligned. Can be set to either
+`left`, `right` or `center`. **Default Value**: `center`.
+ * `indent`: if `align` not set to `center`, then this controls the indent
+level. **Default Value**: `0em`.
+ * `show_menu`: a boolean value that controls whether the mathjax contextual 
+menu is shown. **Default Value**: True
+ * `process_escapes`: a boolean value that controls whether mathjax processes escape 
+sequences. **Default Value**: True
+ * `latex_preview`: controls the preview message users are seen while mathjax is
+rendering LaTex. If set to `Tex`, then the TeX code is used as the preview 
+(which will be visible until it is processed by MathJax). **Default Value**: `Tex`
+ * `color`: controls the color of the mathjax rendered font. **Default Value**: `black`
+ * `ssl`: specifies if ssl should be used to load MathJax engine. Can be set to one
+of three things
+  * `auto`: **Default Value** will automatically determine what protodol to use 
+based on current protocol of the site. 
+  * `force`: will force ssl to be used.
+  * `off`: will ensure that ssl is not used
+
+For example, in settings.py, the following would make math render in blue and
+displaymath align to the left:
+
+    MATH = {'color':'blue','align':left}
+
+LaTex Examples
+--------------
+###Inline
+LaTex between `$`..`$`, for example, `$`x^2`$`, will be rendered inline
+with respect to the current html block.
+
+###Displayed Math
+LaTex between `$$`..`$$`, for example, `$$`x^2`$$`, will be rendered centered in a
+new paragraph.
+
+###Equations
+LaTex between `\begin` and `\end`, for example, `begin{equation}` x^2 `\end{equation}`,
+will be rendered centered in a new paragraph with a right justified equation number
+at the top of the paragraph. This equation number can be referenced in the document.
+To do this, use a `label` inside of the equation format and then refer to that label
+using `ref`. For example: `begin{equation}` `\label{eq}` X^2 `\end{equation}`. Now
+refer to that equation number by `$`\ref{eq}`$`.
+
+MathML Examples
+---------------
+The following will render the Quadratic formula:
+```
+<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 
+  <mrow>
+    <mi>x</mi>
+    <mo>=</mo>
+    <mfrac>
+      <mrow>
+        <mo>&#x2212;</mo>
+        <mi>b</mi>
+        <mo>&#xB1;</mo>
+        <msqrt>
+          <mrow>
+            <msup>
+              <mi>b</mi>
+              <mn>2</mn>
+            </msup>
+            <mo>&#x2212;</mo>
+            <mn>4</mn>
+            <mi>a</mi>
+            <mi>c</mi>
+          </mrow>
+        </msqrt>
+      </mrow>
+      <mrow>
+        <mn>2</mn>
+        <mi>a</mi>
+      </mrow>
+    </mfrac>
+  </mrow>
+</math>
+```

+ 1 - 0
render_math/__init__.py

@@ -0,0 +1 @@
+from .math import *

+ 379 - 0
render_math/math.py

@@ -0,0 +1,379 @@
+# -*- coding: utf-8 -*-
+"""
+Math Render Plugin for Pelican
+==============================
+This plugin allows your site to render Math. It supports both LaTeX and MathML
+using the MathJax JavaScript engine.
+
+Typogrify Compatibility
+-----------------------
+This plugin now plays nicely with Typogrify, but it requires
+Typogrify version 2.04 or above.
+
+User Settings
+-------------
+Users are also able to pass a dictionary of settings in the settings file which
+will control how the MathJax library renders things. This could be very useful
+for template builders that want to adjust the look and feel of the math.
+See README for more details.
+"""
+
+import os
+import re
+
+from pelican import signals
+from pelican import contents
+
+
+# Global Variables
+_TYPOGRIFY = None  # if Typogrify is enabled, this is set to the typogrify.filter function
+_WRAP_LATEX = None  # the tag to wrap LaTeX math in (needed to play nicely with Typogrify or for template designers)
+_MATH_REGEX = re.compile(r'(\$\$|\$|\\begin\{(.+?)\}|<(math)(?:\s.*?)?>).*?(\1|\\end\{\2\}|</\3>)', re.DOTALL | re.IGNORECASE)  # used to detect math
+_MATH_SUMMARY_REGEX = None  # used to match math in summary
+_MATH_INCOMPLETE_TAG_REGEX = None  # used to match math that has been cut off in summary
+_MATHJAX_SETTINGS = {}  # settings that can be specified by the user, used to control mathjax script settings
+with open (os.path.dirname(os.path.realpath(__file__))+'/mathjax_script.txt', 'r') as mathjax_script:  # Read the mathjax javascript from file
+    _MATHJAX_SCRIPT=mathjax_script.read()
+
+
+# Python standard library for binary search, namely bisect is cool but I need
+# specific business logic to evaluate my search predicate, so I am using my
+# own version
+def binary_search(match_tuple, ignore_within):
+    """Determines if t is within tupleList. Using the fact that tupleList is
+    ordered, binary search can be performed which is O(logn)
+    """
+
+    ignore = False
+    if ignore_within == []:
+        return False
+
+    lo = 0
+    hi = len(ignore_within)-1
+
+    # Find first value in array where predicate is False
+    # predicate function: tupleList[mid][0] < t[index]
+    while lo < hi:
+        mid = lo + (hi-lo+1)//2
+        if ignore_within[mid][0] < match_tuple[0]:
+            lo = mid
+        else:
+            hi = mid-1
+
+    if lo >= 0 and lo <= len(ignore_within)-1:
+        ignore = (ignore_within[lo][0] <= match_tuple[0] and ignore_within[lo][1] >= match_tuple[1])
+
+    return ignore
+
+
+def ignore_content(content):
+    """Creates a list of match span tuples for which content should be ignored
+    e.g. <pre> and <code> tags
+    """
+    ignore_within = []
+
+    # used to detect all <pre> and <code> tags. NOTE: Alter this regex should
+    # additional tags need to be ignored
+    ignore_regex = re.compile(r'<(pre|code)(?:\s.*?)?>.*?</(\1)>', re.DOTALL | re.IGNORECASE)
+
+    for match in ignore_regex.finditer(content):
+        ignore_within.append(match.span())
+
+    return ignore_within
+
+
+def wrap_math(content, ignore_within):
+    """Wraps math in user specified tags.
+
+    This is needed for Typogrify to play nicely with math but it can also be
+    styled by template providers
+    """
+
+    wrap_math.found_math = False
+
+    def math_tag_wrap(match):
+        """function for use in re.sub"""
+
+        # determine if the tags are within <pre> and <code> blocks
+        ignore = binary_search(match.span(1), ignore_within) or binary_search(match.span(4), ignore_within)
+
+        if ignore or match.group(3) == 'math':
+            if match.group(3) == 'math':
+                # Will detect mml, but not wrap anything around it
+                wrap_math.found_math = True
+
+            return match.group(0)
+        else:
+            wrap_math.found_math = True
+            return '<%s>%s</%s>' % (_WRAP_LATEX, match.group(0), _WRAP_LATEX)
+
+    return (_MATH_REGEX.sub(math_tag_wrap, content), wrap_math.found_math)
+
+
+def process_summary(instance, ignore_within):
+    """Summaries need special care. If Latex is cut off, it must be restored.
+
+    In addition, the mathjax script must be included if necessary thereby
+    making it independent to the template
+    """
+
+    process_summary.altered_summary = False
+    insert_mathjax = False
+    end_tag = '</%s>' % _WRAP_LATEX if _WRAP_LATEX is not None else ''
+
+    # use content's _get_summary method to obtain summary
+    summary = instance._get_summary()
+
+    # Determine if there is any math in the summary which are not within the
+    # ignore_within tags
+    math_item = None
+    for math_item in _MATH_SUMMARY_REGEX.finditer(summary):
+        ignore = binary_search(math_item.span(2), ignore_within)
+        if '...' not in math_item.group(5):
+            ignore = ignore or binary_search(math_item.span(5), ignore_within)
+        else:
+            ignore = ignore or binary_search(math_item.span(6), ignore_within)
+
+        if ignore:
+            math_item = None # In <code> or <pre> tags, so ignore
+        else:
+            insert_mathjax = True
+
+    # Repair the math if it was cut off math_item will be the final math
+    # code  matched that is not within <pre> or <code> tags
+    if math_item and '...' in math_item.group(5):
+        if math_item.group(3) is not None:
+            end = r'\end{%s}' % math_item.group(3)
+        elif math_item.group(4) is not None:
+            end = r'</math>'
+        elif math_item.group(2) is not None:
+            end = math_item.group(2)
+
+        search_regex = r'%s(%s.*?%s)' % (re.escape(instance._content[0:math_item.start(1)]), re.escape(math_item.group(1)), re.escape(end))
+        math_match = re.search(search_regex, instance._content, re.DOTALL | re.IGNORECASE)
+
+        if math_match:
+            new_summary = summary.replace(math_item.group(0), math_match.group(1)+'%s ...' % end_tag)
+
+            if new_summary != summary:
+                if _MATHJAX_SETTINGS['auto_insert']:
+                    return new_summary+_MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
+                else:
+                    instance.mathjax = True
+                    return new_summary
+
+    def incomplete_end_latex_tag(match):
+        """function for use in re.sub"""
+        if binary_search(match.span(3), ignore_within):
+            return match.group(0)
+
+        process_summary.altered_summary = True
+        return match.group(1) + match.group(4)
+
+    # check for partial math tags at end. These must be removed
+    summary = _MATH_INCOMPLETE_TAG_REGEX.sub(incomplete_end_latex_tag, summary)
+
+    if process_summary.altered_summary or insert_mathjax:
+        if insert_mathjax:
+            if _MATHJAX_SETTINGS['auto_insert']:
+                summary+= _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
+            else:
+                instance.mathjax = True
+
+        return summary
+
+    return None  # Making it explicit that summary was not altered
+
+
+def process_settings(settings):
+    """Sets user specified MathJax settings (see README for more details)"""
+
+    global _MATHJAX_SETTINGS
+
+    # NOTE TO FUTURE DEVELOPERS: Look at the README and what is happening in
+    # this function if any additional changes to the mathjax settings need to
+    # be incorporated. Also, please inline comment what the variables
+    # will be used for
+
+    # Default settings
+    _MATHJAX_SETTINGS['align'] = 'center'  # controls alignment of of displayed equations (values can be: left, right, center)
+    _MATHJAX_SETTINGS['indent'] = '0em'  # if above is not set to 'center', then this setting acts as an indent
+    _MATHJAX_SETTINGS['show_menu'] = 'true'  # controls whether to attach mathjax contextual menu
+    _MATHJAX_SETTINGS['process_escapes'] = 'true'  # controls whether escapes are processed
+    _MATHJAX_SETTINGS['latex_preview'] = 'TeX'  # controls what user sees while waiting for LaTex to render
+    _MATHJAX_SETTINGS['color'] = 'black'  # controls color math is rendered in
+
+    # Source for MathJax: default (below) is to automatically determine what protocol to use
+    _MATHJAX_SETTINGS['source'] = """'https:' == document.location.protocol
+                ? 'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
+                : 'http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"""
+
+    # This next setting controls whether the mathjax script should be automatically
+    # inserted into the content. The mathjax script will not be inserted into
+    # the content if no math is detected. For summaries that are present in the
+    # index listings, mathjax script will also be automatically inserted.
+    # Setting this value to false means the template must be altered if this
+    # plugin is to work, and so it is only recommended for the template
+    # designer who wants maximum control.
+    _MATHJAX_SETTINGS['auto_insert'] = True  # controls whether mathjax script is automatically inserted into the content
+
+    if not isinstance(settings, dict):
+        return
+
+    # The following mathjax settings can be set via the settings dictionary
+    # Iterate over dictionary in a way that is compatible with both version 2
+    # and 3 of python
+    for key, value in ((key, settings[key]) for key in settings):
+        if key == 'auto_insert' and isinstance(value, bool):
+            _MATHJAX_SETTINGS[key] = value
+
+        if key == 'align' and isinstance(value, str):
+            if value == 'left' or value == 'right' or value == 'center':
+                _MATHJAX_SETTINGS[key] = value
+            else:
+                _MATHJAX_SETTINGS[key] = 'center'
+
+        if key == 'indent':
+            _MATHJAX_SETTINGS[key] = value
+
+        if key == 'show_menu' and isinstance(value, bool):
+            _MATHJAX_SETTINGS[key] = 'true' if value else 'false'
+
+        if key == 'process_escapes' and isinstance(value, bool):
+            _MATHJAX_SETTINGS[key] = 'true' if value else 'false'
+
+        if key == 'latex_preview' and isinstance(value, str):
+            _MATHJAX_SETTINGS[key] = value
+
+        if key == 'color' and isinstance(value, str):
+            _MATHJAX_SETTINGS[key] = value
+
+        if key == 'ssl' and isinstance(value, str):
+            if value == 'off':
+                _MATHJAX_SETTINGS['source'] = "'http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"
+
+            if value == 'force':
+                _MATHJAX_SETTINGS['source'] = "'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"
+
+
+def process_content(instance):
+    """Processes content, with logic to ensure that Typogrify does not clash
+    with math.
+
+    In addition, mathjax script is inserted at the end of the content thereby
+    making it independent of the template
+    """
+
+    if not instance._content:
+        return
+
+    ignore_within = ignore_content(instance._content)
+
+    if _WRAP_LATEX:
+        instance._content, math = wrap_math(instance._content, ignore_within)
+    else:
+        math = True if _MATH_REGEX.search(instance._content) else False
+
+    # The user initially set Typogrify to be True, but since it would clash
+    # with math, we set it to False. This means that the default reader will
+    # not call Typogrify, so it is called here, where we are able to control
+    # logic for it ignore math if necessary
+    if _TYPOGRIFY:
+        # Tell Typogrify to ignore the tags that math has been wrapped in
+        # also, Typogrify must always ignore mml (math) tags
+        ignore_tags = [_WRAP_LATEX,'math'] if _WRAP_LATEX else ['math']
+
+        # Exact copy of the logic as found in the default reader
+        instance._content = _TYPOGRIFY(instance._content, ignore_tags)
+        instance.metadata['title'] = _TYPOGRIFY(instance.metadata['title'], ignore_tags)
+
+    if math:
+        if _MATHJAX_SETTINGS['auto_insert']:
+            # Mathjax script added to content automatically. Now it
+            # does not need to be explicitly added to the template
+            instance._content += _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
+        else:
+            # Place the burden on ensuring mathjax script is available to
+            # browser on the template designer (see README for more details)
+            instance.mathjax = True
+
+        # The summary needs special care because math math cannot just be cut
+        # off
+        summary = process_summary(instance, ignore_within)
+        if summary is not None:
+            instance._summary = summary
+
+
+def pelican_init(pelicanobj):
+    """Intialializes certain global variables and sets typogogrify setting to
+    False should it be set to True.
+    """
+
+    global _TYPOGRIFY
+    global _WRAP_LATEX
+    global _MATH_SUMMARY_REGEX
+    global _MATH_INCOMPLETE_TAG_REGEX
+
+    try:
+        settings = pelicanobj.settings['MATH']
+    except:
+        settings = None
+
+    process_settings(settings)
+
+    # Allows MathJax script to be accessed from template should it be needed
+    pelicanobj.settings['MATHJAXSCRIPT'] = _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
+
+    # If Typogrify set to True, then we need to handle it manually so it does
+    # not conflict with LaTeX
+    try:
+        if pelicanobj.settings['TYPOGRIFY'] is True:
+            pelicanobj.settings['TYPOGRIFY'] = False
+            try:
+                from typogrify.filters import typogrify
+
+                # Determine if this is the correct version of Typogrify to use
+                import inspect
+                typogrify_args = inspect.getargspec(typogrify).args
+                if len(typogrify_args) < 2 or 'ignore_tags' not in typogrify_args:
+                    raise TypeError('Incorrect version of Typogrify')
+
+                # At this point, we are happy to use Typogrify, meaning
+                # it is installed and it is a recent enough version
+                # that can be used to ignore all math
+                _TYPOGRIFY = typogrify
+                _WRAP_LATEX = 'mathjax' # default to wrap mathjax content inside of
+            except ImportError:
+                print("\nTypogrify is not installed, so it is being ignored.\nIf you want to use it, please install via: pip install typogrify\n")
+            except TypeError:
+                print("\nA more recent version of Typogrify is needed for the render_math module.\nPlease upgrade Typogrify to the latest version (anything above version 2.04 is okay).\nTypogrify will be turned off due to this reason.\n")
+    except KeyError:
+        pass
+
+    # Set _WRAP_LATEX to the settings tag if defined. The idea behind this is
+    # to give template designers control over how math would be rendered
+    try:
+        if pelicanobj.settings['MATH']['wrap_latex']:
+            _WRAP_LATEX = pelicanobj.settings['MATH']['wrap_latex']
+    except (KeyError, TypeError):
+        pass
+
+    # regular expressions that depend on _WRAP_LATEX are set here
+    tag_start= r'<%s>' % _WRAP_LATEX if not _WRAP_LATEX is None else ''
+    tag_end = r'</%s>' % _WRAP_LATEX if not _WRAP_LATEX is None else ''
+    math_summary_regex = r'((\$\$|\$|\\begin\{(.+?)\}|<(math)(?:\s.*?)?>).+?)(\2|\\end\{\3\}|</\4>|\s?\.\.\.)(%s|</\4>)?' % tag_end
+
+    # NOTE: The logic in _get_summary will handle <math> correctly because it
+    # is perceived as an html tag. Therefore we are only interested in handling
+    # non mml (i.e. LaTex)
+    incomplete_end_latex_tag = r'(.*)(%s)(\\\S*?|\$)\s*?(\s?\.\.\.)(%s)?$' % (tag_start, tag_end)
+
+    _MATH_SUMMARY_REGEX = re.compile(math_summary_regex, re.DOTALL | re.IGNORECASE)
+    _MATH_INCOMPLETE_TAG_REGEX = re.compile(incomplete_end_latex_tag, re.DOTALL | re.IGNORECASE)
+
+
+def register():
+    """Plugin registration"""
+
+    signals.initialized.connect(pelican_init)
+    signals.content_object_init.connect(process_content)

+ 28 - 0
render_math/mathjax_script.txt

@@ -0,0 +1,28 @@
+<script type= "text/javascript">
+    if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {{
+        var mathjaxscript = document.createElement('script');
+        mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
+        mathjaxscript.type = 'text/javascript';
+        mathjaxscript.src = {source};
+        mathjaxscript[(window.opera ? "innerHTML" : "text")] =
+            "MathJax.Hub.Config({{" +
+            "    config: ['MMLorHTML.js']," +
+            "    TeX: {{ extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: {{ autoNumber: 'AMS' }} }}," +
+            "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
+            "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
+            "    displayAlign: '{align}'," +
+            "    displayIndent: '{indent}'," +
+            "    showMathMenu: {show_menu}," +
+            "    tex2jax: {{ " +
+            "        inlineMath: [ ['$','$'] ], " +
+            "        displayMath: [ ['$$','$$'] ]," +
+            "        processEscapes: {process_escapes}," +
+            "        preview: '{latex_preview}'," +
+            "    }}, " +
+            "    'HTML-CSS': {{ " +
+            "        styles: {{ '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {{color: '{color} ! important'}} }}" +
+            "    }} " +
+            "}}); ";
+        (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
+    }}
+</script>

+ 26 - 0
simple_footnotes/README.md

@@ -0,0 +1,26 @@
+Simple Footnotes
+================
+
+A Pelican plugin to add footnotes to blog posts.
+
+When writing a post or page, add a footnote like this:
+
+    Here's my written text[ref]and here is a footnote[/ref].
+
+This will appear as, roughly:
+
+Here's my written text<sup>1</sup>
+
+ 1. and here is a footnote ↩
+
+Inspired by Andrew Nacin's [Simple Footnotes WordPress plugin](http://wordpress.org/plugins/simple-footnotes/).
+
+Requirements
+============
+
+Needs html5lib, so you'll want to `pip install html5lib` before running.
+
+Should work with any content format (ReST, Markdown, whatever), because
+it looks for the `[ref]` and `[/ref]` once the conversion to HTML has happened.
+
+Stuart Langridge, http://www.kryogenix.org/, February 2014.

+ 1 - 0
simple_footnotes/__init__.py

@@ -0,0 +1 @@
+from .simple_footnotes import *

+ 91 - 0
simple_footnotes/simple_footnotes.py

@@ -0,0 +1,91 @@
+from pelican import signals
+import re
+import html5lib
+
+RAW_FOOTNOTE_CONTAINERS = ["code"]
+
+def getText(node, recursive = False):
+    """Get all the text associated with this node.
+       With recursive == True, all text from child nodes is retrieved."""
+    L = ['']
+    for n in node.childNodes:
+        if n.nodeType in (node.TEXT_NODE, node.CDATA_SECTION_NODE):
+            L.append(n.data)
+        else:
+            if not recursive:
+                return None
+        L.append(getText(n) )
+    return ''.join(L)
+
+def parse_for_footnotes(article_generator):
+    for article in article_generator.articles:
+        if "[ref]" in article._content and "[/ref]" in article._content:
+            content = article._content.replace("[ref]", "<x-simple-footnote>").replace("[/ref]", "</x-simple-footnote>")
+            parser = html5lib.HTMLParser(tree=html5lib.getTreeBuilder("dom"))
+            dom = parser.parse(content)
+            endnotes = []
+            count = 0
+            for footnote in dom.getElementsByTagName("x-simple-footnote"):
+                pn = footnote
+                leavealone = False
+                while pn:
+                    if pn.nodeName in RAW_FOOTNOTE_CONTAINERS:
+                        leavealone = True
+                        break
+                    pn = pn.parentNode
+                if leavealone:
+                    continue
+                count += 1
+                fnid = "sf-%s-%s" % (article.slug, count)
+                fnbackid = "%s-back" % (fnid,)
+                endnotes.append((footnote, fnid, fnbackid))
+                number = dom.createElement("sup")
+                number.setAttribute("id", fnbackid)
+                numbera = dom.createElement("a")
+                numbera.setAttribute("href", "#%s" % fnid)
+                numbera.setAttribute("class", "simple-footnote")
+                numbera.appendChild(dom.createTextNode(str(count)))
+                txt = getText(footnote, recursive=True).replace("\n", " ")
+                numbera.setAttribute("title", txt)
+                number.appendChild(numbera)
+                footnote.parentNode.insertBefore(number, footnote)
+            if endnotes:
+                ol = dom.createElement("ol")
+                ol.setAttribute("class", "simple-footnotes")
+                for e, fnid, fnbackid in endnotes:
+                    li = dom.createElement("li")
+                    li.setAttribute("id", fnid)
+                    while e.firstChild:
+                        li.appendChild(e.firstChild)
+                    backlink = dom.createElement("a")
+                    backlink.setAttribute("href", "#%s" % fnbackid)
+                    backlink.setAttribute("class", "simple-footnote-back")
+                    backlink.appendChild(dom.createTextNode(u'\u21a9'))
+                    li.appendChild(dom.createTextNode(" "))
+                    li.appendChild(backlink)
+                    ol.appendChild(li)
+                    e.parentNode.removeChild(e)
+                dom.getElementsByTagName("body")[0].appendChild(ol)
+                s = html5lib.serializer.htmlserializer.HTMLSerializer(omit_optional_tags=False, quote_attr_values=True)
+                output_generator = s.serialize(html5lib.treewalkers.getTreeWalker("dom")(dom.getElementsByTagName("body")[0]))
+                article._content =  "".join(list(output_generator)).replace(
+                    "<x-simple-footnote>", "[ref]").replace("</x-simple-footnote>", "[/ref]").replace(
+                    "<body>", "").replace("</body>", "")
+        if False:
+            count = 0
+            endnotes = []
+            for f in footnotes:
+                count += 1
+                fnstr = '<a class="simple-footnote" name="%s-%s-back" href="#%s-%s"><sup>%s</a>' % (
+                    article.slug, count, article.slug, count, count)
+                endstr = '<li id="%s-%s">%s <a href="#%s-%s-back">&uarr;</a></li>' % (
+                    article.slug, count, f[len("[ref]"):-len("[/ref]")], article.slug, count)
+                content = content.replace(f, fnstr)
+                endnotes.append(endstr)
+            content += '<h4>Footnotes</h4><ol class="simple-footnotes">%s</ul>' % ("\n".join(endnotes),)
+            article._content = content
+
+
+def register():
+    signals.article_generator_finalized.connect(parse_for_footnotes)
+

+ 33 - 0
simple_footnotes/test_simple_footnotes.py

@@ -0,0 +1,33 @@
+import unittest
+from simple_footnotes import parse_for_footnotes
+
+class PseudoArticleGenerator(object):
+    articles = []
+class PseudoArticle(object):
+    _content = ""
+    slug = "article"
+
+class TestFootnotes(unittest.TestCase):
+
+    def _expect(self, input, expected_output):
+        ag = PseudoArticleGenerator()
+        art = PseudoArticle()
+        art._content = input
+        ag.articles = [art]
+        parse_for_footnotes(ag)
+        self.assertEqual(art._content, expected_output)
+
+    def test_simple(self):
+        self._expect("words[ref]footnote[/ref]end",
+        ('words<sup id="sf-article-1-back"><a title="footnote" '
+         'href="#sf-article-1" class="simple-footnote">1</a></sup>end'
+         '<ol class="simple-footnotes">'
+         u'<li id="sf-article-1">footnote <a href="#sf-article-1-back" class="simple-footnote-back">\u21a9</a></li>'
+         '</ol>'))
+
+    def test_no_footnote_inside_code(self):
+        self._expect("words<code>this is code[ref]footnote[/ref] end code </code> end",
+            "words<code>this is code[ref]footnote[/ref] end code </code> end")
+
+if __name__ == '__main__':
+    unittest.main()

+ 26 - 0
static_comments/Readme.md

@@ -0,0 +1,26 @@
+Static comments
+---------------
+
+This plugin allows you to add static comments to an article. By default the
+plugin looks for the comments of each article in a local file named
+``comments/{slug}.md``, where ``{slug}`` is the value of the slug tag for the
+article. The comments file should be formatted using markdown.
+
+Set the ``STATIC_COMMENTS`` parameter to True to enable the plugin. Default is
+False.
+
+Set the ``STATIC_COMMENTS_DIR`` parameter to the directory where the comments
+are located. Default is ``comments``.
+
+On the template side, you just have to add a section for the comments to your
+``article.html``, as in this example::
+
+    {% if STATIC_COMMENTS %}
+    <section id="comments" class="body">
+    <h2>Comments!</h2>
+    {{ article.metadata.static_comments }}
+    </section>
+    {% endif %}
+
+Here is an example of usage:
+<http://jesrui.sdf-eu.org/pelican-static-comments.html>

+ 1 - 0
static_comments/__init__.py

@@ -0,0 +1 @@
+from .static_comments import *

+ 46 - 0
static_comments/static_comments.py

@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+import codecs
+import logging
+import markdown
+import os
+
+logger = logging.getLogger(__name__)
+
+from pelican import signals
+
+
+def initialized(pelican):
+    from pelican.settings import DEFAULT_CONFIG
+    DEFAULT_CONFIG.setdefault('STATIC_COMMENTS', False)
+    DEFAULT_CONFIG.setdefault('STATIC_COMMENTS_DIR' 'comments')
+    if pelican:
+        pelican.settings.setdefault('STATIC_COMMENTS', False)
+        pelican.settings.setdefault('STATIC_COMMENTS_DIR', 'comments')
+
+
+def add_static_comments(gen, metadata):
+    if gen.settings['STATIC_COMMENTS'] != True:
+        return
+
+    if not 'slug' in metadata:
+        logger.warning("static_comments: "
+                "cant't locate comments file without slug tag in the article")
+        return
+
+    fname = os.path.join(gen.settings['STATIC_COMMENTS_DIR'],
+            metadata['slug'] + ".md")
+
+    if not os.path.exists(fname):
+        return
+
+    input_file = codecs.open(fname, mode="r", encoding="utf-8")
+    text = input_file.read()
+    html = markdown.markdown(text)
+
+    metadata['static_comments'] = html
+
+
+def register():
+    signals.initialized.connect(initialized)
+    signals.article_generator_context.connect(add_static_comments)

+ 1 - 1
subcategory/README.md

@@ -28,7 +28,7 @@ breadcrumb-style navigation you might try something like this:
         </li>
         </li>
     {% for subcategory in article.subcategories %}
     {% for subcategory in article.subcategories %}
         <li>
         <li>
-            <a href="{{ SITEURL }}/{{ subcategory.url }}>{{ subcategory.shortname }}</a>
+            <a href="{{ SITEURL }}/{{ subcategory.url }}">{{ subcategory.shortname }}</a>
         </li>
         </li>
     {% endfor %}
     {% endfor %}
     </ol>
     </ol>

+ 118 - 0
twitter_bootstrap_rst_directives/Demo.rst

@@ -0,0 +1,118 @@
+This will be turned into :abbr:`HTML (HyperText Markup Language)`.
+
+Love this music :glyph:`music` 
+
+.. role:: story_time_glyph(glyph)
+    :target: http://www.youtube.com/watch?v=5g8ykQLYnX0
+    :class: small text-info  
+
+Love this music :story_time_glyph:`music` 
+
+This is an example of code: :code:`<example>`.
+
+This is an example of kbd: :kbd:`<example>`.
+
+
+.. label-default::
+    
+    This is a default label content
+
+.. label-primary::
+    
+    This is a primary label content
+
+.. label-success::
+    
+    This is a success label content
+
+.. label-info::
+    
+    This is a info label content
+
+.. label-warning::
+    
+    This is a warning label content
+
+.. label-danger::
+    
+    This is a danger label content
+
+
+.. panel-default::
+    :title: panel default title
+    
+    This is a default panel content
+
+.. panel-primary::
+    :title: panel primary title
+    
+    This is a primary panel content
+
+.. panel-success::
+    :title: panel success title
+    
+    This is a success panel content
+
+.. panel-info::
+    :title: panel info title
+    
+    This is a info panel content
+
+.. panel-warning::
+    :title: panel warning title
+    
+    This is a warning panel content
+
+.. panel-danger::
+    :title: panel danger title
+    
+    This is a danger panel content
+
+
+.. alert-success::
+    
+    This is a success alert content
+
+.. alert-info::
+    
+    This is a info alert content
+
+.. alert-warning::
+    
+    This is a warning alert content
+
+.. alert-danger::
+    
+    This is a danger alert content
+
+        
+.. media:: http://stuffkit.com/wp-content/uploads/2012/11/Worlds-Most-Beautiful-Lady-Camilla-Belle-HD-Photos-4.jpg
+                :height: 750
+                :width: 1000
+                :scale: 20
+                :target: http://www.google.com
+                :alt: Camilla Belle
+                :position: left
+
+                .. class:: h3
+
+                    left position
+
+                This image is not mine. Credit goes to http://stuffkit.com
+                
+
+
+.. media:: http://stuffkit.com/wp-content/uploads/2012/11/Worlds-Most-Beautiful-Lady-Camilla-Belle-HD-Photos-4.jpg
+                :height: 750
+                :width: 1000
+                :scale: 20
+                :target: http://www.google.com
+                :alt: Camilla Belle
+                :position: right
+
+                .. class:: h3
+
+                    right position
+
+
+                This image is not mine. Credit goes to http://stuffkit.com

+ 46 - 0
twitter_bootstrap_rst_directives/Readme.rst

@@ -0,0 +1,46 @@
+Twitter Bootstrap Directive for restructured text
+-------------------------------------------------
+
+This plugin defines some rst directive that enable a clean usage of the twitter bootstrap CSS and Javascript components.
+
+Directives
+----------
+
+Implemented directives:
+
+    label,
+    alert,
+    panel,
+    media
+
+Implemented roles:
+
+    glyph,
+    code,
+    kbd
+
+Usage
+-----
+
+For more informations about the usage of each directive, read the corresponding class description.
+Or checkout this demo page.
+
+Dependencies
+------------
+
+In order to use this plugin, you need to use a template that supports bootstrap 3.1.1 with the glyph font setup
+correctly. Usually you should have this structure::
+
+    static
+    ├──  css
+    |     └──  bootstrap.min.css
+    ├──  font
+    |     └──  glyphicons-halflings-regular.ttf
+    └──  js
+          └──     
+
+Warning
+-------
+
+In order to support some unique features and avoid conflicts with bootstrap, this plugin will use a custom html writer which
+is modifying the traditional docutils output.

+ 4 - 0
twitter_bootstrap_rst_directives/__init__.py

@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from .bootstrap_rst_directives import *

+ 543 - 0
twitter_bootstrap_rst_directives/bootstrap_rst_directives.py

@@ -0,0 +1,543 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+"""
+Twitter Bootstrap RST directives Plugin For Pelican
+===================================================
+
+This plugin defines rst directives for different CSS and Javascript components from
+the twitter bootstrap framework.
+
+"""
+
+from uuid import uuid1
+
+from cgi import escape
+from docutils import nodes, utils
+import docutils
+from docutils.parsers import rst
+from docutils.parsers.rst import directives, roles, Directive
+from pelican import signals
+from pelican.readers import RstReader, PelicanHTMLTranslator
+
+
+
+class CleanHTMLTranslator(PelicanHTMLTranslator):
+
+    """
+        A custom HTML translator based on the Pelican HTML translator.
+        Used to clean up some components html classes that could conflict 
+        with the bootstrap CSS classes.
+        Also defines new tags that are not handleed by the current implementation of 
+        docutils.
+
+        The most obvious example is the Container component
+    """
+
+    def visit_literal(self, node):
+        classes = node.get('classes', node.get('class', []))
+        if 'code' in classes:
+            self.body.append(self.starttag(node, 'code'))
+        elif 'kbd' in classes:
+            self.body.append(self.starttag(node, 'kbd'))
+        else:
+            self.body.append(self.starttag(node, 'pre'))
+
+    def depart_literal(self, node):
+        classes = node.get('classes', node.get('class', []))
+        if 'code' in classes:
+            self.body.append('</code>\n')
+        elif 'kbd' in classes:
+            self.body.append('</kbd>\n')
+        else:
+            self.body.append('</pre>\n')
+
+    def visit_container(self, node):
+        self.body.append(self.starttag(node, 'div'))
+
+
+class CleanRSTReader(RstReader):
+
+    """
+        A custom RST reader that behaves exactly like its parent class RstReader with
+        the difference that it uses the CleanHTMLTranslator
+    """
+
+    def _get_publisher(self, source_path):
+        extra_params = {'initial_header_level': '2',
+                        'syntax_highlight': 'short',
+                        'input_encoding': 'utf-8'}
+        user_params = self.settings.get('DOCUTILS_SETTINGS')
+        if user_params:
+            extra_params.update(user_params)
+
+        pub = docutils.core.Publisher(
+            destination_class=docutils.io.StringOutput)
+        pub.set_components('standalone', 'restructuredtext', 'html')
+        pub.writer.translator_class = CleanHTMLTranslator
+        pub.process_programmatic_settings(None, extra_params, None)
+        pub.set_source(source_path=source_path)
+        pub.publish()
+        return pub
+
+
+def keyboard_role(name, rawtext, text, lineno, inliner,
+                  options={}, content=[]):
+    """
+        This function creates an inline console input block as defined in the twitter bootstrap documentation
+        overrides the default behaviour of the kbd role
+
+        *usage:*
+            :kbd:`<your code>`
+
+        *Example:*
+
+            :kbd:`<section>`
+
+        This code is not highlighted
+    """
+    new_element = nodes.literal(rawtext, text)
+    new_element.set_class('kbd')
+
+    return [new_element], []
+
+
+def code_role(name, rawtext, text, lineno, inliner,
+              options={}, content=[]):
+    """
+        This function creates an inline code block as defined in the twitter bootstrap documentation
+        overrides the default behaviour of the code role
+
+        *usage:*
+            :code:`<your code>`
+
+        *Example:*
+
+            :code:`<section>`
+
+        This code is not highlighted
+    """
+    new_element = nodes.literal(rawtext, text)
+    new_element.set_class('code')
+
+    return [new_element], []
+
+
+def glyph_role(name, rawtext, text, lineno, inliner,
+               options={}, content=[]):
+    """
+        This function defines a glyph inline role that show a glyph icon from the 
+        twitter bootstrap framework
+
+        *Usage:*
+
+            :glyph:`<glyph_name>`
+
+        *Example:*
+
+            Love this music :glyph:`music` :)
+
+        Can be subclassed to include a target
+
+        *Example:*
+
+            .. role:: story_time_glyph(glyph)
+                :target: http://www.youtube.com/watch?v=5g8ykQLYnX0
+                :class: small text-info
+
+            Love this music :story_time_glyph:`music` :)
+
+    """
+
+    target = options.get('target', None)
+    glyph_name = 'glyphicon-{}'.format(text)
+
+    if target:
+        target = utils.unescape(target)
+        new_element = nodes.reference(rawtext, ' ', refuri=target)
+    else:
+        new_element = nodes.container()
+    classes = options.setdefault('class', [])
+    classes += ['glyphicon', glyph_name]
+    for custom_class in classes:
+        new_element.set_class(custom_class)
+    return [new_element], []
+
+glyph_role.options = {
+    'target': rst.directives.unchanged,
+}
+glyph_role.content = False
+
+
+class Label(rst.Directive):
+
+    '''
+        generic Label directive class definition.
+        This class define a directive that shows 
+        bootstrap Labels around its content
+
+        *usage:*
+
+            .. label-<label-type>::
+
+                <Label content>
+
+        *example:*
+
+            .. label-default::
+
+                This is a default label content
+
+    '''
+
+    has_content = True
+    custom_class = ''
+
+    def run(self):
+        # First argument is the name of the glyph
+        label_name = 'label-{}'.format(self.custom_class)
+        # get the label content
+        text = '\n'.join(self.content)
+        # Create a new container element (div)
+        new_element = nodes.container(text)
+        # Update its content
+        self.state.nested_parse(self.content, self.content_offset,
+                                new_element)
+        # Set its custom bootstrap classes
+        new_element['classes'] += ['label ', label_name]
+        # Return one single element
+        return [new_element]
+
+
+class DefaultLabel(Label):
+
+    custom_class = 'default'
+
+
+class PrimaryLabel(Label):
+
+    custom_class = 'primary'
+
+
+class SuccessLabel(Label):
+
+    custom_class = 'success'
+
+
+class InfoLabel(Label):
+
+    custom_class = 'info'
+
+
+class WarningLabel(Label):
+
+    custom_class = 'warning'
+
+
+class DangerLabel(Label):
+
+    custom_class = 'danger'
+
+
+class Panel(rst.Directive):
+
+    """
+        generic Panel directive class definition.
+        This class define a directive that shows 
+        bootstrap Labels around its content
+
+        *usage:*
+
+            .. panel-<panel-type>:: 
+                :title: <title>
+
+                <Panel content>
+
+        *example:*
+
+            .. panel-default:: 
+                :title: panel title
+
+                This is a default panel content
+
+    """
+
+    has_content = True
+    option_spec = {
+        'title': rst.directives.unchanged,
+    }
+    custom_class = ''
+
+    def run(self):
+        # First argument is the name of the glyph
+        panel_name = 'panel-{}'.format(self.custom_class)
+        # get the label title
+        title_text = self.options.get('title', self.custom_class.title())
+        # get the label content
+        text = '\n'.join(self.content)
+        # Create the panel element
+        panel_element = nodes.container()
+        panel_element['classes'] += ['panel', panel_name]
+        # Create the panel headings
+        heading_element = nodes.container(title_text)
+        title_nodes, messages = self.state.inline_text(title_text,
+                                                       self.lineno)
+        title = nodes.paragraph(title_text, '', *title_nodes)
+        heading_element.append(title)
+        heading_element['classes'] += ['panel-heading']
+        # Create a new container element (div)
+        body_element = nodes.container(text)
+        # Update its content
+        self.state.nested_parse(self.content, self.content_offset,
+                                body_element)
+        # Set its custom bootstrap classes
+        body_element['classes'] += ['panel-body']
+        # add the heading and body to the panel
+        panel_element.append(heading_element)
+        panel_element.append(body_element)
+        # Return the panel element
+        return [panel_element]
+
+
+class DefaultPanel(Panel):
+
+    custom_class = 'default'
+
+
+class PrimaryPanel(Panel):
+
+    custom_class = 'primary'
+
+
+class SuccessPanel(Panel):
+
+    custom_class = 'success'
+
+
+class InfoPanel(Panel):
+
+    custom_class = 'info'
+
+
+class WarningPanel(Panel):
+
+    custom_class = 'warning'
+
+
+class DangerPanel(Panel):
+
+    custom_class = 'danger'
+
+
+class Alert(rst.Directive):
+
+    """
+        generic Alert directive class definition.
+        This class define a directive that shows 
+        bootstrap Labels around its content
+
+        *usage:*
+
+            .. alert-<alert-type>::
+
+                <alert content>
+
+        *example:*
+
+            .. alert-default::
+
+                This is a default alert content
+
+    """
+    has_content = True
+    custom_class = ''
+
+    def run(self):
+        # First argument is the name of the glyph
+        alert_name = 'alert-{}'.format(self.custom_class)
+        # get the label content
+        text = '\n'.join(self.content)
+        # Create a new container element (div)
+        new_element = nodes.compound(text)
+        # Update its content
+        self.state.nested_parse(self.content, self.content_offset,
+                                new_element)
+        # Recurse inside its children and change the hyperlinks classes
+        for child in new_element.traverse(include_self=False):
+            if isinstance(child, nodes.reference):
+                child.set_class('alert-link')
+        # Set its custom bootstrap classes
+        new_element['classes'] += ['alert ', alert_name]
+        # Return one single element
+        return [new_element]
+
+
+class SuccessAlert(Alert):
+
+    custom_class = 'success'
+
+
+class InfoAlert(Alert):
+
+    custom_class = 'info'
+
+
+class WarningAlert(Alert):
+
+    custom_class = 'warning'
+
+
+class DangerAlert(Alert):
+
+    custom_class = 'danger'
+
+
+class Media(rst.Directive):
+
+    '''
+        generic Media directive class definition.
+        This class define a directive that shows 
+        bootstrap media image with text according
+        to the media component on bootstrap
+
+        *usage*:
+            .. media:: <image_uri>
+                :position: <position>
+                :alt: <alt>
+                :height: <height>
+                :width: <width>
+                :scale: <scale>
+                :target: <target>
+
+                <text content>
+
+        *example*:
+            .. media:: http://stuffkit.com/wp-content/uploads/2012/11/Worlds-Most-Beautiful-Lady-Camilla-Belle-HD-Photos-4.jpg
+                :height: 750
+                :width: 1000
+                :scale: 20
+                :target: www.google.com
+                :alt: Camilla Belle
+                :position: left
+
+                This image is not mine. Credit goes to http://stuffkit.com
+
+
+
+    '''
+
+    has_content = True
+    required_arguments = 1
+
+    option_spec = {
+        'position': str,
+        'alt': rst.directives.unchanged,
+        'height': rst.directives.length_or_unitless,
+        'width': rst.directives.length_or_percentage_or_unitless,
+        'scale': rst.directives.percentage,
+        'target': rst.directives.unchanged_required,
+    }
+
+    def get_image_element(self):
+        # Get the image url
+        image_url = self.arguments[0]
+        image_reference = rst.directives.uri(image_url)
+        self.options['uri'] = image_reference
+
+        reference_node = None
+        messages = []
+        if 'target' in self.options:
+            block = rst.states.escape2null(
+                self.options['target']).splitlines()
+            block = [line for line in block]
+            target_type, data = self.state.parse_target(
+                block, self.block_text, self.lineno)
+            if target_type == 'refuri':
+                container_node = nodes.reference(refuri=data)
+            elif target_type == 'refname':
+                container_node = nodes.reference(
+                    refname=fully_normalize_name(data),
+                    name=whitespace_normalize_name(data))
+                container_node.indirect_reference_name = data
+                self.state.document.note_refname(container_node)
+            else:                           # malformed target
+                messages.append(data)       # data is a system message
+            del self.options['target']
+        else:
+            container_node = nodes.container()
+
+        # get image position
+        position = self.options.get('position', 'left')
+        position_class = 'pull-{}'.format(position)
+
+        container_node.set_class(position_class)
+
+        image_node = nodes.image(self.block_text, **self.options)
+        image_node['classes'] += ['media-object']
+
+        container_node.append(image_node)
+        return container_node
+
+    def run(self):
+        # now we get the content
+        text = '\n'.join(self.content)
+
+        # get image alternative text
+        alternative_text = self.options.get('alternative-text', '')
+
+        # get container element
+        container_element = nodes.container()
+        container_element['classes'] += ['media']
+
+        # get image element
+        image_element = self.get_image_element()
+
+        # get body element
+        body_element = nodes.container(text)
+        body_element['classes'] += ['media-body']
+        self.state.nested_parse(self.content, self.content_offset,
+                                body_element)
+
+        container_element.append(image_element)
+        container_element.append(body_element)
+        return [container_element, ]
+
+
+def register_directives():
+    rst.directives.register_directive('label-default', DefaultLabel)
+    rst.directives.register_directive('label-primary', PrimaryLabel)
+    rst.directives.register_directive('label-success', SuccessLabel)
+    rst.directives.register_directive('label-info', InfoLabel)
+    rst.directives.register_directive('label-warning', WarningLabel)
+    rst.directives.register_directive('label-danger', DangerLabel)
+
+    rst.directives.register_directive('panel-default', DefaultPanel)
+    rst.directives.register_directive('panel-primary', PrimaryPanel)
+    rst.directives.register_directive('panel-success', SuccessPanel)
+    rst.directives.register_directive('panel-info', InfoPanel)
+    rst.directives.register_directive('panel-warning', WarningPanel)
+    rst.directives.register_directive('panel-danger', DangerPanel)
+
+    rst.directives.register_directive('alert-success', SuccessAlert)
+    rst.directives.register_directive('alert-info', InfoAlert)
+    rst.directives.register_directive('alert-warning', WarningAlert)
+    rst.directives.register_directive('alert-danger', DangerAlert)
+
+    rst.directives.register_directive( 'media', Media )
+
+
+def register_roles():
+    rst.roles.register_local_role('glyph', glyph_role)
+    rst.roles.register_local_role('code', code_role)
+    rst.roles.register_local_role('kbd', keyboard_role)
+
+
+def add_reader(readers):
+    readers.reader_classes['rst'] = CleanRSTReader
+
+
+def register():
+    register_directives()
+    register_roles()
+    signals.readers_init.connect(add_reader)