diff --git a/docs/index.rst b/docs/index.rst index 6ad22670..34a1355c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,6 +61,7 @@ A French version of the documentation is available at :doc:`fr/index`. getting_started settings themes + plugins internals pelican-themes importer diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 00000000..db5a4bfc --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,108 @@ +.. _plugins: + +Plugins +####### + +Since version 3.0, pelican manages plugins. Plugins are a way to add features +to pelican without having to directly hack pelican code. + +Pelican is shipped with a set of core plugins, but you can easily implement +your own (and this page describes how). + +How to use plugins? +==================== + +To load plugins, you have to specify them in your settings file. You have two +ways to do so. +Either by specifying strings with the path to the callables:: + + PLUGINS = ['pelican.plugins.gravatar',] + +Or by importing them and adding them to the list:: + + from pelican.plugins import gravatar + PLUGINS = [gravatar, ] + +If your plugins are not in an importable path, you can specify a `PLUGIN_PATH` +in the settings:: + + PLUGIN_PATH = "plugins" + PLUGINS = ["list", "of", "plugins"] + +How to create plugins? +====================== + +Plugins are based on the concept of signals. Pelican sends signals and plugins +subscribe to those signals. The list of signals are defined in a following +section. + +The only rule to follow for plugins is to define a `register` callable, in +which you map the signals to your plugin logic. Let's take a simple exemple:: + + from pelican import signals + + def test(sender): + print "%s initialized !!" % sender + + def register(): + signals.initialized.connect(test) + + +List of signals +=============== + +Here is the list of currently implemented signals: + +========================= ============================ ========================================= +Signal Arguments Description +========================= ============================ ========================================= +initialized pelican object +article_generate_context article_generator, metadata +article_generator_init article_generator invoked in the ArticlesGenerator.__init__ +========================= ============================ ========================================= + +The list is currently small, don't hesitate to add signals and make a pull +request if you need them! + +List of plugins +=============== + +Not all the list are described here, but a few of them have been extracted from +pelican core and provided in pelican.plugins. They are described here: + +Tag cloud +--------- + +Translation +----------- + +Github Activity +--------------- + +This plugin makes use of the ``feedparser`` library that you'll need to +install. + +Set the GITHUB_ACTIVITY_FEED parameter to your github activity feed. +For example, my setting would look like:: + + GITHUB_ACTIVITY_FEED = 'https://github.com/kpanic.atom' + +On the templates side, you just have to iterate over the ``github_activity`` +variable, as in the example:: + + {% if GITHUB_ACTIVITY_FEED %} +
+

Github Activity

