diff --git a/creole_reader/Readme.md b/creole_reader/Readme.md new file mode 100644 index 0000000..d478bf2 --- /dev/null +++ b/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 `<
> <
>` +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 +``` +<
> +title: Créole +tags: creole, python, pelican_open +date: 2013-12-12 +<
> + += Title 1 +== Title 2 + +Some nice texte with **strong** and //emphasis//. + +* A nice list +** With subelements +* Python + +# An ordered list +# A second item +``` diff --git a/creole_reader/__init__.py b/creole_reader/__init__.py new file mode 100644 index 0000000..2e26d46 --- /dev/null +++ b/creole_reader/__init__.py @@ -0,0 +1 @@ +from .creole_reader import * diff --git a/creole_reader/creole_reader.py b/creole_reader/creole_reader.py new file mode 100644 index 0000000..49317c5 --- /dev/null +++ b/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) diff --git a/github_activity/Readme.rst b/github_activity/Readme.rst index fa3b95d..5e17eb0 100644 --- a/github_activity/Readme.rst +++ b/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:: diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index fb3b2b0..76e3405 100644 --- a/github_activity/github_activity.py +++ b/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): diff --git a/latex b/latex new file mode 120000 index 0000000..4a2d98c --- /dev/null +++ b/latex @@ -0,0 +1 @@ +render_math/ \ No newline at end of file diff --git a/latex/Readme.md b/latex/Readme.md deleted file mode 100644 index 51424ce..0000000 --- a/latex/Readme.md +++ /dev/null @@ -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 `` 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). diff --git a/latex/__init__.py b/latex/__init__.py deleted file mode 100644 index 1b2ce76..0000000 --- a/latex/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .latex import * \ No newline at end of file diff --git a/latex/latex.py b/latex/latex.py deleted file mode 100644 index 1d4c579..0000000 --- a/latex/latex.py +++ /dev/null @@ -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 = """ - -""" - -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) diff --git a/liquid_tags/Readme.md b/liquid_tags/Readme.md index 0ddfe37..f2c83bf 100644 --- a/liquid_tags/Readme.md +++ b/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: diff --git a/liquid_tags/notebook.py b/liquid_tags/notebook.py index f155e71..54b4aa2 100644 --- a/liquid_tags/notebook.py +++ b/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('
', '
')
 
 
@@ -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:
diff --git a/liquid_tags/vimeo.py b/liquid_tags/vimeo.py
new file mode 100644
index 0000000..6dc929a
--- /dev/null
+++ b/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
+------
+
+ +[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 = '
'.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 diff --git a/neighbors/neighbors.py b/neighbors/neighbors.py index a6dd2f4..b65fcfe 100755 --- a/neighbors/neighbors.py +++ b/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) diff --git a/pelican_comment_system/Readme.md b/pelican_comment_system/Readme.md new file mode 100644 index 0000000..3b4dfd4 --- /dev/null +++ b/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 | | + +## 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). diff --git a/pelican_comment_system/__init__.py b/pelican_comment_system/__init__.py new file mode 100644 index 0000000..4c8a3ca --- /dev/null +++ b/pelican_comment_system/__init__.py @@ -0,0 +1 @@ +from .pelican_comment_system import * diff --git a/pelican_comment_system/avatars.py b/pelican_comment_system/avatars.py new file mode 100644 index 0000000..d357f19 --- /dev/null +++ b/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') diff --git a/pelican_comment_system/comment.py b/pelican_comment_system/comment.py new file mode 100644 index 0000000..cf5f4c8 --- /dev/null +++ b/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) diff --git a/pelican_comment_system/doc/avatars.md b/pelican_comment_system/doc/avatars.md new file mode 100644 index 0000000..f0567c7 --- /dev/null +++ b/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 +Avatar +``` + +Of cause the `height` and `width` are optional, but they make sure that everything has the same size (in particular specific avatars). diff --git a/pelican_comment_system/doc/feed.md b/pelican_comment_system/doc/feed.md new file mode 100644 index 0000000..949bd25 --- /dev/null +++ b/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 %} + ... +
{{ comment.content }}
+ ... +{% endfor %} +``` +## Theme +#### Link +To display a link to the article feed simply add the following to your theme: + +```html +{% if article %} + Comment Atom Feed +{% endif %} +``` + + diff --git a/pelican_comment_system/doc/form.md b/pelican_comment_system/doc/form.md new file mode 100644 index 0000000..81124a8 --- /dev/null +++ b/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 + +``` + +#### Form +A basic form so your visitors can write comments. + +```html +
+ + + + +
+``` +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 + +``` +(jQuery is required for this script) + +Don't forget to set the Variables `user` and `domain`. diff --git a/pelican_comment_system/doc/installation.md b/pelican_comment_system/doc/installation.md new file mode 100644 index 0000000..97e6c65 --- /dev/null +++ b/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 %} +
+ Permalink +

