Преглед на файлове

Merge pull request #190 from Scheirle/master

[pelican_comment_system] Added Avatars, Identicons and Comment Atom Feed
Justin Mayer преди 10 години
родител
ревизия
c96846c201

+ 22 - 183
pelican_comment_system/Readme.md

@@ -1,192 +1,31 @@
 # Pelican comment system
-The pelican comment system allows you to add static comments to your articles. It also supports replies to comments.
-
+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.
 
-See it in action here: [blog.scheirle.de](http://blog.scheirle.de/posts/2014/March/29/static-comments-via-email/)
+#### 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
 
-Thanks to jesrui the author of [Static comments](https://github.com/getpelican/pelican-plugins/tree/master/static_comments). I reused some code from it.
+
+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>
 
-## 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
-
-
-### 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 (without 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
-`replyto`     | no        | Identifier of the parent comment. Identifier = Filename (without extension)
-`locale_date` | forbidden | Will be overwritten with a locale representation of the date
-
-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.metadata.comments_count` | Amount of total comments for this article (including replies to comments)
-`article.metadata.comments`       | Array containing the top level comments for this article (no replies to comments)
-
-#### Comment object
-Variables  | Description
------------|--------------------------
-`id`       | Identifier of this comment
-`content`  | Content of this comment
-`metadata` | All metadata as in the comment file (or described above)
-`replies`  | Array containing the top level replies for this comment
-
-##### Example article.html theme
-(only the comment section)
-
-```html
-{% if article.metadata.comments %}
-	{% for comment in article.metadata.comments recursive %}
-		{% set metadata = comment.metadata %}
-		{% 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>{{ metadata['author'] }}</h4>
-				<p>Posted on <abbr class="published" title="{{ metadata['date'].isoformat() }}">{{ metadata['locale_date'] }}</abbr></p>
-
-				{{ comment.content }}
-				{% if comment.replies %}
-					{{ loop(comment.replies) }}
-				{% endif %}
-			</article>
-	{% endfor %}
-{% else %}
-	<p>There are no comments yet.<p>
-{% endif %}
-```
-## Recommendation
-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 above `for` loop, so your visitors can reply to a comment.
-
-```html
-<button onclick="reply('{{comment.id | urlencode}}');">Reply</button>
-```
-
-##### form + javascript
-
-```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>
-```
-
-```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`.
+## 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).

+ 94 - 0
pelican_comment_system/avatars.py

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

+ 45 - 0
pelican_comment_system/comment.py

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

+ 35 - 0
pelican_comment_system/doc/avatars.md

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

+ 28 - 0
pelican_comment_system/doc/feed.md

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

+ 83 - 0
pelican_comment_system/doc/form.md

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

+ 106 - 0
pelican_comment_system/doc/installation.md

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

Файловите разлики са ограничени, защото са твърде много
+ 11 - 0
pelican_comment_system/identicon/LICENSE


+ 17 - 0
pelican_comment_system/identicon/README.md

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

+ 0 - 0
pelican_comment_system/identicon/__init__.py


+ 256 - 0
pelican_comment_system/identicon/identicon.py

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

+ 62 - 55
pelican_comment_system/pelican_comment_system.py

@@ -7,92 +7,94 @@ 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.utils import strftime
 from pelican.readers import MarkdownReader
+from pelican.writers import Writer
 
-class Comment:
-	def __init__(self, id, metadata, content):
-		self.id = id
-		self.content = content
-		self.metadata = metadata
-		self.replies = []
-
-	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']
+from . comment import Comment
+from . import avatars
 
-	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)
-
-
-def initialized(pelican):
+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')
-
-
-def add_static_comments(gen, metadata):
+		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
 
-	metadata['comments_count'] = 0
-	metadata['comments'] = []
+	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)
 
-	if not 'slug' in metadata:
-		logger.warning("pelican_comment_system: cant't locate comments files without slug tag in the article")
+	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 = []
-	folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], metadata['slug'])
-
-	if not os.path.isdir(folder):
-		logger.debug("No comments found for: " + metadata['slug'])
-		return
 
 	for file in os.listdir(folder):
 		name, extension = os.path.splitext(file)
 		if extension[1:].lower() in reader.file_extensions:
-			content, meta = reader.read(folder + "/" + file)
-			meta['locale_date'] = strftime(meta['date'], gen.settings['DEFAULT_DATE_FORMAT'])
-			com = Comment(name, meta, content)
+			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):
@@ -106,9 +108,14 @@ def add_static_comments(gen, metadata):
 
 	comments = sorted(comments)
 
-	metadata['comments_count'] = len(comments) + count
-	metadata['comments'] = comments
+	content.comments_count = len(comments) + count
+	content.comments = comments
+
+def writeIdenticonsToDisk(gen, writer):
+	avatars.generateAndSaveMissingAvatars()
 
 def register():
-	signals.initialized.connect(initialized)
-	signals.article_generator_context.connect(add_static_comments)
+	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)