attempt to merge master with collapsible code

This commit is contained in:
Jörg Dietrich
2014-05-02 13:51:33 +02:00
42 changed files with 2556 additions and 152 deletions

35
creole_reader/Readme.md Normal file
View File

@@ -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
```

View File

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

View File

@@ -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)

View File

@@ -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::

View File

@@ -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
latex Symbolic link
View File

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

View File

@@ -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).

View File

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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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
liquid_tags/vimeo.py Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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).

View File

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

View File

@@ -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')

View File

@@ -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)

View File

@@ -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).

View File

@@ -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 %}
```

View File

@@ -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`.

View File

@@ -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 %}
```

View File

@@ -0,0 +1,11 @@
identicon.py is Licesensed under FreeBSD License.
(http://www.freebsd.org/copyright/freebsd-license.html)
Copyright 1994-2009 Shin Adachi. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -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`.

View File

@@ -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')

View File

@@ -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
render_math/Readme.md Normal file
View File

@@ -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
render_math/__init__.py Normal file
View File

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

379
render_math/math.py Normal file
View File

@@ -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)

View File

@@ -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>

View File

@@ -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.

View File

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

View File

@@ -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)

View File

@@ -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
static_comments/Readme.md Normal file
View File

@@ -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>

View File

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

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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.

View File

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

View File

@@ -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)