Bläddra i källkod

attempt to merge master with collapsible code

Jörg Dietrich 10 år sedan
förälder
incheckning
42f90c94fb
42 ändrade filer med 2556 tillägg och 152 borttagningar
  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'
 
+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``
 variable, as in this example::
 

+ 2 - 1
github_activity/github_activity.py

@@ -25,6 +25,7 @@ class GitHubActivity():
         import feedparser
         self.activities = feedparser.parse(
             generator.settings['GITHUB_ACTIVITY_FEED'])
+        self.max_entries = generator.settings['GITHUB_ACTIVITY_MAX_ENTRIES'] 
 
     def fetch(self):
         """
@@ -37,7 +38,7 @@ class GitHubActivity():
                     [element for element in [activity['title'],
                         activity['content'][0]['value']]])
 
-        return entries
+        return entries[0:self.max_entries]
 
 
 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'
     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
 
@@ -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
 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
 To insert flash/HTML5-friendly video into a post, enable the
 ``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.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 IPython.nbconvert.exporters import HTMLExporter
@@ -64,9 +69,10 @@ from IPython.config import Config
 from IPython.nbformat import current as nbformat
 
 try:
-    from IPython.nbconvert.transformers import Transformer
+    from IPython.nbconvert.preprocessors import Preprocessor
 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 copy import deepcopy
@@ -111,6 +117,17 @@ pre.ipynb {
   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}
 
 div.collapseheader {
@@ -169,7 +186,7 @@ CSS_WRAPPER = """
 
 
 #----------------------------------------------------------------------
-# Create a custom transformer
+# Create a custom preprocessor
 class SliceIndex(Integer):
     """An integer trait that accepts None"""
     default_value = None
@@ -181,28 +198,32 @@ class SliceIndex(Integer):
             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"""
     start = SliceIndex(0, config=True,
                        help="first cell of notebook to be converted")
     end = SliceIndex(None, config=True,
                      help="last cell of notebook to be converted")
 
-    def call(self, nb, resources):
+    def preprocess(self, nb, resources):
         nbc = deepcopy(nb)
-        for worksheet in nbc.worksheets :
+        for worksheet in nbc.worksheets:
             cells = worksheet.cells[:]
             worksheet.cells = cells[self.start:self.end]
         return nbc, resources
 
+    call = preprocess # IPython < 2.0
+
 
 
 #----------------------------------------------------------------------
 # Custom highlighter:
 #  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')
-    output = _pygment_highlight(source, formatter, language)
+    if not language:
+        language = 'ipython'
+    output = _pygments_highlight(source, formatter, language)
     return output.replace('<pre>', '<pre class="ipynb">')
 
 
@@ -252,10 +273,17 @@ def notebook(preprocessor, tag, markup):
         template_file = 'pelicanhtml'
     else:
         template_file = 'basic'
+    
+    if LooseVersion(IPython.__version__) >= '2.0':
+        subcell_kwarg = dict(preprocessors=[SubCell])
+    else:
+        subcell_kwarg = dict(transformers=[SubCell])
+    
     exporter = HTMLExporter(config=c,
                             template_file=template_file,
                             filters={'highlight2html': custom_highlighter},
-                            transformers=[SubCell])
+                            extra_loaders=[pelican_loader],
+                            **subcell_kwarg)
 
     # read and parse the notebook
     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))
         set_neighbors(
             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():
     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 %}
+```

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 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>
     {% for subcategory in article.subcategories %}
         <li>
-            <a href="{{ SITEURL }}/{{ subcategory.url }}>{{ subcategory.shortname }}</a>
+            <a href="{{ SITEURL }}/{{ subcategory.url }}">{{ subcategory.shortname }}</a>
         </li>
     {% endfor %}
     </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)