+ +
+ {% endif %} + + + +``github_activity`` is a list of lists. The first element is the title +and the second element is the raw html from github. diff --git a/docs/settings.rst b/docs/settings.rst index 582cd9d4..b36c9953 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -61,6 +61,7 @@ Setting name (default value) What doe `rst2pdf`. `RELATIVE_URLS` (``True``) Defines whether Pelican should use relative URLs or not. +`PLUGINS` (``[]``) The list of plugins to load. See :ref:`plugins`. `SITENAME` (``'A Pelican Blog'``) Your site name `SITEURL` Base URL of your website. Not defined by default, which means the base URL is assumed to be "/" with a diff --git a/pelican/__init__.py b/pelican/__init__.py index 7e546b29..e942393c 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -5,6 +5,8 @@ import time import logging import argparse +from pelican import signals + from pelican.generators import (ArticlesGenerator, PagesGenerator, StaticGenerator, PdfGenerator, LessCSSGenerator) from pelican.log import init @@ -22,7 +24,7 @@ logger = logging.getLogger(__name__) class Pelican(object): def __init__(self, settings=None, path=None, theme=None, output_path=None, - markup=None, delete_outputdir=False): + markup=None, delete_outputdir=False, plugin_path=None): """Read the settings, and performs some checks on the environment before doing anything else. """ @@ -58,6 +60,20 @@ class Pelican(object): else: raise Exception("Impossible to find the theme %s" % theme) + self.init_plugins() + signals.initialized.send(self) + + def init_plugins(self): + self.plugins = self.settings['PLUGINS'] + for plugin in self.plugins: + # if it's a string, then import it + if isinstance(plugin, basestring): + log.debug("Loading plugin `{0}' ...".format(plugin)) + plugin = __import__(plugin, globals(), locals(), 'module') + + log.debug("Registering plugin `{0}' ...".format(plugin.__name__)) + plugin.register() + def _handle_deprecation(self): if self.settings.get('CLEAN_URLS', False): diff --git a/pelican/generators.py b/pelican/generators.py index e37de9e9..1ddc13c2 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -17,6 +17,7 @@ from jinja2.exceptions import TemplateNotFound from pelican.contents import Article, Page, Category, is_valid_content from pelican.readers import read_file from pelican.utils import copy, process_translations, open +from pelican import signals logger = logging.getLogger(__name__) @@ -118,6 +119,7 @@ class ArticlesGenerator(Generator): self.authors = defaultdict(list) super(ArticlesGenerator, self).__init__(*args, **kwargs) self.drafts = [] + signals.article_generator_init.send(self) def generate_feeds(self, writer): """Generate the feeds from the current context, and output files.""" @@ -274,6 +276,7 @@ class ArticlesGenerator(Generator): metadata['date'] = datetime.datetime.fromtimestamp( os.stat(f).st_ctime) + signals.article_generate_context.send(self, metadata=metadata) article = Article(content, metadata, settings=self.settings, filename=f) if not is_valid_content(article, f): diff --git a/pelican/plugins/__init__.py b/pelican/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pelican/plugins/github_activity.py b/pelican/plugins/github_activity.py new file mode 100644 index 00000000..f2ba1da7 --- /dev/null +++ b/pelican/plugins/github_activity.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) Marco Milanesi + + A plugin to list your Github Activity + To enable it set in your pelican config file the GITHUB_ACTIVITY_FEED + parameter pointing to your github activity feed. + + for example my personal activity feed is: + + https://github.com/kpanic.atom + + in your template just write a for in jinja2 syntax against the + github_activity variable. + + i.e. + +
+

Github Activity