{{ comment.author }}

+

Posted on {{ comment.locale_date }}

+ {{ comment.metadata['my_custom_metadata'] }} + {{ comment.content }} + {% if comment.replies %} + {{ loop(comment.replies) }} + {% endif %} +
+ {% endfor %} +{% else %} +

There are no comments yet.

+{% endif %} +``` diff --git a/pelican_comment_system/identicon/LICENSE b/pelican_comment_system/identicon/LICENSE new file mode 100755 index 0000000..e6e964f --- /dev/null +++ b/pelican_comment_system/identicon/LICENSE @@ -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. diff --git a/pelican_comment_system/identicon/README.md b/pelican_comment_system/identicon/README.md new file mode 100755 index 0000000..1aa768c --- /dev/null +++ b/pelican_comment_system/identicon/README.md @@ -0,0 +1,17 @@ +identicon.py: identicon python implementation. +============================================== +:Author:Shin Adachi + +## 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`. \ No newline at end of file diff --git a/pelican_comment_system/identicon/__init__.py b/pelican_comment_system/identicon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pelican_comment_system/identicon/identicon.py b/pelican_comment_system/identicon/identicon.py new file mode 100755 index 0000000..eb4abe5 --- /dev/null +++ b/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 + += 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') diff --git a/pelican_comment_system/pelican_comment_system.py b/pelican_comment_system/pelican_comment_system.py new file mode 100644 index 0000000..aecf8a0 --- /dev/null +++ b/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) diff --git a/render_math/Readme.md b/render_math/Readme.md new file mode 100644 index 0000000..1f1fdd8 --- /dev/null +++ b/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 `` and `` 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 +`...` 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: +``` + + + x + = + + + + b + ± + + + + b + 2 + + + 4 + a + c + + + + + 2 + a + + + + +``` diff --git a/render_math/__init__.py b/render_math/__init__.py new file mode 100644 index 0000000..2ac15dd --- /dev/null +++ b/render_math/__init__.py @@ -0,0 +1 @@ +from .math import * diff --git a/render_math/math.py b/render_math/math.py new file mode 100644 index 0000000..2bd292e --- /dev/null +++ b/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\}|)', 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.

 and  tags
