Browse Source

Merge pull request #190 from Scheirle/master

[pelican_comment_system] Added Avatars, Identicons and Comment Atom Feed
Justin Mayer 11 years ago
parent
commit
c96846c201

+ 22 - 183
pelican_comment_system/Readme.md

@@ -1,192 +1,31 @@
 # Pelican comment system
 # 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.
 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
 Author             | Website                   | Github
 -------------------|---------------------------|------------------------------
 -------------------|---------------------------|------------------------------
 Bernhard Scheirle  | <http://blog.scheirle.de> | <https://github.com/Scheirle>
 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 %}
+```

File diff suppressed because it is too large
+ 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
 Author: Bernhard Scheirle
 """
 """
-
+from __future__ import unicode_literals
 import logging
 import logging
 import os
 import os
+import copy
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 from itertools import chain
 from itertools import chain
 from pelican import signals
 from pelican import signals
-from pelican.utils import strftime
 from pelican.readers import MarkdownReader
 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
 	from pelican.settings import DEFAULT_CONFIG
 	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM', False)
 	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM', False)
 	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_DIR' 'comments')
 	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:
 	if pelican:
 		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
 		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
 		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments')
 		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:
 	if gen.settings['PELICAN_COMMENT_SYSTEM'] != True:
 		return
 		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
 		return
 
 
 	reader = MarkdownReader(gen.settings)
 	reader = MarkdownReader(gen.settings)
 	comments = []
 	comments = []
 	replies = []
 	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):
 	for file in os.listdir(folder):
 		name, extension = os.path.splitext(file)
 		name, extension = os.path.splitext(file)
 		if extension[1:].lower() in reader.file_extensions:
 		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:
 			if 'replyto' in meta:
 				replies.append( com )
 				replies.append( com )
 			else:
 			else:
 				comments.append( com )
 				comments.append( com )
 
 
+	writer.write_feed( comments + replies, context, path)
+
 	#TODO: Fix this O(n²) loop
 	#TODO: Fix this O(n²) loop
 	for reply in replies:
 	for reply in replies:
 		for comment in chain(comments, replies):
 		for comment in chain(comments, replies):
@@ -106,9 +108,14 @@ def add_static_comments(gen, metadata):
 
 
 	comments = sorted(comments)
 	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():
 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)