+ +
+ + github_activity is a list containing a list. The first element is the title + and the second element is the raw html from github +""" + +from pelican import signals + + +class GitHubActivity(): + """ + A class created to fetch github activity with feedparser + """ + def __init__(self, generator): + try: + import feedparser + self.activities = feedparser.parse( + generator.settings['GITHUB_ACTIVITY_FEED']) + except ImportError: + raise Exception("Unable to find feedparser") + + def fetch(self): + """ + returns a list of html snippets fetched from github actitivy feed + """ + + entries = [] + for activity in self.activities['entries']: + entries.append( + [element for element in [activity['title'], + activity['content'][0]['value']]]) + + return entries + + +def fetch_github_activity(gen, metadata): + """ + registered handler for the github activity plugin + it puts in generator.context the html needed to be displayed on a + template + """ + + if 'GITHUB_ACTIVITY_FEED' in gen.settings.keys(): + gen.context['github_activity'] = gen.plugin_instance.fetch() + + +def feed_parser_initialization(generator): + """ + Initialization of feed parser + """ + + generator.plugin_instance = GitHubActivity(generator) + + +def register(): + """ + Plugin registration + """ + signals.article_generator_init.connect(feed_parser_initialization) + signals.article_generate_context.connect(fetch_github_activity) diff --git a/pelican/plugins/global_license.py b/pelican/plugins/global_license.py new file mode 100644 index 00000000..463a93b3 --- /dev/null +++ b/pelican/plugins/global_license.py @@ -0,0 +1,23 @@ +from pelican import signals + +""" +License plugin for Pelican +========================== + +Simply add license variable in article's context, which contain +the license text. + +Settings: +--------- + +Add LICENSE to your settings file to define default license. + +""" + +def add_license(generator, metadata): + if 'license' not in metadata.keys()\ + and 'LICENSE' in generator.settings.keys(): + metadata['license'] = generator.settings['LICENSE'] + +def register(): + signals.article_generate_context.connect(add_license) diff --git a/pelican/plugins/gravatar.py b/pelican/plugins/gravatar.py new file mode 100644 index 00000000..4ab8ea9c --- /dev/null +++ b/pelican/plugins/gravatar.py @@ -0,0 +1,40 @@ +import hashlib + +from pelican import signals +""" +Gravatar plugin for Pelican +=========================== + +Simply add author_gravatar variable in article's context, which contains +the gravatar url. + +Settings: +--------- + +Add AUTHOR_EMAIL to your settings file to define default author email. + +Article metadata: +------------------ + +:email: article's author email + +If one of them are defined, the author_gravatar variable is added to +article's context. +""" + +def add_gravatar(generator, metadata): + + #first check email + if 'email' not in metadata.keys()\ + and 'AUTHOR_EMAIL' in generator.settings.keys(): + metadata['email'] = generator.settings['AUTHOR_EMAIL'] + + #then add gravatar url + if 'email' in metadata.keys(): + gravatar_url = "http://www.gravatar.com/avatar/" + \ + hashlib.md5(metadata['email'].lower()).hexdigest() + metadata["author_gravatar"] = gravatar_url + + +def register(): + signals.article_generate_context.connect(add_gravatar) diff --git a/pelican/plugins/html_rst_directive.py b/pelican/plugins/html_rst_directive.py new file mode 100644 index 00000000..d0a656f5 --- /dev/null +++ b/pelican/plugins/html_rst_directive.py @@ -0,0 +1,63 @@ +from docutils import nodes +from docutils.parsers.rst import directives, Directive +from pelican import log + +""" +HTML tags for reStructuredText +============================== + +Directives +---------- + +.. html:: + + (HTML code) + + +Example +------- + +A search engine: + +.. html:: +
+ + + +
+ + +A contact form: + +.. html:: + +
+

+ +
+ +
+ +

+
+ +""" + + +class RawHtml(Directive): + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = True + has_content = True + + def run(self): + html = u' '.join(self.content) + node = nodes.raw('', html, format='html') + return [node] + + + +def register(): + directives.register_directive('html', RawHtml) + diff --git a/pelican/plugins/initialized.py b/pelican/plugins/initialized.py new file mode 100644 index 00000000..5e4cf174 --- /dev/null +++ b/pelican/plugins/initialized.py @@ -0,0 +1,7 @@ +from pelican import signals + +def test(sender): + print "%s initialized !!" % sender + +def register(): + signals.initialized.connect(test) diff --git a/pelican/settings.py b/pelican/settings.py index 647d3e93..b9912a47 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -69,6 +69,7 @@ _DEFAULT_CONFIG = {'PATH': '.', 'TYPOGRIFY': False, 'LESS_GENERATOR': False, 'WEBASSETS': False, + 'PLUGINS': [], } diff --git a/pelican/signals.py b/pelican/signals.py new file mode 100644 index 00000000..b1c35794 --- /dev/null +++ b/pelican/signals.py @@ -0,0 +1,5 @@ +from blinker import signal + +initialized = signal('pelican_initialized') +article_generate_context = signal('article_generate_context') +article_generator_init = signal('article_generator_init') diff --git a/setup.py b/setup.py index 0e57c83b..a8a8fbd9 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from setuptools import setup -requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz'] +requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz', 'blinker'] try: import argparse @@ -25,7 +25,7 @@ setup( author_email = 'alexis@notmyidea.org', description = "A tool to generate a static blog from reStructuredText or Markdown input files.", long_description=open('README.rst').read(), - packages = ['pelican', 'pelican.tools'], + packages = ['pelican', 'pelican.tools', 'pelican.plugins'], include_package_data = True, install_requires = requires, entry_points = entry_points,