+    """
+    ignore_within = []
+
+    # used to detect all 
 and  tags. NOTE: Alter this regex should
+    # additional tags need to be ignored
+    ignore_regex = re.compile(r'<(pre|code)(?:\s.*?)?>.*?', 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 
 and  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' % (_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 = '' % _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  or 
 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 
 or  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''
+        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'' % _WRAP_LATEX if not _WRAP_LATEX is None else ''
+    math_summary_regex = r'((\$\$|\$|\\begin\{(.+?)\}|<(math)(?:\s.*?)?>).+?)(\2|\\end\{\3\}||\s?\.\.\.)(%s|)?' % tag_end
+
+    # NOTE: The logic in _get_summary will handle  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)
diff --git a/render_math/mathjax_script.txt b/render_math/mathjax_script.txt
new file mode 100644
index 0000000..b9c75c3
--- /dev/null
+++ b/render_math/mathjax_script.txt
@@ -0,0 +1,28 @@
+
diff --git a/simple_footnotes/README.md b/simple_footnotes/README.md
new file mode 100644
index 0000000..d4105cc
--- /dev/null
+++ b/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 text1
+
+ 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.
diff --git a/simple_footnotes/__init__.py b/simple_footnotes/__init__.py
new file mode 100644
index 0000000..2f958a8
--- /dev/null
+++ b/simple_footnotes/__init__.py
@@ -0,0 +1 @@
+from .simple_footnotes import *
diff --git a/simple_footnotes/simple_footnotes.py b/simple_footnotes/simple_footnotes.py
new file mode 100644
index 0000000..ea2e480
--- /dev/null
+++ b/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]", "").replace("[/ref]", "")
+            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(
+                    "", "[ref]").replace("", "[/ref]").replace(
+                    "", "").replace("", "")
+        if False:
+            count = 0
+            endnotes = []
+            for f in footnotes:
+                count += 1
+                fnstr = '%s' % (
+                    article.slug, count, article.slug, count, count)
+                endstr = '
  • %s
  • ' % ( + article.slug, count, f[len("[ref]"):-len("[/ref]")], article.slug, count) + content = content.replace(f, fnstr) + endnotes.append(endstr) + content += '

    Footnotes

      %s' % ("\n".join(endnotes),) + article._content = content + + +def register(): + signals.article_generator_finalized.connect(parse_for_footnotes) + diff --git a/simple_footnotes/test_simple_footnotes.py b/simple_footnotes/test_simple_footnotes.py new file mode 100644 index 0000000..04af1dc --- /dev/null +++ b/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", + ('words1end' + '
        ' + u'
      1. footnote \u21a9
      2. ' + '
      ')) + + def test_no_footnote_inside_code(self): + self._expect("wordsthis is code[ref]footnote[/ref] end code end", + "wordsthis is code[ref]footnote[/ref] end code end") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/static_comments/Readme.md b/static_comments/Readme.md new file mode 100644 index 0000000..f95fbc4 --- /dev/null +++ b/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 %} +
      +

      Comments!

      + {{ article.metadata.static_comments }} +
      + {% endif %} + +Here is an example of usage: + diff --git a/static_comments/__init__.py b/static_comments/__init__.py new file mode 100644 index 0000000..9d131b2 --- /dev/null +++ b/static_comments/__init__.py @@ -0,0 +1 @@ +from .static_comments import * diff --git a/static_comments/static_comments.py b/static_comments/static_comments.py new file mode 100644 index 0000000..65c4491 --- /dev/null +++ b/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) diff --git a/subcategory/README.md b/subcategory/README.md index 21c610f..015bea0 100644 --- a/subcategory/README.md +++ b/subcategory/README.md @@ -28,7 +28,7 @@ breadcrumb-style navigation you might try something like this: {% for subcategory in article.subcategories %}
    1. - {{ subcategory.shortname }}
    2. {% endfor %}
    diff --git a/twitter_bootstrap_rst_directives/Demo.rst b/twitter_bootstrap_rst_directives/Demo.rst new file mode 100644 index 0000000..c38ae10 --- /dev/null +++ b/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:``. + +This is an example of kbd: :kbd:``. + + +.. 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 \ No newline at end of file diff --git a/twitter_bootstrap_rst_directives/Readme.rst b/twitter_bootstrap_rst_directives/Readme.rst new file mode 100644 index 0000000..f84417c --- /dev/null +++ b/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. \ No newline at end of file diff --git a/twitter_bootstrap_rst_directives/__init__.py b/twitter_bootstrap_rst_directives/__init__.py new file mode 100644 index 0000000..7dfdc27 --- /dev/null +++ b/twitter_bootstrap_rst_directives/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from .bootstrap_rst_directives import * diff --git a/twitter_bootstrap_rst_directives/bootstrap_rst_directives.py b/twitter_bootstrap_rst_directives/bootstrap_rst_directives.py new file mode 100644 index 0000000..8385134 --- /dev/null +++ b/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('
    \n') + elif 'kbd' in classes: + self.body.append('\n') + else: + self.body.append('
    \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:`` + + *Example:* + + :kbd:`
    ` + + 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:`` + + *Example:* + + :code:`
    ` + + 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:`` + + *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-:: + +