Merge pull request #270 from bbinet/plugins

Plugins
This commit is contained in:
Alexis Metaireau 2012-03-21 17:58:24 -07:00
commit 97d8d0daa6
88 changed files with 2998 additions and 1305 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ docs/fr/_build
build
dist
output
tags
.tox

13
.travis.yml Normal file
View file

@ -0,0 +1,13 @@
language: python
python:
- "2.6"
- "2.7"
install:
- pip install nose unittest2 mock --use-mirrors
- pip install . --use-mirrors
script: nosetests -s tests
notifications:
irc:
channels:
- "irc.freenode.org#pelican"
on_success: change

100
CHANGELOG Normal file
View file

@ -0,0 +1,100 @@
X.X
* Refactored the way URL are handled.
* Improved the english documentation
* Fixed packaging using setuptools entrypoints
* Added typogrify support
* Added a way to disable feed generation
2.8
* dotclear importer
* Allow the usage of markdown extensions
* Themes are now easily extensible
* Don't output pagination information if there is only one page.
* Add a page per author, with all their articles
* Improved the test suite
* Made the themes more easy to extend
* Removed Skribit support
* Added a "pelican-quickstart" script
* Fixed timezone-related issues
* Add some scripts for windows support
* Date can be specified in seconds
* Never fail when generating posts (skip and continue)
* Allow the use of future dates
* Support having different timezones per languages.
* Enhanced the documentation
2.7
* Uses logging rather than echoing to stdout
* Support custom jinja filters
* Compatibility with python 2.5
* Add a theme manager
* Packaged for debian
* Add draft support
2.6
* changes in the output directory structure
* makes templates easier to work with / create
* Add RSS support (was only atom previously)
* Add tag support for the feeds
* Enhance the documentation
* Add another theme (brownstone)
* Add translations
* Add a way to use "cleaner urls" with a rewrite url module (or equivalent)
* Add a tag cloud
* Add an autoreloading feature: the blog is automatically regenerated each time a modification is detected
* Translate the documentation in french
* import a blog from an rss feed
* Pagination support
* Add skribit support
2.5
* import from wordpress
* add some new themes (martyalchin / wide-notmyidea)
* first bug report !
* linkedin support
* added a FAQ
* google analytics support
* twitter support
* use relative urls not static ones
2.4
* minor themes changes
* add disqus support (so we have comments)
* another code refactoring
* add config settings about pages
* blog entries can also be generated in pdf
2.3
* markdown support
2.2
* Prettify output
* Manages static pages as well
2.1
* Put the notmyidea theme by default
2.0
* Refactoring to be more extensible
* Change into the setting variables
1.2
* Add a debug option
* Add feeds per category
* Use filsystem to get dates if no metadata provided
* Add pygment support
1.1:
* first working version

View file

@ -1,3 +1,4 @@
include *.rst
global-include *.py
recursive-include pelican *.html *.css *png
include LICENSE

View file

@ -1,53 +1,59 @@
Pelican
#######
.. image:: https://secure.travis-ci.org/ametaireau/pelican.png?branch=master
Pelican is a simple weblog generator, written in `Python <http://www.python.org/>`_.
* Write your weblog entries directly with your editor of choice (vim!) and
directly in `reStructuredText <http://docutils.sourceforge.net/rst.html>`_, or `Markdown <http://daringfireball.net/projects/markdown/>`_.
* A simple cli-tool to (re)generate the weblog.
* Write your weblog entries directly with your editor of choice (vim!)
in `reStructuredText <http://docutils.sourceforge.net/rst.html>`_ or `Markdown <http://daringfireball.net/projects/markdown/>`_
* Includes a simple CLI tool to (re)generate the weblog
* Easy to interface with DVCSes and web hooks
* Completely static output, so easy to host anywhere !
* Completely static output is easy to host anywhere
Features
--------
Pelican currently supports:
* blog articles and pages
* comments, via an external service (disqus). Please notice that while
it's useful, it's an external service, and you'll not manage the
comments by yourself. It could potentially eat your data.
* theming support (themes are done using `jinja2 <http://jinjna.pocoo.org>`_)
* PDF generation of the articles/pages (optional).
* Translations
* Syntactic recognition
* Blog articles and pages
* Comments, via an external service (Disqus). (Please note that while
useful, Disqus is an external service, and thus the comment data will be
somewhat outside of your control and potentially subject to data loss.)
* Theming support (themes are created using `jinja2 <http://jinja.pocoo.org/>`_)
* PDF generation of the articles/pages (optional)
* Publication of articles in multiple languages
* Atom/RSS feeds
* Code syntax highlighting
* Import from WordPress, Dotclear, or RSS feeds
* Integration with external tools: Twitter, Google Analytics, etc. (optional)
Have a look to `the documentation <http://alexis.notmyidea.org/pelican/>`_ for
more informations.
Have a look at `the documentation <http://pelican.notmyidea.org/en/latest/>`_ for
more information.
Why the name "Pelican" ?
Why the name "Pelican"?
------------------------
Heh, you didn't noticed? "Pelican" is an anagram for "Calepin" ;)
Heh, you didn't notice? "Pelican" is an anagram for « Calepin » ;)
Source code
-----------
You can access the source code via git on http://github.com/ametaireau/pelican/
You can access the source code via git at: https://github.com/ametaireau/pelican
If you feel hackish, have a look to the `pelican's internals explanations
<http://alexis.notmyidea.org/pelican/internals.html>`_.
If you feel hackish, have a look at the explanation of `Pelican's internals
<http://pelican.notmyidea.org/en/latest/internals.html>`_.
Feedback / Contact us
=====================
---------------------
If you want to see new features in Pelican, dont hesitate to tell me, to clone
the repository, etc. That's open source, dude!
If you want to see new features in Pelican, don't hesitate to offer suggestions,
clone the repository, etc. There are many ways to `contribute
<http://pelican.notmyidea.org/en/latest/contribute.html>`_. That's open source, dude!
Contact me at "alexis at notmyidea dot org" for any request/feedback! You can
also join the team at `#pelican on irc.freenode.org
<irc://irc.freenode.net/pelican>`_
(or if you don't have any IRC client, using `the webchat
(or if you don't have any IRC client, use `the webchat
<http://webchat.freenode.net/?channels=pelican&uio=d4>`_)
for quick feedback.

1
THANKS
View file

@ -16,3 +16,4 @@ bugs or giving ideas. Thanks to them !
- Marcus Fredriksson
- Günter Kolousek
- Simon Liedtke
- Manuel F. Viera

3
TODO
View file

@ -1,8 +1,9 @@
* Add a way to support pictures (see how sphinx makes that)
* Find a way to extend the existing templates instead of rewriting all from scratch.
* Make the program support UTF8-encoded files as input (and later: any encoding?)
* Add status support (draft, published, hidden)
* Add a serve + automatic generation behaviour.
* Recompile only the changed files, not all.
* Add a way to make the coffee (or not)
* Add a sitemap generator.
* read templates from the templates folder per default
* add support of github via ghg import

View file

@ -1,3 +0,0 @@
#!/usr/bin/env python
from pelican import main
main()

View file

@ -1,5 +1,7 @@
Jinja2==2.5.5
Pygments==1.4
docutils==0.7
feedgenerator==1.2.1
Jinja2
Pygments
docutils
feedgenerator
unittest2
pytz
mock

BIN
docs/_static/overall.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/_static/theme-basic.zip vendored Normal file

Binary file not shown.

BIN
docs/_static/uml.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import sys, os
sys.path.append(os.path.abspath('..'))
from pelican import __version__, __major__
# -- General configuration -----------------------------------------------------
templates_path = ['_templates']
extensions = ['sphinx.ext.autodoc',]
@ -9,12 +13,11 @@ master_doc = 'index'
project = u'Pelican'
copyright = u'2010, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
version = "2"
release = version
version = __version__
release = __major__
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
html_theme = 'pelican'
@ -40,7 +43,7 @@ man_pages = [
('index', 'pelican', u'pelican documentation',
[u'Alexis Métaireau'], 1),
('pelican-themes', 'pelican-themes', u'A theme manager for Pelican',
[u'Mickaël Raybaud'], 'en.1'),
('fr/pelican-themes', 'pelican-themes', u'Un gestionnaire de thèmes pour Pelican',
[u'Mickaël Raybaud'], 'fr.1')
[u'Mickaël Raybaud'], 1),
('themes', 'pelican-theming', u'How to create themes for Pelican',
[u'The Pelican contributors'], 1)
]

View file

@ -1,39 +1,49 @@
How to contribute ?
How to contribute?
###################
There are many ways to contribute to pelican. You can enhance the
documentation, add missing features, fix bugs or just report them.
There are many ways to contribute to Pelican. You can enhance the
documentation, add missing features, and fix bugs (or just report them).
Don't hesitate to fork and make a pull request on github.
Don't hesitate to fork and make a pull request on GitHub.
Set up the development environment
==================================
Setting up the development environment
======================================
You're free to setup up the environment in any way you like. Here is a way
using virtualenv and virtualenvwrapper. If you don't have them, you can install
them using::
You're free to set up your development environment any way you like. Here is a
way using virtualenv and virtualenvwrapper. If you don't have them, you can
install these packages via::
$ pip install virtualenvwrapper
Virtual environments allow you to work on an installation of python which is
not the one installed on your system. Especially, it will install the different
projects under a different location.
Virtual environments allow you to work on Python projects which are isolated
from one another so you can use different packages (and package versions) with
different projects.
To create the virtualenv environment, you have to do::
To create a virtual environment, use the following syntax::
$ mkvirtualenv pelican --no-site-package
$ mkvirtualenv pelican
Then you would have to install all the dependencies::
To manually install the dependencies::
$ pip install -r dev_requirements.txt
$ python setup.py develop
Running the test suite
======================
Each time you add a feature, there are two things to do regarding tests:
checking that the tests run in a right way, and be sure that you add tests for
the feature you are working on or the bug you're fixing.
checking that the existing tests pass, and adding tests for your new feature
or for the bug you're fixing.
The tests leaves under "pelican/tests" and you can run them using the
The tests live in "pelican/tests" and you can run them using the
"discover" feature of unittest2::
$ unit2 discover
Coding standards
================
Try to respect what is described in the PEP8
(http://www.python.org/dev/peps/pep-0008/) when providing patches. This can be
eased by the pep8 tool (http://pypi.python.org/pypi/pep8) or by Flake8, which
will give you some other cool hints about what's good or wrong
(http://pypi.python.org/pypi/flake8/)

View file

@ -1,38 +1,51 @@
Frequently Asked Questions (FAQ)
################################
Here is a summary of the frequently asked questions for pelican.
Here is a summary of the frequently asked questions for Pelican.
Is it mandatory to have a configuration file ?
==============================================
Is it mandatory to have a configuration file?
=============================================
No, it's not. Configurations files are just an easy way to configure pelican.
For the basic operations, it's possible to specify options while invoking
pelican with the command line (see `pelican --help` for more informations about
that)
No, it's not. Configuration files are just an easy way to configure Pelican.
For basic operations, it's possible to specify options while invoking Pelican
via the command line. See `pelican --help` for more information.
I'm creating my own theme, how to use pygments ?
================================================
I'm creating my own theme. How do I use Pygments for syntax highlighting?
=========================================================================
Pygment add some classes to the generated content, so the theming of your theme
will be done thanks to a css file. You can have a look to the one proposed by
default `on the project website <http://pygments.org/demo/15101/>`_
Pygments adds some classes to the generated content. These classes are used by
themes to style code syntax highlighting via CSS. Specifically, you can
customize the appearance of your syntax highlighting via the `.codehilite pre`
class in your theme's CSS file. To see how various styles can be used to render
Django code, for example, you can use the demo `on the project website
<http://pygments.org/demo/15101/>`_.
How do I create my own theme ?
How do I create my own theme?
==============================
Please refer yourself to :ref:`theming-pelican`.
Please refer to :ref:`theming-pelican`.
How can I help ?
How can I help?
================
You have different options to help. First, you can use pelican, and report any
idea or problem you have on `the bugtracker
There are several ways to help out. First, you can use Pelican and report any
suggestions or problems you might have on `the bugtracker
<http://github.com/ametaireau/pelican/issues>`_.
If you want to contribute, please have a look to `the git repository
<https://github.com/ametaireau/pelican/>`_, fork it, add your changes and do
a pull request, I'll review them as soon as possible.
If you want to contribute, please fork `the git repository
<https://github.com/ametaireau/pelican/>`_, make your changes, and issue
a pull request. I'll review your changes as soon as possible.
You can also contribute by creating themes, and making the documentation
better.
You can also contribute by creating themes and improving the documentation.
I want to use Markdown, but I got an error.
===========================================
Markdown is not a hard dependency for Pelican, so you will need to explicitly
install it. You can do so by typing::
$ (sudo) pip install markdown
In case you don't have pip installed, consider installing it via::
$ (sudo) easy_install pip

View file

@ -54,3 +54,4 @@ Documentation
parametres_article
astuces
faq
pelican-themes

View file

@ -169,19 +169,3 @@ dates_pages La page actuelle d'articles, ordonnée par date
croissante.
page_name 'tag/`nom du tag`'.
=================== ===================================================
Inclure le script skribit
=========================
Pour pouvoir supporter les scripts skribit dans vos thèmes, vous devez
faire ceci :
* Copier `skribit_tab_script.html` et `skribit_widget_script.html` dans
votre dossier de templates.
* Ajouter {% include 'skribit_tab_script' %} dans votre <head> pour
ajouter le support de l'onglet de suggestions.
* Ajouter {% include 'skribit_widget_script' %} là où vous le souhaitez
pour ajouter le widget dans la sidebar.
Vous pouvez regarder le thème par défault (notmyidea) pour voir un
exemple de thème fonctionnel.

View file

@ -4,45 +4,44 @@ Getting started
Installing
==========
You're ready? Let's go ! You can install pelican in a lot of different ways,
the simpler one is via `pip <http://pip.openplans.org/>`_::
You're ready? Let's go! You can install Pelican via several different methods. The simplest is via `pip <http://pip.openplans.org/>`_::
$ pip install pelican
If you have the sources, you can install pelican using the distutils command
install. I recommend to do so in a virtualenv::
If you have the project source, you can install Pelican using the distutils
method. I recommend doing so in a virtualenv::
$ virtualenv .
$ virtualenv pelican_venv
$ source bin/activate
$ python setup.py install
Dependencies
------------
At this time, pelican is dependent of the following python packages:
At this time, Pelican is dependent on the following Python packages:
* feedgenerator, to generate the ATOM feeds.
* jinja2, for templating support.
* feedgenerator, to generate the Atom feeds
* jinja2, for templating support
If you're not using python 2.7, you will also need `argparse`.
If you're not using Python 2.7, you will also need `argparse`.
Optionally:
* docutils, for reST support
* pygments, to have syntactic colorization with resT input
* Markdown, for Markdown as an input format
* pygments, for syntax highlighting
* docutils, for supporting reStructuredText as an input format
* Markdown, for supporting Markdown as an input format
Writing articles using pelican
Writing articles using Pelican
==============================
Files metadata
File metadata
--------------
Pelican tries to be smart enough to get the informations it needs from the
file system (for instance, about the category of your articles), but you need to
provide by hand some of those informations in your files.
Pelican tries to be smart enough to get the information it needs from the
file system (for instance, about the category of your articles), but some
information you need to provide in the form of metadata inside your files.
You could provide the metadata in the restructured text files, using the
You can provide this metadata in reStructuredText text files via the
following syntax (give your file the `.rst` extension)::
My super title
@ -54,35 +53,46 @@ following syntax (give your file the `.rst` extension)::
:author: Alexis Metaireau
You can also use a markdown syntax (with a file ending in `.md`)::
You can also use Markdown syntax (with a file ending in `.md`)::
Date: 2010-12-03
Title: My super title
Tags: thats, awesome
Slug: my-super-post
Put you content here.
This is the content of my super blog post.
Note that none of those are mandatory: if the date is not specified, pelican will
rely on the mtime of your file, and the category can also be determined by the
directory where the rst file is. For instance, the category of
`python/foobar/myfoobar.rst` is `foobar`.
Note that, aside from the title, none of this metadata is mandatory: if the date
is not specified, Pelican will rely on the file's "mtime" timestamp, and the
category can be determined by the directory in which the file resides. For
example, a file located at `python/foobar/myfoobar.rst` will have a category of
`foobar`.
Generate your blog
------------------
To launch pelican, just use the `pelican` command::
To launch Pelican, just use the `pelican` command::
$ pelican /path/to/your/content/ [-s path/to/your/settings.py]
And… that's all! You can see your weblog generated on the `content/` folder.
And… that's all! Your weblog will be generated and saved in the `content/`
folder.
This one will just generate a simple output, with the default theme. It's not
really sexy, as it's a simple HTML output (without any style).
The above command will use the default theme to produce a simple site. It's not
very sexy, as it's just simple HTML output (without any style).
You can create your own style if you want, have a look to the help to see all
You can create your own style if you want. Have a look at the help to see all
the options you can use::
$ pelican --help
Kickstart a blog
----------------
You also can use the `pelican-quickstart` script to start a new blog in
seconds, by just answering few questions. Just run `pelican-quickstart` and
you're done! (Added in Pelican 3.0)
Pages
-----
@ -95,34 +105,26 @@ the menu.
Importing an existing blog
--------------------------
It is possible to import wordpress themes and RSS themes using a script which
is living in `tools`: importer.
You can call it this way for a wordpress import::
$ python importer.py --wpfile /your/wordpress/export -o output_dir
And like this for an import from an RSS feed::
$ python importer.py --feed http://your/rss/feed -o output_dir
It is possible to import your blog from Dotclear, WordPress, and RSS feeds using
a simple script. See :ref:`import`.
Translations
------------
It is possible to translate articles. To do so, you need to add a `lang` meta
in your articles/pages, and to set a `DEFAULT_LANG` setting (which is en by
default).
Then, only articles with this default language will be listed, and
each article will have a translation list.
attribute to your articles/pages and set a `DEFAULT_LANG` setting (which is
English [en] by default). With those settings in place, only articles with the
default language will be listed, and each article will be accompanied by a list
of available translations for that article.
Pelican uses the "slug" of two articles to compare if they are translations of
each others. So it's possible to define (in restructured text) the slug
directly.
Pelican uses the article's URL "slug" to determine if two or more articles are
translations of one another. The slug can be set manually in the file's
metadata; if not set explicitly, Pelican will auto-generate the slug from the
title of the article.
Here is an exemple of two articles (one in english and the other one in
french).
Here is an example of two articles, one in English and the other in French.
The english one::
The English article::
Foobar is not dead
##################
@ -130,9 +132,9 @@ The english one::
:slug: foobar-is-not-dead
:lang: en
That's true, foobar is still alive !
That's true, foobar is still alive!
And the french one::
And the French version::
Foobar n'est pas mort !
#######################
@ -142,34 +144,67 @@ And the french one::
Oui oui, foobar est toujours vivant !
Despite the text quality, you can see that only the slug is the same here.
You're not forced to define the slug that way, and it's completely possible to
have two translations with the same title (which defines the slug)
Post content quality notwithstanding, you can see that only item in common
between the two articles is the slug, which is functioning here as an
identifier. If you'd rather not explicitly define the slug this way, you must
then instead ensure that the translated article titles are identical, since the
slug will be auto-generated from the article title.
Syntactic recognition
Syntax highlighting
---------------------
Pelican is able to regognise the syntax you are using, and to colorize the
right way your block codes. To do so, you have to use the following syntax::
Pelican is able to provide colorized syntax highlighting for your code blocks.
To do so, you have to use the following convention for reStructuredText::
.. code-block:: identifier
your code goes here
The identifier is one of the lexers available `here
<http://pygments.org/docs/lexers/>`_.
For Markdown, format your code blocks thusly:
::identifier
your code goes here
The specified identifier should be one that appears on the
`list of available lexers <http://pygments.org/docs/lexers/>`_.
You also can use the default `::` syntax, in which case it will be assumed
that your code is written in Python. For reStructuredText::
::
your code goes here
For Markdown:
::
your code goes here
Autoreload
----------
It's possible to tell pelican to watch for your modifications, instead of
manually launching it each time you need. Use the `-r` option, or
`--autoreload`.
It's possible to tell Pelican to watch for your modifications, instead of
manually launching it every time you want to see your changes. To enable this,
run the `pelican` command with the `-r` or `--autoreload` options.
Publishing drafts
-----------------
If you want to publish an article as a draft, for friends to review it for
instance, you can add a ``status: draft`` to its metadata, it will then be
available under the ``drafts`` folder, and not be listed under the index page nor
any category page.
If you want to publish an article as a draft (for friends to review before
publishing, for example), you can add a ``status: draft`` attribute to its
metadata. That article will then be output to the ``drafts`` folder and not
listed on the index page nor on any category page.
Viewing the generated files
---------------------------
The files generated by Pelican are static files, so you don't actually need
anything special to see what's happening with the generated files.
You can either use your browser to open the files on your disk::
$ firefox output/index.html
Or run a simple web server using Python::
cd output && python -m SimpleHTTPServer

57
docs/importer.rst Normal file
View file

@ -0,0 +1,57 @@
.. _import:
=================================
Import from other blog software
=================================
Description
===========
``pelican-import`` is a command line tool for converting articles from other
software to ReStructuredText. The supported formats are:
- WordPress XML export
- Dotclear export
- RSS/Atom feed
The conversion from HTML to reStructuredText relies on `pandoc
<http://johnmacfarlane.net/pandoc/>`_. For Dotclear, if the source posts are
written with Markdown syntax, they will not be converted (as Pelican also
supports Markdown).
Usage
"""""
| pelican-import [-h] [--wpfile] [--dotclear] [--feed] [-o OUTPUT]
| [--dir-cat]
| input
Optional arguments:
"""""""""""""""""""
-h, --help show this help message and exit
--wpfile Wordpress XML export
--dotclear Dotclear export
--feed Feed to parse
-o OUTPUT, --output OUTPUT
Output path
--dir-cat Put files in directories with categories name
Examples
========
for WordPress::
$ pelican-import --wpfile -o ~/output ~/posts.xml
for Dotclear::
$ pelican-import --dotclear -o ~/output ~/backup.txt
Tests
=====
To test the module, one can use sample files:
- for Wordpress: http://wpcandy.com/made/the-sample-post-collection
- for Dotclear: http://themes.dotaddict.org/files/public/downloads/lorem-backup.txt

View file

@ -1,62 +1,70 @@
Pelican
#######
Pelican is a simple weblog generator, writen in python.
Pelican is a simple weblog generator, written in Python.
* Write your weblog entries directly with your editor of choice (vim!) and
directly in restructured text, or markdown.
* A simple cli-tool to (re)generate the weblog.
* Write your weblog entries directly with your editor of choice (vim!) in
reStructuredText or Markdown
* A simple CLI tool to (re)generate the weblog
* Easy to interface with DVCSes and web hooks
* Completely static output, so easy to host anywhere !
* Completely static output is easy to host anywhere
Features
========
Pelican currently supports:
* blog articles
* comments, via an external service (disqus). Please notice that while
it's useful, it's an external service, and you'll not manage the
comments by yourself. It could potentially eat your data.
* theming support (themes are done using `jinja2 <http://jinjna.pocoo.org>`_)
* PDF generation of the articles/pages (optional).
* Blog articles and pages
* Comments, via an external service (Disqus). (Please note that while
useful, Disqus is an external service, and thus the comment data will be
somewhat outside of your control and potentially subject to data loss.)
* Theming support (themes are created using `jinja2 <http://jinja.pocoo.org/>`_)
* PDF generation of the articles/pages (optional)
* Publication of articles in multiple languages
* Atom/RSS feeds
* Code syntax highlighting
* Import from WordPress, Dotclear, or RSS feeds
* Integration with external tools: Twitter, Google Analytics, etc. (optional)
Why the name "Pelican" ?
========================
Heh, you didn't noticed? "Pelican" is an anagram for "Calepin" ;)
Heh, you didn't notice? "Pelican" is an anagram for « Calepin » ;)
Source code
===========
You can access the source code via git on http://github.com/ametaireau/pelican/
You can access the source code via git at http://github.com/ametaireau/pelican/
Feedback / Contact us
=====================
If you want to see new features in Pelican, dont hesitate to tell me, to clone
If you want to see new features in Pelican, don't hesitate to tell me, to clone
the repository, etc. That's open source, dude!
Contact me at "alexis at notmyidea dot org" for any request/feedback! You can
also join the team at `#pelican on irc.freenode.org
<irc://irc.freenode.net/pelican>`_
(or if you don't have any IRC client, using `the webchat
also join the team at `#pelican on irc.freenode.org
<irc://irc.freenode.net/pelican>`_
(or if you don't have any IRC client, use `the webchat
<http://webchat.freenode.net/?channels=pelican&uio=d4>`_)
for quick feedback.
Documentation
=============
A french version of the documentation is available at :doc:`fr/index`.
A French version of the documentation is available at :doc:`fr/index`.
.. toctree::
:maxdepth: 2
getting_started
settings
themes
pelican-themes
plugins
internals
pelican-themes
importer
faq
tips
contribute
report

View file

@ -1,44 +1,48 @@
Pelican internals
#################
This section describe how pelican is working internally. As you'll see, it's
quite simple, but a bit of documentation doesn't hurt :)
This section describe how Pelican works internally. As you'll see, it's
quite simple, but a bit of documentation doesn't hurt. :)
You can also find in the :doc:`report` section an excerpt of a report the
original author wrote with some software design information.
.. _report: :doc:`report`
Overall structure
=================
What `pelican` does, is taking a list of files, and processing them, to some
sort of output. Usually, the files are restructured text and markdown files,
and the output is a blog, but it can be anything you want.
What `pelican` does is take a list of files and process them into some
sort of output. Usually, the input files are reStructuredText and Markdown
files, and the output is a blog, but both input and output can be anything you
want.
I've separated the logic in different classes and concepts:
The logic is separated into different classes and concepts:
* `writers` are responsible of all the writing process of the
files. It's writing .html files, RSS feeds and so on. Since those operations
are commonly used, the object is created once, and then passed to the
generators.
* `writers` are responsible for writing files: .html files, RSS feeds, and so
on. Since those operations are commonly used, the object is created once and
then passed to the generators.
* `readers` are used to read from various formats (Markdown, and Restructured
Text for now, but the system is extensible). Given a file, they return
metadata (author, tags, category etc) and content (HTML formated)
* `readers` are used to read from various formats (Markdown and
reStructuredText for now, but the system is extensible). Given a file, they return
metadata (author, tags, category, etc.) and content (HTML-formatted).
* `generators` generate the different outputs. For instance, pelican comes with
`ArticlesGenerator` and `PageGenerator`, into others. Given
a configurations, they can do whatever they want. Most of the time it's
generating files from inputs.
* `generators` generate the different outputs. For instance, Pelican comes with
`ArticlesGenerator` and `PageGenerator`. Given a configuration, they can do
whatever they want. Most of the time, it's generating files from inputs.
* `pelican` also uses `templates`, so it's easy to write you own theme. The
* `pelican` also uses `templates`, so it's easy to write your own theme. The
syntax is `jinja2`, and, trust me, really easy to learn, so don't hesitate
a second.
to jump in and build your own theme.
How to implement a new reader ?
===============================
How to implement a new reader?
==============================
There is an awesome markup language you want to add to pelican ?
Well, the only thing you have to do is to create a class that have a `read`
method, that is returning an HTML content and some metadata.
Is there an awesome markup language you want to add to Pelican?
Well, the only thing you have to do is to create a class with a `read`
method that returns HTML content and some metadata.
Take a look to the Markdown reader::
Take a look at the Markdown reader::
class MarkdownReader(Reader):
enabled = bool(Markdown)
@ -58,31 +62,31 @@ Take a look to the Markdown reader::
metadata[name.lower()] = meta
return content, metadata
Simple isn't it ?
Simple, isn't it?
If your new reader requires additional Python dependencies then you should wrap
their `import` statements in `try...except`. Then inside the reader's class
set the `enabled` class attribute to mark import success or failure. This makes
it possible for users to continue using their favourite markup method without
needing to install modules for all the additional formats they don't use.
If your new reader requires additional Python dependencies, then you should wrap
their `import` statements in a `try...except` block. Then inside the reader's
class, set the `enabled` class attribute to mark import success or failure.
This makes it possible for users to continue using their favourite markup method
without needing to install modules for formats they don't use.
How to implement a new generator ?
==================================
How to implement a new generator?
=================================
Generators have basically two important methods. You're not forced to create
both, only the existing ones will be called.
Generators have two important methods. You're not forced to create
both; only the existing ones will be called.
* `generate_context`, that is called in a first place, for all the generators.
* `generate_context`, that is called first, for all the generators.
Do whatever you have to do, and update the global context if needed. This
context is shared between all generators, and will be passed to the
templates. For instance, the `PageGenerator` `generate_context` method find
all the pages, transform them into objects, and populate the context with
them. Be careful to *not* output anything using this context at this stage,
as it is likely to change by the effect of others generators.
templates. For instance, the `PageGenerator` `generate_context` method finds
all the pages, transforms them into objects, and populates the context with
them. Be careful *not* to output anything using this context at this stage,
as it is likely to change by the effect of other generators.
* `generate_output` is then called. And guess what is it made for ? Oh,
generating the output :) That's here that you may want to look at the context
and call the methods of the `writer` object, that is passed at the first
* `generate_output` is then called. And guess what is it made for? Oh,
generating the output. :) It's here that you may want to look at the context
and call the methods of the `writer` object that is passed as the first
argument of this function. In the `PageGenerator` example, this method will
look at all the pages recorded in the global context, and output a file on
look at all the pages recorded in the global context and output a file on
the disk (using the writer method `write_file`) for each page encountered.

View file

@ -57,11 +57,11 @@ With ``pelican-themes``, you can see the available themes by using the ``-l`` or
two-column@
simple
In this example, we can see there is 3 themes available: ``notmyidea``, ``simple`` and ``two-column``.
In this example, we can see there are three themes available: ``notmyidea``, ``simple``, and ``two-column``.
``two-column`` is prefixed with an ``@`` because this theme is not copied to the Pelican theme path, but just linked to it (see `Creating symbolic links`_ for details about creating symbolic links).
``two-column`` is prefixed with an ``@`` because this theme is not copied to the Pelican theme path, but is instead just linked to it (see `Creating symbolic links`_ for details about creating symbolic links).
Note that you can combine the ``--list`` option with the ``-v`` or ``--verbose`` option to get a more verbose output, like this:
Note that you can combine the ``--list`` option with the ``-v`` or ``--verbose`` option to get more verbose output, like this:
.. code-block:: console
@ -95,8 +95,8 @@ This option takes as argument the path(s) of the theme(s) you want to install, a
Removing themes
"""""""""""""""
Pelican themes can also removes themes from the Pelican themes path.
The ``-r`` or ``--remove`` takes as argument the name(s) of the theme(s) you want to remove, and can be combined with the ``--verbose`` option.
The ``pelican-themes`` command can also remove themes from the Pelican themes path.
The ``-r`` or ``--remove`` option takes as argument the name(s) of the theme(s) you want to remove, and can be combined with the ``--verbose`` option.
.. code-block:: console
@ -113,7 +113,7 @@ The ``-r`` or ``--remove`` takes as argument the name(s) of the theme(s) you wan
Creating symbolic links
"""""""""""""""""""""""
``pelican-themes`` can also install themes by creating symbolic links instead of copying the whole themes in the Pelican themes path.
``pelican-themes`` can also install themes by creating symbolic links instead of copying entire themes into the Pelican themes path.
To symbolically link a theme, you can use the ``-s`` or ``--symlink``, which works exactly as the ``--install`` option:
@ -152,7 +152,7 @@ The ``--install``, ``--remove`` and ``--symlink`` option are not mutually exclus
--symlink ~/Dev/Python/pelican-themes/two-column \
--verbose
In this example, the theme ``notmyidea-cms`` is replaced by the theme ``notmyidea-cms-fr``
In this example, the theme ``notmyidea-cms`` is replaced by the theme ``notmyidea-cms-fr``

View file

@ -3,17 +3,18 @@
Plugins
#######
Since version 2.8, pelican manages plugins. Plugins are a way to add feature to
pelican without having to directly hack pelican code.
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)
your own (and this page describes how).
How to use plugins?
====================
To load plugins, you have to specify a them in your settings file. You have two
ways to do so: by specifying strings with the path to the callables: ::
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',]
@ -76,28 +77,18 @@ Translation
-----------
Github Activity
_______________
---------------
This plugins introduces a new depencency, you have to install feedparser
if you want to use it, these are some ways to do it::
This plugin makes use of the ``feedparser`` library that you'll need to
install.
apt-get install python-feedparser # on debian based distributions like ubuntu
sudo easy_install feedparser
sudo pip install feedparser
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
and the config line could be::
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'
in your template just write a for in jinja2 syntax against the
github_activity variable, like for example::
On the templates side, you just have to iterate over the ``github_activity``
variable, as in the example::
{% if GITHUB_ACTIVITY_FEED %}
<div class="social">
@ -113,7 +104,5 @@ github_activity variable, like for example::
github_activity is a list containing a list. The first element is the title and
the second element is the raw html from github so you can include it directly
in your (for example base.html) template and style it in a way that your prefer
using your css skills
``github_activity`` is a list of lists. The first element is the title
and the second element is the raw html from github.

122
docs/report.rst Normal file
View file

@ -0,0 +1,122 @@
Some history about pelican
##########################
.. warning::
This page comes from a report the original author (Alexis Métaireau) wrote
right after writing pelican, in december 2010. The information may not be
up to date.
Pelican is a simple static blog generator. It parses markup files
(markdown or restructured text for now), and generate a HTML folder
with all the files in it.
I've chosen to use python to implement pelican because it seemed to
be simple and to fit to my needs. I did not wanted to define a class for
each thing, but still wanted to keep my things loosely coupled.
It turns out that it was exactly what I wanted. From time to time,
thanks to the feedback of some users, it took me a very few time to
provide fixes on it. So far, I've re-factored the pelican code by two
times, each time took less than 30 minutes.
Use case
========
I was previously using wordpress, a solution you can host on a web
server to manage your blog. Most of the time, I prefer using markup
languages such as Markdown or RestructuredText to type my articles.
To do so, I use vim. I think it is important to let the people choose the
tool they want to write the articles. In my opinion, a blog manager
should just allow you to take any kind of input and transform it to a
weblog. That's what pelican does.
You can write your articles using the tool you want, and the markup
language you want, and then generate a static HTML weblog
.. image:: _static/overall.png
To be flexible enough, pelican have a template support, so you can
easily write you own themes if you want to.
Design process
==============
Pelican came from a need I have. I started by creating a single file
application, and I have make it grow to support what it does by now.
To start, I wrote a piece of documentation about what I wanted to do.
Then, I have created the content I wanted to parse (the restructured
text files), and started experimenting with the code.
Pelican was 200 lines long, and contained almost ten functions and one
class when it was first usable.
I have been facing different problems all over the time, and wanted to
add features to pelican while using it. The first change I have done was
to add the support of a settings file. It is possible to pass the options to
the command line, but can be tedious if there is a lot of them.
In the same way, I have added the support of different things over
time: atom feeds, multiple themes, multiple markup support, etc.
At some point, it appears that the “only one file” mantra was not good
enough for pelican, so I decided to rework a bit all that, and split this in
multiple different files.
Ive separated the logic in different classes and concepts:
* *writers* are responsible of all the writing process of the files.
They are responsible of writing .html files, RSS feeds and so on.
Since those operations are commonly used, the object is created
once, and then passed to the generators.
* *readers* are used to read from various formats (Markdown, and
Restructured Text for now, but the system is extensible). Given a
file, they return metadata (author, tags, category etc) and
content (HTML formated).
* *generators* generate the different outputs. For instance, pelican
comes with an ArticlesGenerator and PagesGenerator, into
others. Given a configuration, they can do whatever you want
them to do. Most of the time its generating files from inputs
(user inputs and files).
I also deal with contents objects. They can be `Articles`, `Pages`, `Quotes`,
or whatever you want. They are defined in the contents.py module,
and represent some content to be used by the program.
In more details
===============
Here is an overview of the classes involved in pelican.
.. image:: _static/uml.jpg
The interface do not really exists, and I have added it only to clarify the
whole picture. I do use duck typing, and not interfaces.
Internally, the following process is followed:
* First of all, the command line is parsed, and some content from
the user are used to initialize the different generator objects.
* A `context` is created. It contains the settings from the command
line and a settings file if provided.
* The `generate_context` method of each generator is called, updating
the context.
* The writer is created, and given to the `generate_output` method of
each generator.
I make two calls because it is important that when the output is
generated by the generators, the context will not change. In other
words, the first method `generate_context` should modify the context,
whereas the second `generate_output` method should not.
Then, it is up to the generators to do what the want, in the
`generate_context` and `generate_content` method.
Taking the `ArticlesGenerator` class will help to understand some others
concepts. Here is what happens when calling the `generate_context`
method:
* Read the folder “path”, looking for restructured text files, load
each of them, and construct a content object (`Article`) with it. To do so,
use `Reader` objects.
* Update the `context` with all those articles.
Then, the `generate_content` method uses the `context` and the `writer` to
generate the wanted output

View file

@ -6,110 +6,252 @@ the command line::
$ pelican -s path/to/your/settingsfile.py path
Settings are given as the form of a python module (a file). You can have an
Settings are configured in the form of a Python module (a file). You can see an
example by looking at `/samples/pelican.conf.py
<https://github.com/ametaireau/pelican/raw/master/samples/pelican.conf.py>`_
All the settings identifiers must be set in caps, otherwise they will not be
All the setting identifiers must be set in all-caps, otherwise they will not be
processed.
The settings you define in the configuration file will be passed to the
templates, it allows you to use them to add site-wide contents if you need.
templates, which allows you to use your settings to add site-wide content.
Here is a list of settings for pelican, regarding the different features.
Here is a list of settings for Pelican:
Basic settings
==============
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`AUTHOR` Default author (put your name)
`SITENAME` (``'A Pelican Blog'``) Your site name
`DATE_FORMATS` (``{}``) If you do manage multiple languages, you can
set the date formatting here.
`DEFAULT_CATEGORY` (``'misc'``) The default category to fallback on.
set the date formatting here. See "Date format and locales"
section below for details.
`DEFAULT_CATEGORY` (``'misc'``) The default category to fall back on.
`DEFAULT_DATE_FORMAT` (``'%a %d %B %Y'``) The default date format you want to use.
`DISPLAY_PAGES_ON_MENU` (``True``) Display or not the pages on the menu of the
template. Templates can follow or not this
settings.
`FALLBACK_ON_FS_DATE` (``True``) If True, pelican will use the file system
dates infos (mtime) if it can't get
informations from the metadata
`DISPLAY_PAGES_ON_MENU` (``True``) Whether to display pages on the menu of the
template. Templates may or not honor this
setting.
`FALLBACK_ON_FS_DATE` (``True``) If True, Pelican will use the file system
timestamp information (mtime) if it can't get
date information from the metadata.
`JINJA_EXTENSIONS` (``[]``) A list of any Jinja2 extensions you want to use.
`DELETE_OUTPUT_DIRECTORY` (``False``) Delete the output directory and just
`DELETE_OUTPUT_DIRECTORY` (``False``) Delete the output directory as well as
the generated files.
`LOCALE` (''[1]_) Change the locale. A list of locales can be provided
`LOCALE` (''[#]_) Change the locale. A list of locales can be provided
here or a single string representing one locale.
When providing a list, all the locales will be tried
until one works.
`MARKUP` (``('rst', 'md')``) A list of available markup languages you want
to use. For the moment, only available values
to use. For the moment, the only available values
are `rst` and `md`.
`MD_EXTENSIONS` (``['codehilite','extra']``) A list of the extensions that the Markdown processor
will use. Refer to the extensions chapter in the
Python-Markdown documentation for a complete list of
supported extensions.
`OUTPUT_PATH` (``'output/'``) Where to output the generated files.
`PATH` (``None``) path to look at for input files.
`PATH` (``None``) Path to look at for input files.
`PAGE_DIR' (``'pages'``) Directory to look at for pages.
`PAGE_EXCLUDES' (``()``) A list of directories to exclude when looking for pages.
`ARTICLE_DIR' (``''``) Directory to look at for articles.
`ARTICLE_EXCLUDES': (``('pages',)``) A list of directories to exclude when looking for articles.
`PDF_GENERATOR` (``False``) Set to True if you want to have PDF versions
of your documents. You will need to install
`rst2pdf`.
`PLUGINS` (``[]``) The list of plugins to load. See :ref:`plugins`.
`RELATIVE_URL` (``True``) Defines if pelican should use relative urls or
`RELATIVE_URLS` (``True``) Defines whether Pelican should use relative URLs or
not.
`SITEURL` base URL of your website. Note that this is
not a way to tell pelican to use relative urls
or static ones. You should rather use the
`RELATIVE_URL` setting for such use.
`SITENAME` (``'A Pelican Blog'``) Your site name
`SITEURL` Base URL of your website. Note that this is
not a way to tell Pelican whether to use relative URLs
or static ones. You should instead use the
`RELATIVE_URL` setting for that purpose.
`STATIC_PATHS` (``['images']``) The static paths you want to have accessible
on the output path "static". By default,
pelican will copy the 'images' folder to the
Pelican will copy the 'images' folder to the
output folder.
`TIMEZONE` The timezone used in the date information, to
generate Atom and RSS feeds. See the "timezone"
section below for more info.
`TYPOGRIFY` (``False``) If set to true, some
additional transformations will be done on the
generated HTML, using the `Typogrify
<http://static.mintchaos.com/projects/typogrify/>`_
library
================================================ =====================================================
.. [#] Default is the system locale.
.. [1] Default is the system locale. Default is to delete the output directory.
Feed settings
=============
URL settings
------------
By default, pelican uses atom feeds. However, it is possible to use RSS feeds
instead, at your covenience.
You can customize the URL's and locations where files will be saved. The URL's and
SAVE_AS variables use python's format strings. These variables allow you to place
your articles in a location such as '{slug}/index.html' and link to then as
'{slug}' for clean urls. These settings give you the flexibility to place your
articles and pages anywhere you want.
Pelican generates category feeds as well as feeds for all your articles. It does
not generate feeds for tags per default, but it is possible to do so using
the ``TAG_FEED`` and ``TAG_FEED_RSS`` settings:
Note: If you specify a datetime directive, it will be substituted using the
input files' date metadata attribute. If the date is not specified for a
particular file, Pelican will rely on the file's mtime timestamp.
Check the Python datetime documentation at http://bit.ly/cNcJUC for more
information.
Also, you can use other file metadata attributes as well:
* slug
* date
* lang
* author
* category
Example usage:
* ARTICLE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/'
* ARTICLE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/index.html'
This would save your articles in something like '/posts/2011/Aug/07/sample-post/index.html',
and the URL to this would be '/posts/2011/Aug/07/sample-post/'.
================================================ =====================================================
Setting name (default value) what does it do?
================================================ =====================================================
`CATEGORY_FEED` ('feeds/%s.atom.xml'[2]_) Where to put the atom categories feeds.
`CATEGORY_FEED_RSS` (``None``, i.e. no RSS) Where to put the categories rss feeds.
`FEED` (``'feeds/all.atom.xml'``) relative url to output the atom feed.
`FEED_RSS` (``None``, i.e. no RSS) relative url to output the rss feed.
`TAG_FEED` (``None``, ie no tag feed) relative url to output the tags atom feed. It should
be defined using a "%s" matchin the tag name
`TAG_FEED_RSS` (``None``, ie no RSS tag feed) relative url to output the tag RSS feed
`ARTICLE_URL` ('{slug}.html') The URL to refer to an ARTICLE.
`ARTICLE_SAVE_AS` ('{slug}.html') The place where we will save an article.
`ARTICLE_LANG_URL` ('{slug}-{lang}.html') The URL to refer to an ARTICLE which doesn't use the
default language.
`ARTICLE_LANG_SAVE_AS` ('{slug}-{lang}.html' The place where we will save an article which
doesn't use the default language.
`PAGE_URL` ('pages/{slug}.html') The URL we will use to link to a page.
`PAGE_SAVE_AS` ('pages/{slug}.html') The location we will save the page.
`PAGE_LANG_URL` ('pages/{slug}-{lang}.html') The URL we will use to link to a page which doesn't
use the default language.
`PAGE_LANG_SAVE_AS` ('pages/{slug}-{lang}.html') The location we will save the page which doesn't
use the default language.
`AUTHOR_URL` ('author/{name}.html') The URL to use for an author.
`AUTHOR_SAVE_AS` ('author/{name}.html') The location to save an author.
`CATEGORY_URL` ('category/{name}.html') The URL to use for a category.
`CATEGORY_SAVE_AS` ('category/{name}.html') The location to save a category.
`TAG_URL` ('tag/{name}.html') The URL to use for a tag.
`TAG_SAVE_AS` ('tag/{name}.html') The location to save the tag page.
================================================ =====================================================
Timezone
--------
If no timezone is defined, UTC is assumed. This means that the generated Atom
and RSS feeds will contain incorrect date information if your locale is not UTC.
Pelican issues a warning in case this setting is not defined, as it was not
mandatory in previous versions.
Have a look at `the wikipedia page`_ to get a list of valid timezone values.
.. _the wikipedia page: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Date format and locale
----------------------
If no DATE_FORMAT is set, fall back to DEFAULT_DATE_FORMAT. If you need to
maintain multiple languages with different date formats, you can set this dict
using language name (``lang`` in your posts) as key. Regarding available format
codes, see `strftime document of python`_ :
.. parsed-literal::
DATE_FORMAT = {
'en': '%a, %d %b %Y',
'jp': '%Y-%m-%d(%a)',
}
You can set locale to further control date format:
.. parsed-literal::
LOCALE = ('usa', 'jpn', # On Windows
'en_US', 'ja_JP' # On Unix/Linux
)
Also, it is possible to set different locale settings for each language. If you
put (locale, format) tuples in the dict, this will override the LOCALE setting
above:
.. parsed-literal::
# On Unix/Linux
DATE_FORMAT = {
'en': ('en_US','%a, %d %b %Y'),
'jp': ('ja_JP','%Y-%m-%d(%a)'),
}
# On Windows
DATE_FORMAT = {
'en': ('usa','%a, %d %b %Y'),
'jp': ('jpn','%Y-%m-%d(%a)'),
}
This is a list of available `locales on Windows`_ . On Unix/Linux, usually you
can get a list of available locales via the ``locale -a`` command; see manpage
`locale(1)`_ for more information.
.. _strftime document of python: http://docs.python.org/library/datetime.html#strftime-strptime-behavior
.. _locales on Windows: http://msdn.microsoft.com/en-us/library/cdax410z%28VS.71%29.aspx
.. _locale(1): http://linux.die.net/man/1/locale
Feed settings
=============
By default, Pelican uses Atom feeds. However, it is also possible to use RSS
feeds if you prefer.
Pelican generates category feeds as well as feeds for all your articles. It does
not generate feeds for tags by default, but it is possible to do so using
the ``TAG_FEED`` and ``TAG_FEED_RSS`` settings:
================================================ =====================================================
Setting name (default value) What does it do?
================================================ =====================================================
`CATEGORY_FEED` ('feeds/%s.atom.xml'[2]_) Where to put the category Atom feeds.
`CATEGORY_FEED_RSS` (``None``, i.e. no RSS) Where to put the category RSS feeds.
`FEED` (``'feeds/all.atom.xml'``) Relative URL to output the Atom feed.
`FEED_RSS` (``None``, i.e. no RSS) Relative URL to output the RSS feed.
`TAG_FEED` (``None``, ie no tag feed) Relative URL to output the tag Atom feed. It should
be defined using a "%s" match in the tag name.
`TAG_FEED_RSS` (``None``, ie no RSS tag feed) Relative URL to output the tag RSS feed
`FEED_MAX_ITEMS` Maximum number of items allowed in a feed. Feed item
quantity is unrestricted by default.
================================================ =====================================================
If you don't want to generate some of these feeds, set ``None`` to the
variables above.
.. [2] %s is the name of the category.
Pagination
==========
The default behaviour of pelican is to list all the articles titles alongside
with a short description of them on the index page. While it works pretty well
for little to medium blogs, it is convenient to have a way to paginate this.
The default behaviour of Pelican is to list all the article titles along
with a short description on the index page. While it works pretty well
for small-to-medium blogs, for sites with large quantity of articles it would
be convenient to have a way to paginate the list.
You can use the following settings to configure the pagination.
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`DEFAULT_ORPHANS` (0) The minimum number of articles allowed on the
last page. Use this when you don't want to
have a last page with very few articles.
`DEFAULT_PAGINATION` (5) The maximum number of articles to include on a
page, not including orphans.
`WITH_PAGINATION` (``False``) Activate pagination.
`DEFAULT_PAGINATION` (False) The maximum number of articles to include on a
page, not including orphans. False to disable
pagination.
================================================ =====================================================
Tag cloud
@ -119,11 +261,11 @@ If you want to generate a tag cloud with all your tags, you can do so using the
following settings.
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`TAG_CLOUD_STEPS` (4) Count of different font sizes in the tag
cloud.
`TAG_CLOUD_MAX_ITEMS` (100) Maximum tags count in the cloud.
`TAG_CLOUD_MAX_ITEMS` (100) Maximum number of tags in the cloud.
================================================ =====================================================
The default theme does not support tag clouds, but it is pretty easy to add::
@ -134,157 +276,107 @@ The default theme does not support tag clouds, but it is pretty easy to add::
{% endfor %}
</ul>
You should then also define a CSS with the appropriate classes (tag-0 to tag-N, where
N matches `TAG_CLOUD_STEPS` -1.
You should then also define a CSS style with the appropriate classes (tag-0 to tag-N, where
N matches `TAG_CLOUD_STEPS` -1).
Translations
============
Pelican offers a way to translate articles. See the section on getting started for
more information about that.
Pelican offers a way to translate articles. See the Getting Started section for
more information.
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`DEFAULT_LANG` (``'en'``) The default language to use.
`TRANSLATION_FEED` ('feeds/all-%s.atom.xml'[3]_) Where to put the RSS feed for translations.
`TRANSLATION_FEED` ('feeds/all-%s.atom.xml'[3]_) Where to put the feed for translations.
================================================ =====================================================
.. [3] %s is the language
Ordering contents
Ordering content
=================
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`REVERSE_ARCHIVE_ORDER` (``False``) Reverse the archives order. (True makes it in
descending order: the newer first)
`REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True makes it in
descending order, default is alphabetically)
`REVERSE_ARCHIVE_ORDER` (``False``) Reverse the archives list order. (True: orders by date
in descending order, with newer articles first.)
`REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse
alphabetical order; default lists alphabetically.)
================================================ =====================================================
Theming
=======
Theming is addressed in a dedicated section (see :ref:`theming-pelican`).
Theming is addressed in a dedicated section (see :ref:`theming-pelican`).
However, here are the settings that are related to theming.
================================================ =====================================================
Setting name (default value) what does it do?
Setting name (default value) What does it do?
================================================ =====================================================
`THEME` theme to use to produce the output. can be the
`THEME` Theme to use to produce the output. Can be the
complete static path to a theme folder, or
chosen between the list of default themes (see
below)
`THEME_STATIC_PATHS` (``['static']``) Static theme paths you want to copy. Default
values is `static`, but if your theme has
value is `static`, but if your theme has
other static paths, you can put them here.
`CSS_FILE` (``'main.css'``) specify the CSS file you want to load
`CSS_FILE` (``'main.css'``) Specify the CSS file you want to load.
================================================ =====================================================
By default, two themes are availablee. You can specify them using the `-t` option:
By default, two themes are available. You can specify them using the `-t` option:
* notmyidea
* simple (a synonym for "full text" :)
You can define your own theme too, and specify it's emplacement in the same
way (be sure to specify the full absolute path to it).
You can define your own theme too, and specify its placement in the same
manner. (Be sure to specify the full absolute path to it.)
Here is `a guide on how to create your theme
<http://alexis.notmyidea.org/pelican/themes.html>`_
<http://pelican.notmyidea.org/en/latest/themes.html>`_
You can find a list of themes at http://github.com/ametaireau/pelican-themes.
Pelican comes with :doc:`pelican-themes` a small script for managing themes.
Pelican comes with :doc:`pelican-themes`, a small script for managing themes.
The `notmyidea` theme can make good use of the following settings. I recommend
to use them too in your themes.
using them in your themes as well.
======================= =======================================================
Setting name what does it do ?
Setting name What does it do ?
======================= =======================================================
`DISQUS_SITENAME` Pelican can handle disqus comments, specify the
sitename you've filled in on disqus
`GITHUB_URL` Your github URL (if you have one), it will then
use it to create a github ribbon.
`GOOGLE_ANALYTICS` 'UA-XXXX-YYYY' to activate google analytics.
`LINKS` A list of tuples (Title, Url) for links to appear on
the header.
`DISQUS_SITENAME` Pelican can handle Disqus comments. Specify the
Disqus sitename identifier here.
`GITHUB_URL` Your GitHub URL (if you have one). It will then
use this information to create a GitHub ribbon.
`GOOGLE_ANALYTICS` 'UA-XXXX-YYYY' to activate Google Analytics.
`MENUITEMS` A list of tuples (Title, URL) for additional menu
items to appear at the beginning of the main menu.
`PIWIK_URL` URL to your Piwik server - without 'http://' at the
beginning.
`PIWIK_SSL_URL` If the SSL-URL differs from the normal Piwik-URL
you have to include this setting too. (optional)
`PIWIK_SITE_ID` ID for the monitored website. You can find the ID
in the Piwik admin interface > settings > websites.
`SOCIAL` A list of tuples (Title, Url) to appear in the "social"
section.
`TWITTER_USERNAME` Allows to add a button on the articles to tweet about
them. Add you twitter username if you want this
button to appear.
`LINKS` A list of tuples (Title, URL) for links to appear on
the header.
`SOCIAL` A list of tuples (Title, URL) to appear in the
"social" section.
`TWITTER_USERNAME` Allows for adding a button to articles to encourage
others to tweet about them. Add your Twitter username
if you want this button to appear.
======================= =======================================================
In addition, you can use the "wide" version of the `notmyidea` theme, by
adding that in your configuration::
In addition, you can use the "wide" version of the `notmyidea` theme by
adding the following to your configuration::
CSS_FILE = "wide.css"
Skribit
-------
Skribit has two ways to display suggestions : as a sidebar widget or as a
suggestions tab. You can choose one of the display by setting the SKRIBIT_TYPE
in your config.
* SKRIBIT_WIDGET_ID : the identifier of your blog.
All the customizations are done in the skribit web interface.
To retrieve your identifier from the code snippet, you can use this python code::
import re
regex = re.compile('.*http://assets.skribit.com/javascripts/SkribitWidget.\
js\?renderTo=writeSkribitHere&amp;blog=(.*)&amp;.*')
snippet = '''SNIPPET CONTENT'''
snippet = snippet.replace('\n', '')
identifier = regex.match(snippet).groups()[0]
Suggestion tab
--------------
The setting for suggestion tab's customizations are :
* SKRIBIT_TAB_COLOR
* SKRIBIT_TAB_DISTANCE_HORIZ
* SKRIBIT_TAB_DISTANCE_VERT
* SKRIBIT_TAB_PLACEMENT
The identifier is :
* SKRIBIT_TAB_SITENAME : the identifier of your blog
To retrieve your sitename from the code snippet, you can use this python code::
import re
regex = re.compile('.*http://skribit.com/lightbox/(.*)\',.*')
snippet = '''SNIPPET CONTENT'''
snippet = snippet.replace('\n', '')
identifier = regex.match(snippet).groups()[0]
Skribit settings
----------------
================================================ =====================================================
Setting name (default value) what does it do?
================================================ =====================================================
`SKRIBIT_TYPE` The type of skribit widget (TAB or WIDGET).
`SKRIBIT_TAB_COLOR` Tab color (#XXXXXX, default #333333).
`SKRIBIT_TAB_HORIZ` Tab Distance from Left (% or distance, default Null).
`SKRIBIT_TAB_VERT` Tab Distance from Top (% or distance, default 20%).
`SKRIBIT_TAB_PLACEMENT` Tab placement (Top, Bottom, Left or Right,
default LEFT).
`SKRIBIT_TAB_SITENAME` Tab identifier (See Skribit part below).
`SKRIBIT_WIDGET_ID` Widget identifier (See Skribit part below).
================================================ =====================================================
.. _pelican-themes: :doc:`pelican-themes`
Example settings
================
.. literalinclude:: ../samples/pelican.conf.py
:language: python

View file

@ -1,10 +1,10 @@
.. _theming-pelican:
How to create themes for pelican
How to create themes for Pelican
################################
Pelican uses the great `jinja2 <http://jinja.pocoo.org>`_ templating engine to
generate it's HTML output. The jinja2 syntax is really simple. If you want to
generate its HTML output. The jinja2 syntax is really simple. If you want to
create your own theme, feel free to take inspiration from the "simple" theme,
which is available `here
<https://github.com/ametaireau/pelican/tree/master/pelican/themes/simple/templates>`_
@ -20,6 +20,8 @@ To make your own theme, you must follow the following structure::
└── templates
├── archives.html // to display archives
├── article.html // processed for each article
├── author.html // processed for each author
├── authors.html // must list all the authors
├── categories.html // must list all the categories
├── category.html // processed for each category
├── index.html // the index. List all the articles
@ -27,152 +29,262 @@ To make your own theme, you must follow the following structure::
├── tag.html // processed for each tag
└── tags.html // must list all the tags. Can be a tag cloud.
* `static` contains all the static content. It will be copied on the output
`theme/static` folder then. I've put the css and image folders, but they are
* `static` contains all the static assets, which will be copied to the output
`theme/static` folder. I've put the CSS and image folders here, but they are
just examples. Put what you need here.
* `templates` contains all the templates that will be used to generate the content.
I've just put the mandatory templates here, you can define your own if it helps
you to organize yourself while doing the theme.
I've just put the mandatory templates here; you can define your own if it helps
you keep things organized while creating your theme.
Templates and variables
=======================
It's using a simple syntax, that you can embbed into your html pages.
This document describes which templates should exist on a theme, and which
variables will be passed to each template, while generating it.
The idea is to use a simple syntax that you can embed into your HTML pages.
This document describes which templates should exist in a theme, and which
variables will be passed to each template at generation time.
All templates will receive the variables defined in your settings file, if they
are in caps. You can access them directly.
are in all-caps. You can access them directly.
Common variables
----------------
All of those settings will be given to all templates.
All of these settings will be available to all templates.
============= ===================================================
Variable Description
============= ===================================================
articles That's the list of articles, ordered desc. by date
all the elements are `Article` objects, so you can
access their properties (e.g. title, summary, author
etc.).
dates The same list of article, but ordered by date,
ascending.
tags A dict containing each tags (keys), and the list of
relative articles.
categories A dict containing each category (keys), and the
list of relative articles.
pages The list of pages.
articles The list of articles, ordered descending by date
All the elements are `Article` objects, so you can
access their attributes (e.g. title, summary, author
etc.)
dates The same list of articles, but ordered by date,
ascending
tags A key-value dict containing the tags (the keys) and
the list of respective articles (the values)
categories A key-value dict containing the categories (keys)
and the list of respective articles (values)
pages The list of pages
============= ===================================================
index.html
----------
Home page of your blog, will finally remain at output/index.html.
This is the home page of your blog, generated at output/index.html.
If pagination is active, next pages will remain at output/index`n`.html.
If pagination is active, subsequent pages will reside in output/index`n`.html.
=================== ===================================================
Variable Description
=================== ===================================================
articles_paginator A paginator object of article list.
articles_page The current page of articles.
dates_paginator A paginator object of article list, ordered by date,
ascending.
articles_paginator A paginator object for the list of articles
articles_page The current page of articles
dates_paginator A paginator object for the article list, ordered by
date, ascending.
dates_page The current page of articles, ordered by date,
ascending.
page_name 'index'. Useful for pagination links.
page_name 'index' -- useful for pagination links
=================== ===================================================
author.html
-------------
This template will be processed for each of the existing authors, with
output generated at output/author/`author_name`.html.
If pagination is active, subsequent pages will reside at
output/author/`author_name``n`.html.
=================== ===================================================
Variable Description
=================== ===================================================
author The name of the author being processed
articles Articles by this author
dates Articles by this author, but ordered by date,
ascending
articles_paginator A paginator object for the list of articles
articles_page The current page of articles
dates_paginator A paginator object for the article list, ordered by
date, ascending.
dates_page The current page of articles, ordered by date,
ascending.
page_name 'author/`author_name`' -- useful for pagination
links
=================== ===================================================
category.html
-------------
This template will be processed for each of the existing categories, and will
finally remain at output/category/`category_name`.html.
This template will be processed for each of the existing categories, with
output generated at output/category/`category_name`.html.
If pagination is active, next pages will remain at
If pagination is active, subsequent pages will reside at
output/category/`category_name``n`.html.
=================== ===================================================
Variable Description
=================== ===================================================
category The name of the category being processed.
articles Articles of this category.
dates Articles of this category, but ordered by date,
ascending.
articles_paginator A paginator object of article list.
articles_page The current page of articles.
dates_paginator A paginator object of article list, ordered by date,
ascending.
category The name of the category being processed
articles Articles for this category
dates Articles for this category, but ordered by date,
ascending
articles_paginator A paginator object for the list of articles
articles_page The current page of articles
dates_paginator A paginator object for the list of articles,
ordered by date, ascending
dates_page The current page of articles, ordered by date,
ascending.
page_name 'category/`category_name`'. Useful for pagination
links.
ascending
page_name 'category/`category_name`' -- useful for pagination
links
=================== ===================================================
article.html
-------------
This template will be processed for each article. .html files will be output
in output/`article_name`.html. Here are the specific variables it gets.
This template will be processed for each article, with .html files saved
as output/`article_name`.html. Here are the specific variables it gets.
============= ===================================================
Variable Description
============= ===================================================
article The article object to be displayed.
category The name of the category of the current article.
article The article object to be displayed
category The name of the category for the current article
============= ===================================================
page.html
---------
For each page, this template will be processed. It will create .html files in
output/`page_name`.html.
This template will be processed for each page, with corresponding .html files
saved as output/`page_name`.html.
============= ===================================================
Variable Description
============= ===================================================
page The page object to be displayed. You can access to
its title, slug and content.
page The page object to be displayed. You can access its
title, slug, and content.
============= ===================================================
tag.html
--------
For each tag, this template will be processed. It will create .html files in
output/tag/`tag_name`.html.
This template will be processed for each tag, with corresponding .html files
saved as output/tag/`tag_name`.html.
If pagination is active, next pages will remain at
If pagination is active, subsequent pages will reside at
output/tag/`tag_name``n`.html.
=================== ===================================================
Variable Description
=================== ===================================================
tag The name of the tag being processed.
articles Articles related to this tag.
tag The name of the tag being processed
articles Articles related to this tag
dates Articles related to this tag, but ordered by date,
ascending.
articles_paginator A paginator object of article list.
articles_page The current page of articles.
dates_paginator A paginator object of article list, ordered by date,
ascending.
ascending
articles_paginator A paginator object for the list of articles
articles_page The current page of articles
dates_paginator A paginator object for the list of articles,
ordered by date, ascending
dates_page The current page of articles, ordered by date,
ascending.
page_name 'tag/`tag_name`'. Useful for pagination links.
ascending
page_name 'tag/`tag_name`' -- useful for pagination links
=================== ===================================================
Include skribit script
======================
Inheritance
===========
In order to support skribit scripts in your themes, you must perform these
actions:
Since version 3.0, Pelican supports inheritance from the ``simple`` theme, so
you can re-use the ``simple`` theme templates in your own themes.
* Copy `skribit_tab_script.html` and `skribit_widget_script.html` in your
templates directory.
* Add {% include 'skribit_tab_script.html' %} in your <head> part in order to
support suggestions tab.
* Add {% include 'skribit_widget_script.html' %} where you want in order to
support sidebar widget.
If one of the mandatory files in the ``templates/`` directory of your theme is
missing, it will be replaced by the matching template from the ``simple`` theme.
So if the HTML structure of a template in the ``simple`` theme is right for you,
you don't have to write a new template from scratch.
You can take a look at notmyidea default theme for working example.
You can also extend templates from the ``simple`` themes in your own themes by using the ``{% extends %}`` directive as in the following example:
.. code-block:: html+jinja
{% extends "!simple/index.html" %} <!-- extends the ``index.html`` template from the ``simple`` theme -->
{% extends "index.html" %} <!-- "regular" extending -->
Example
-------
With this system, it is possible to create a theme with just two files.
base.html
"""""""""
The first file is the ``templates/base.html`` template:
.. code-block:: html+jinja
{% extends "!simple/base.html" %}
{% block head %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="{{ SITEURL }}/theme/css/style.css" />
{% endblock %}
1. On the first line, we extend the ``base.html`` template from the ``simple`` theme, so we don't have to rewrite the entire file.
2. On the third line, we open the ``head`` block which has already been defined in the ``simple`` theme.
3. On the fourth line, the function ``super()`` keeps the content previously inserted in the ``head`` block.
4. On the fifth line, we append a stylesheet to the page.
5. On the last line, we close the ``head`` block.
This file will be extended by all the other templates, so the stylesheet will be linked from all pages.
style.css
"""""""""
The second file is the ``static/css/style.css`` CSS stylesheet:
.. code-block:: css
body {
font-family : monospace ;
font-size : 100% ;
background-color : white ;
color : #111 ;
width : 80% ;
min-width : 400px ;
min-height : 200px ;
padding : 1em ;
margin : 5% 10% ;
border : thin solid gray ;
border-radius : 5px ;
display : block ;
}
a:link { color : blue ; text-decoration : none ; }
a:hover { color : blue ; text-decoration : underline ; }
a:visited { color : blue ; }
h1 a { color : inherit !important }
h2 a { color : inherit !important }
h3 a { color : inherit !important }
h4 a { color : inherit !important }
h5 a { color : inherit !important }
h6 a { color : inherit !important }
pre {
margin : 2em 1em 2em 4em ;
}
#menu li {
display : inline ;
}
#post-list {
margin-bottom : 1em ;
margin-top : 1em ;
}
Download
""""""""
You can download this example theme :download:`here <_static/theme-basic.zip>`.

33
docs/tips.rst Normal file
View file

@ -0,0 +1,33 @@
Tips
####
Here are some tips about Pelican that you might find useful.
Publishing to GitHub
====================
GitHub comes with an interesting "pages" feature: you can upload things there
and it will be available directly from their servers. As Pelican is a static
file generator, we can take advantage of this.
The excellent `ghp-import <https://github.com/davisp/ghp-import>`_ makes this
really easy. You will have to install it::
$ pip install ghp-import
Then, given a repository containing your articles, you would simply have
to run Pelican and upload the output to GitHub::
$ pelican -s pelican.conf.py .
$ ghp-import output
$ git push origin gh-pages
And that's it.
If you want, you can put that directly into a post-commit hook, so each time you
commit, your blog is up to date on GitHub!
Put the following into `.git/hooks/post-commit`::
pelican -s pelican.conf.py . && ghp-import output && git push origin
gh-pages

View file

@ -1,17 +1,21 @@
import argparse
import os
import sys
import re
import time
from pelican import signals
from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator)
from pelican.settings import read_settings
from pelican.settings import read_settings, _DEFAULT_CONFIG
from pelican.utils import clean_output_dir, files_changed
from pelican.writers import Writer
from pelican import log
VERSION = "2.7.2"
__major__ = 3
__minor__ = 0
__version__ = "{0}.{1}".format(__major__, __minor__)
class Pelican(object):
@ -20,9 +24,12 @@ class Pelican(object):
"""Read the settings, and performs some checks on the environment
before doing anything else.
"""
if settings is None:
settings = _DEFAULT_CONFIG
self.path = path or settings['PATH']
if not self.path:
raise Exception('you need to specify a path containing the content'
raise Exception('You need to specify a path containing the content'
' (see pelican --help for more information)')
if self.path.endswith('/'):
@ -30,11 +37,15 @@ class Pelican(object):
# define the default settings
self.settings = settings
self._handle_deprecation()
self.theme = theme or settings['THEME']
output_path = output_path or settings['OUTPUT_PATH']
self.output_path = os.path.realpath(output_path)
self.markup = markup or settings['MARKUP']
self.delete_outputdir = delete_outputdir or settings['DELETE_OUTPUT_DIRECTORY']
self.delete_outputdir = delete_outputdir \
or settings['DELETE_OUTPUT_DIRECTORY']
# find the theme in pelican.theme if the given one does not exists
if not os.path.exists(self.theme):
@ -44,7 +55,7 @@ class Pelican(object):
self.theme = theme_path
else:
raise Exception("Impossible to find the theme %s" % theme)
self.init_plugins()
signals.initialized.send(self)
@ -52,13 +63,52 @@ class Pelican(object):
self.plugins = self.settings['PLUGINS']
for plugin in self.plugins:
# if it's a string, then import it
if isinstance(plugin, str):
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):
log.warning('Found deprecated `CLEAN_URLS` in settings. Modifing'
' the following settings for the same behaviour.')
self.settings['ARTICLE_URL'] = '{slug}/'
self.settings['ARTICLE_LANG_URL'] = '{slug}-{lang}/'
self.settings['PAGE_URL'] = 'pages/{slug}/'
self.settings['PAGE_LANG_URL'] = 'pages/{slug}-{lang}/'
for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL',
'PAGE_LANG_URL'):
log.warning("%s = '%s'" % (setting, self.settings[setting]))
if self.settings.get('ARTICLE_PERMALINK_STRUCTURE', False):
log.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in'
' settings. Modifing the following settings for'
' the same behaviour.')
structure = self.settings['ARTICLE_PERMALINK_STRUCTURE']
# Convert %(variable) into {variable}.
structure = re.sub('%\((\w+)\)s', '{\g<1>}', structure)
# Convert %x into {date:%x} for strftime
structure = re.sub('(%[A-z])', '{date:\g<1>}', structure)
# Strip a / prefix
structure = re.sub('^/', '', structure)
for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL',
'PAGE_LANG_URL', 'ARTICLE_SAVE_AS',
'ARTICLE_LANG_SAVE_AS', 'PAGE_SAVE_AS',
'PAGE_LANG_SAVE_AS'):
self.settings[setting] = os.path.join(structure,
self.settings[setting])
log.warning("%s = '%s'" % (setting, self.settings[setting]))
def run(self):
"""Run the generators and return"""
@ -79,9 +129,9 @@ class Pelican(object):
if hasattr(p, 'generate_context'):
p.generate_context()
# erase the directory if it is not the source and if that's
# erase the directory if it is not the source and if that's
# explicitely asked
if (self.delete_outputdir and
if (self.delete_outputdir and not
os.path.realpath(self.path).startswith(self.output_path)):
clean_output_dir(self.output_path)
@ -91,7 +141,6 @@ class Pelican(object):
if hasattr(p, 'generate_output'):
p.generate_output(writer)
def get_generator_classes(self):
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator]
if self.settings['PDF_GENERATOR']:
@ -100,45 +149,51 @@ class Pelican(object):
def get_writer(self):
return Writer(self.output_path, settings=self.settings)
def main():
parser = argparse.ArgumentParser(description="""A tool to generate a
static blog, with restructured text input files.""")
static blog, with restructured text input files.""",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(dest='path', nargs='?',
help='Path where to find the content files')
help='Path where to find the content files.')
parser.add_argument('-t', '--theme-path', dest='theme',
help='Path where to find the theme templates. If not specified, it'
'will use the default one included with pelican.')
parser.add_argument('-o', '--output', dest='output',
help='Where to output the generated files. If not specified, a directory'
' will be created, named "output" in the current path.')
parser.add_argument('-m', '--markup', default=None, dest='markup',
help='the list of markup language to use (rst or md). Please indicate '
'them separated by commas')
parser.add_argument('-s', '--settings', dest='settings', default='',
help='the settings of the application.')
parser.add_argument('-d', '--delete-output-directory', dest='delete_outputdir',
help='Where to output the generated files. If not specified, a '
'directory will be created, named "output" in the current path.')
parser.add_argument('-m', '--markup', dest='markup',
help='The list of markup language to use (rst or md). Please indicate '
'them separated by commas.')
parser.add_argument('-s', '--settings', dest='settings',
help='The settings of the application.')
parser.add_argument('-d', '--delete-output-directory',
dest='delete_outputdir',
action='store_true', help='Delete the output directory.')
parser.add_argument('-v', '--verbose', action='store_const', const=log.INFO, dest='verbosity',
help='Show all messages')
parser.add_argument('-q', '--quiet', action='store_const', const=log.CRITICAL, dest='verbosity',
help='Show only critical errors')
parser.add_argument('-D', '--debug', action='store_const', const=log.DEBUG, dest='verbosity',
help='Show all message, including debug messages')
parser.add_argument('--version', action='version', version=VERSION,
help='Print the pelican version and exit')
parser.add_argument('-r', '--autoreload', dest='autoreload', action='store_true',
help="Relaunch pelican each time a modification occurs on the content"
"files")
parser.add_argument('-v', '--verbose', action='store_const',
const=log.INFO, dest='verbosity',
help='Show all messages.')
parser.add_argument('-q', '--quiet', action='store_const',
const=log.CRITICAL, dest='verbosity',
help='Show only critical errors.')
parser.add_argument('-D', '--debug', action='store_const',
const=log.DEBUG, dest='verbosity',
help='Show all message, including debug messages.')
parser.add_argument('--version', action='version', version=__version__,
help='Print the pelican version and exit.')
parser.add_argument('-r', '--autoreload', dest='autoreload',
action='store_true',
help="Relaunch pelican each time a modification occurs"
" on the content files.")
args = parser.parse_args()
log.init(args.verbosity)
# Split the markup languages only if some have been given. Otherwise, populate
# the variable with None.
markup = [a.strip().lower() for a in args.markup.split(',')] if args.markup else None
# Split the markup languages only if some have been given. Otherwise,
# populate the variable with None.
markup = [a.strip().lower() for a in args.markup.split(',')]\
if args.markup else None
settings = read_settings(args.settings)
@ -168,8 +223,9 @@ def main():
else:
pelican.run()
except Exception, e:
log.critical(str(e))
log.critical(unicode(e))
if __name__ == '__main__':
main()
if (args.verbosity == log.DEBUG):
raise
else:
sys.exit(getattr(e, 'exitcode', 1))

View file

@ -1,7 +1,14 @@
# -*- coding: utf-8 -*-
from pelican.utils import slugify, truncate_html_words
from pelican.log import *
from datetime import datetime
from os import getenv
from sys import platform, stdin
import functools
import locale
from pelican.log import warning, error
from pelican.settings import _DEFAULT_CONFIG
from pelican.utils import slugify, truncate_html_words
class Page(object):
"""Represents a page
@ -11,29 +18,34 @@ class Page(object):
"""
mandatory_properties = ('title',)
def __init__(self, content, metadata=None, settings=None, filename=None):
def __init__(self, content, metadata=None, settings=None,
filename=None):
# init parameters
if not metadata:
metadata = {}
if not settings:
settings = _DEFAULT_CONFIG
self.settings = settings
self._content = content
self.translations = []
self.status = "published" # default value
local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
local_metadata.update(metadata)
# set metadata as attributes
for key, value in local_metadata.items():
setattr(self, key.lower(), value)
# default author to the one in settings if not defined
if not hasattr(self, 'author'):
if 'AUTHOR' in settings:
self.author = settings['AUTHOR']
self.author = Author(settings['AUTHOR'], settings)
else:
title = filename.decode('utf-8') if filename else self.title
self.author = Author(getenv('USER', 'John Doe'), settings)
warning(u"Author of `{0}' unknown, assuming that his name is "
"`{1}'".format(title, self.author))
# manage languages
self.in_default_lang = True
@ -48,21 +60,6 @@ class Page(object):
if not hasattr(self, 'slug') and hasattr(self, 'title'):
self.slug = slugify(self.title)
# create save_as from the slug (+lang)
if not hasattr(self, 'save_as') and hasattr(self, 'slug'):
if self.in_default_lang:
self.save_as = '%s.html' % self.slug
clean_url = '%s/' % self.slug
else:
self.save_as = '%s-%s.html' % (self.slug, self.lang)
clean_url = '%s-%s/' % (self.slug, self.lang)
# change the save_as regarding the settings
if settings.get('CLEAN_URLS', False):
self.url = clean_url
elif hasattr(self, 'save_as'):
self.url = self.save_as
if filename:
self.filename = filename
@ -73,16 +70,29 @@ class Page(object):
else:
self.date_format = settings['DEFAULT_DATE_FORMAT']
if hasattr(self, 'date'):
self.locale_date = self.date.strftime(self.date_format.encode('ascii','xmlcharrefreplace')).decode('utf')
if isinstance(self.date_format, tuple):
locale.setlocale(locale.LC_ALL, self.date_format[0])
self.date_format = self.date_format[1]
# manage summary
if not hasattr(self, 'summary'):
self.summary = property(lambda self: truncate_html_words(self.content, 50)).__get__(self, Page)
if hasattr(self, 'date'):
encoded_date = self.date.strftime(
self.date_format.encode('ascii', 'xmlcharrefreplace'))
if platform == 'win32':
self.locale_date = encoded_date.decode(stdin.encoding)
else:
self.locale_date = encoded_date.decode('utf')
# manage status
if not hasattr(self, 'status'):
self.status = settings['DEFAULT_STATUS']
if not settings['WITH_FUTURE_DATES']:
if hasattr(self, 'date') and self.date > datetime.now():
self.status = 'draft'
# store the summary metadata if it is set
if 'summary' in metadata:
self._summary = metadata['summary']
def check_properties(self):
"""test that each mandatory property is set."""
@ -90,6 +100,24 @@ class Page(object):
if not hasattr(self, prop):
raise NameError(prop)
@property
def url_format(self):
return {
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
'date': getattr(self, 'date', datetime.now()),
'author': self.author,
'category': getattr(self, 'category', 'misc'),
}
def _expand_settings(self, key):
fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()
return self.settings[fq_key].format(**self.url_format)
def get_url_setting(self, key):
key = key if self.in_default_lang else 'lang_%s' % key
return self._expand_settings(key)
@property
def content(self):
if hasattr(self, "_get_content"):
@ -98,6 +126,24 @@ class Page(object):
content = self._content
return content
def _get_summary(self):
"""Returns the summary of an article, based on the summary metadata
if it is set, else troncate the content."""
if hasattr(self, '_summary'):
return self._summary
else:
return truncate_html_words(self.content, 50)
def _set_summary(self, summary):
"""Dummy function"""
pass
summary = property(_get_summary, _set_summary, "Summary of the article."
"Based on the content. Can't be set")
url = property(functools.partial(get_url_setting, key='url'))
save_as = property(functools.partial(get_url_setting, key='save_as'))
class Article(Page):
mandatory_properties = ('title', 'date', 'category')
@ -107,10 +153,53 @@ class Quote(Page):
base_properties = ('author', 'date')
class URLWrapper(object):
def __init__(self, name, settings):
self.name = unicode(name)
self.slug = slugify(self.name)
self.settings = settings
def as_dict(self):
return self.__dict__
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == unicode(other)
def __str__(self):
return str(self.name)
def __unicode__(self):
return self.name
def _from_settings(self, key):
setting = "%s_%s" % (self.__class__.__name__.upper(), key)
return self.settings[setting].format(**self.as_dict())
url = property(functools.partial(_from_settings, key='URL'))
save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
class Category(URLWrapper):
pass
class Tag(URLWrapper):
def __init__(self, name, *args, **kwargs):
super(Tag, self).__init__(unicode.strip(name), *args, **kwargs)
class Author(URLWrapper):
pass
def is_valid_content(content, f):
try:
content.check_properties()
return True
except NameError, e:
error(u"Skipping %s: impossible to find informations about '%s'" % (f, e))
error(u"Skipping %s: impossible to find informations about '%s'"\
% (f, e))
return False

212
pelican/generators.py Executable file → Normal file
View file

@ -1,20 +1,21 @@
# -*- coding: utf-8 -*-
from operator import attrgetter, itemgetter
from itertools import chain
from functools import partial
from datetime import datetime
from collections import defaultdict
import os
import datetime
import math
import random
from jinja2 import Environment, FileSystemLoader
from collections import defaultdict
from functools import partial
from itertools import chain
from operator import attrgetter, itemgetter
from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
from jinja2.exceptions import TemplateNotFound
from pelican.utils import copy, get_relative_path, process_translations, open
from pelican.contents import Article, Page, is_valid_content
from pelican.contents import Article, Page, Category, is_valid_content
from pelican.log import warning, error, debug, info
from pelican.readers import read_file
from pelican.log import *
from pelican.utils import copy, process_translations, open
from pelican import signals
@ -31,12 +32,24 @@ class Generator(object):
# templates cache
self._templates = {}
self._templates_path = os.path.expanduser(os.path.join(self.theme, 'templates'))
self._templates_path = os.path.expanduser(
os.path.join(self.theme, 'templates'))
theme_path = os.path.dirname(os.path.abspath(__file__))
simple_loader = FileSystemLoader(os.path.join(theme_path,
"themes", "simple", "templates"))
self._env = Environment(
loader=FileSystemLoader(self._templates_path),
loader=ChoiceLoader([
FileSystemLoader(self._templates_path),
simple_loader, # implicit inheritance
PrefixLoader({'!simple': simple_loader}) # explicit one
]),
extensions=self.settings.get('JINJA_EXTENSIONS', []),
)
debug('template list: {0}'.format(self._env.list_templates()))
# get custom Jinja filters from user settings
custom_filters = self.settings.get('JINJA_FILTERS', {})
self._env.filters.update(custom_filters)
@ -50,8 +63,8 @@ class Generator(object):
try:
self._templates[name] = self._env.get_template(name + '.html')
except TemplateNotFound:
raise Exception('[templates] unable to load %s.html from %s' % (
name, self._templates_path))
raise Exception('[templates] unable to load %s.html from %s' \
% (name, self._templates_path))
return self._templates[name]
def get_files(self, path, exclude=[], extensions=None):
@ -67,7 +80,7 @@ class Generator(object):
try:
iter = os.walk(path, followlinks=True)
except TypeError: # python 2.5 does not support followlinks
except TypeError: # python 2.5 does not support followlinks
iter = os.walk(path)
for root, dirs, temp_files in iter:
@ -94,11 +107,12 @@ class ArticlesGenerator(Generator):
def __init__(self, *args, **kwargs):
"""initialize properties"""
self.articles = [] # only articles in default language
self.articles = [] # only articles in default language
self.translations = []
self.dates = {}
self.tags = defaultdict(list)
self.categories = defaultdict(list)
self.authors = defaultdict(list)
super(ArticlesGenerator, self).__init__(*args, **kwargs)
self.drafts = []
signals.article_generator_init.send(self)
@ -106,49 +120,52 @@ class ArticlesGenerator(Generator):
def generate_feeds(self, writer):
"""Generate the feeds from the current context, and output files."""
writer.write_feed(self.articles, self.context, self.settings['FEED'])
if 'FEED_RSS' in self.settings:
if self.settings.get('FEED'):
writer.write_feed(self.articles, self.context,
self.settings['FEED_RSS'], feed_type='rss')
self.settings['FEED'])
if self.settings.get('FEED_RSS'):
writer.write_feed(self.articles, self.context,
self.settings['FEED_RSS'], feed_type='rss')
for cat, arts in self.categories:
arts.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED'] % cat)
if 'CATEGORY_FEED_RSS' in self.settings:
if self.settings.get('CATEGORY_FEED'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_RSS'] % cat,
feed_type='rss')
self.settings['CATEGORY_FEED'] % cat)
if 'TAG_FEED' in self.settings:
if self.settings.get('CATEGORY_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_RSS'] % cat,
feed_type='rss')
if self.settings.get('TAG_FEED') or self.settings.get('TAG_FEED_RSS'):
for tag, arts in self.tags.items():
arts.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(arts, self.context,
self.settings['TAG_FEED'] % tag)
if 'TAG_FEED_RSS' in self.settings:
if self.settings.get('TAG_FEED'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_RSS'] % tag, feed_type='rss')
self.settings['TAG_FEED'] % tag)
translations_feeds = defaultdict(list)
for article in chain(self.articles, self.translations):
translations_feeds[article.lang].append(article)
if self.settings.get('TAG_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_RSS'] % tag,
feed_type='rss')
for lang, items in translations_feeds.items():
items.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(items, self.context,
self.settings['TRANSLATION_FEED'] % lang)
if self.settings.get('TRANSLATION_FEED'):
translations_feeds = defaultdict(list)
for article in chain(self.articles, self.translations):
translations_feeds[article.lang].append(article)
for lang, items in translations_feeds.items():
items.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(items, self.context,
self.settings['TRANSLATION_FEED'] % lang)
def generate_pages(self, writer):
"""Generate the pages on the disk"""
write = partial(
writer.write_file,
relative_urls = self.settings.get('RELATIVE_URLS')
)
write = partial(writer.write_file,
relative_urls=self.settings.get('RELATIVE_URLS'))
# to minimize the number of relative path stuff modification
# in writer, articles pass first
@ -163,55 +180,69 @@ class ArticlesGenerator(Generator):
paginated = {}
if template in PAGINATED_TEMPLATES:
paginated = {'articles': self.articles, 'dates': self.dates}
write('%s.html' % template, self.get_template(template), self.context,
blog=True, paginated=paginated, page_name=template)
write('%s.html' % template, self.get_template(template),
self.context, blog=True, paginated=paginated,
page_name=template)
# and subfolders after that
tag_template = self.get_template('tag')
for tag, articles in self.tags.items():
articles.sort(key=attrgetter('date'), reverse=True)
dates = [article for article in self.dates if article in articles]
write('tag/%s.html' % tag, tag_template, self.context, tag=tag,
write(tag.save_as, tag_template, self.context, tag=tag,
articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name='tag/%s' % tag)
page_name=u'tag/%s' % tag)
category_template = self.get_template('category')
for cat, articles in self.categories:
dates = [article for article in self.dates if article in articles]
write('category/%s.html' % cat, category_template, self.context,
write(cat.save_as, category_template, self.context,
category=cat, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name='category/%s' % cat)
page_name=u'category/%s' % cat)
author_template = self.get_template('author')
for aut, articles in self.authors:
dates = [article for article in self.dates if article in articles]
write(aut.save_as, author_template, self.context,
author=aut, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name=u'author/%s' % aut)
for article in self.drafts:
write('drafts/%s.html' % article.slug, article_template, self.context,
article=article, category=article.category)
write('drafts/%s.html' % article.slug, article_template,
self.context, article=article, category=article.category)
def generate_context(self):
"""change the context"""
# return the list of files to use
files = self.get_files(self.path, exclude=['pages',])
all_articles = []
for f in files:
content, metadata = read_file(f)
for f in self.get_files(
os.path.join(self.path, self.settings['ARTICLE_DIR']),
exclude=self.settings['ARTICLE_EXCLUDES']):
try:
content, metadata = read_file(f, settings=self.settings)
except Exception, e:
warning(u'Could not process %s\n%s' % (f, str(e)))
continue
# if no category is set, use the name of the path as a category
if 'category' not in metadata.keys():
if 'category' not in metadata:
if os.path.dirname(f) == self.path:
category = self.settings['DEFAULT_CATEGORY']
else:
category = os.path.basename(os.path.dirname(f))
category = os.path.basename(os.path.dirname(f))\
.decode('utf-8')
if category != '':
metadata['category'] = unicode(category)
metadata['category'] = Category(category, self.settings)
if 'date' not in metadata.keys()\
and self.settings['FALLBACK_ON_FS_DATE']:
metadata['date'] = datetime.fromtimestamp(os.stat(f).st_ctime)
if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']:
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,
@ -232,7 +263,7 @@ class ArticlesGenerator(Generator):
for article in self.articles:
# only main articles are listed in categories, not translations
self.categories[article.category].append(article)
self.authors[article.author].append(article)
# sort the articles by date
self.articles.sort(key=attrgetter('date'), reverse=True)
@ -246,21 +277,20 @@ class ArticlesGenerator(Generator):
for tag in getattr(article, 'tags', []):
tag_cloud[tag] += 1
tag_cloud = sorted(tag_cloud.items(), key = itemgetter(1), reverse = True)
tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
tags = map(itemgetter(1), tag_cloud)
if tags:
max_count = max(tags)
max_count = max(tags)
steps = self.settings.get('TAG_CLOUD_STEPS')
# calculate word sizes
self.tag_cloud = [
(
tag,
int(
math.floor(steps - (steps - 1) * math.log(count) / (math.log(max_count)or 1))
)
int(math.floor(steps - (steps - 1) * math.log(count)
/ (math.log(max_count)or 1)))
)
for tag, count in tag_cloud
]
@ -271,9 +301,13 @@ class ArticlesGenerator(Generator):
# order the categories per name
self.categories = list(self.categories.items())
self.categories.sort(reverse=self.settings.get('REVERSE_CATEGORY_ORDER'))
self._update_context(('articles', 'dates', 'tags', 'categories', 'tag_cloud'))
self.categories.sort(reverse=self.settings['REVERSE_CATEGORY_ORDER'])
self.authors = list(self.authors.items())
self.authors.sort()
self._update_context(('articles', 'dates', 'tags', 'categories',
'tag_cloud', 'authors'))
def generate_output(self, writer):
self.generate_feeds(writer)
@ -289,8 +323,14 @@ class PagesGenerator(Generator):
def generate_context(self):
all_pages = []
for f in self.get_files(os.sep.join((self.path, 'pages'))):
content, metadata = read_file(f)
for f in self.get_files(
os.path.join(self.path, self.settings['PAGE_DIR']),
exclude=self.settings['PAGE_EXCLUDES']):
try:
content, metadata = read_file(f)
except Exception, e:
error(u'Could not process %s\n%s' % (f, str(e)))
continue
page = Page(content, metadata, settings=self.settings,
filename=f)
if not is_valid_content(page, f):
@ -304,9 +344,9 @@ class PagesGenerator(Generator):
def generate_output(self, writer):
for page in chain(self.translations, self.pages):
writer.write_file('pages/%s' % page.save_as, self.get_template('page'),
writer.write_file(page.save_as, self.get_template('page'),
self.context, page=page,
relative_urls = self.settings.get('RELATIVE_URLS'))
relative_urls=self.settings.get('RELATIVE_URLS'))
class StaticGenerator(Generator):
@ -317,8 +357,8 @@ class StaticGenerator(Generator):
final_path=None):
"""Copy all the paths from source to destination"""
for path in paths:
copy(path, source, os.path.join(output_path, destination), final_path,
overwrite=True)
copy(path, source, os.path.join(output_path, destination),
final_path, overwrite=True)
def generate_output(self, writer):
self._copy_paths(self.settings['STATIC_PATHS'], self.path,
@ -328,7 +368,8 @@ class StaticGenerator(Generator):
# copy all the files needed
for source, destination in self.settings['FILES_TO_COPY']:
copy(source, self.path, self.output_path, destination, overwrite=True)
copy(source, self.path, self.output_path, destination,
overwrite=True)
class PdfGenerator(Generator):
@ -337,7 +378,8 @@ class PdfGenerator(Generator):
def __init__(self, *args, **kwargs):
try:
from rst2pdf.createpdf import RstToPdf
self.pdfcreator = RstToPdf(breakside=0, stylesheets=['twelvepoint'])
self.pdfcreator = RstToPdf(breakside=0,
stylesheets=['twelvepoint'])
except ImportError:
raise Exception("unable to find rst2pdf")
super(PdfGenerator, self).__init__(*args, **kwargs)
@ -345,9 +387,10 @@ class PdfGenerator(Generator):
def _create_pdf(self, obj, output_path):
if obj.filename.endswith(".rst"):
filename = obj.slug + ".pdf"
output_pdf=os.path.join(output_path, filename)
output_pdf = os.path.join(output_path, filename)
# print "Generating pdf for", obj.filename, " in ", output_pdf
self.pdfcreator.createPdf(text=open(obj.filename), output=output_pdf)
with open(obj.filename) as f:
self.pdfcreator.createPdf(text=f, output=output_pdf)
info(u' [ok] writing %s' % output_pdf)
def generate_context(self):
@ -358,11 +401,12 @@ class PdfGenerator(Generator):
# since we write our own files
info(u' Generating PDF files...')
pdf_path = os.path.join(self.output_path, 'pdf')
try:
os.mkdir(pdf_path)
except OSError:
error("Couldn't create the pdf output folder in " + pdf_path)
pass
if not os.path.exists(pdf_path):
try:
os.mkdir(pdf_path)
except OSError:
error("Couldn't create the pdf output folder in " + pdf_path)
pass
for article in self.context['articles']:
self._create_pdf(article, pdf_path)

View file

@ -1,30 +1,33 @@
from logging import CRITICAL, ERROR, WARN, INFO, DEBUG
import os
import sys
from logging import CRITICAL, ERROR, WARN, INFO, DEBUG
from logging import critical, error, info, warning, warn, debug
from logging import Formatter, getLogger, StreamHandler
import sys
import os
global ANSI
ANSI = {
'gray' : lambda(text) : u'\033[1;30m' + unicode(text) + u'\033[1;m',
'red' : lambda(text) : u'\033[1;31m' + unicode(text) + u'\033[1;m',
'green' : lambda(text) : u'\033[1;32m' + unicode(text) + u'\033[1;m',
'yellow' : lambda(text) : u'\033[1;33m' + unicode(text) + u'\033[1;m',
'blue' : lambda(text) : u'\033[1;34m' + unicode(text) + u'\033[1;m',
'magenta' : lambda(text) : u'\033[1;35m' + unicode(text) + u'\033[1;m',
'cyan' : lambda(text) : u'\033[1;36m' + unicode(text) + u'\033[1;m',
'white' : lambda(text) : u'\033[1;37m' + unicode(text) + u'\033[1;m',
'bgred' : lambda(text) : u'\033[1;41m' + unicode(text) + u'\033[1;m',
'bggreen' : lambda(text) : u'\033[1;42m' + unicode(text) + u'\033[1;m',
'bgbrown' : lambda(text) : u'\033[1;43m' + unicode(text) + u'\033[1;m',
'bgblue' : lambda(text) : u'\033[1;44m' + unicode(text) + u'\033[1;m',
'bgmagenta' : lambda(text) : u'\033[1;45m' + unicode(text) + u'\033[1;m',
'bgcyan' : lambda(text) : u'\033[1;46m' + unicode(text) + u'\033[1;m',
'bggray' : lambda(text) : u'\033[1;47m' + unicode(text) + u'\033[1;m',
'bgyellow' : lambda(text) : u'\033[1;43m' + unicode(text) + u'\033[1;m',
'bggrey' : lambda(text) : u'\033[1;100m' + unicode(text) + u'\033[1;m'
RESET_TERM = u'\033[0;m'
def start_color(index):
return u'\033[1;{0}m'.format(index)
def term_color(color):
code = COLOR_CODES[color]
return lambda text: start_color(code) + unicode(text) + RESET_TERM
COLOR_CODES = {
'red': 31,
'yellow': 33,
'cyan': 36,
'white': 37,
'bgred': 41,
'bggrey': 100,
}
ANSI = dict((col, term_color(col)) for col in COLOR_CODES)
class ANSIFormatter(Formatter):
"""
@ -62,17 +65,16 @@ class TextFormatter(Formatter):
class DummyFormatter(object):
"""
A dummy class.
Return an instance of the appropriate formatter (ANSIFormatter if sys.stdout.isatty() is True, else TextFormatter)
Return an instance of the appropriate formatter (ANSIFormatter if
sys.stdout.isatty() is True, else TextFormatter)
"""
def __new__(cls, *args, **kwargs):
if os.isatty(sys.stdout.fileno()): # thanks to http://stackoverflow.com/questions/2086961/how-can-i-determine-if-a-python-script-is-executed-from-crontab/2087031#2087031
if os.isatty(sys.stdout.fileno())\
and not sys.platform.startswith('win'):
return ANSIFormatter(*args, **kwargs)
else:
return TextFormatter( *args, **kwargs)
return TextFormatter(*args, **kwargs)
def init(level=None, logger=getLogger(), handler=StreamHandler()):
@ -93,15 +95,15 @@ if __name__ == '__main__':
__all__ = [
"debug",
"info",
"warn",
"debug",
"info",
"warn",
"warning",
"error",
"critical",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"error",
"critical",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"CRITICAL"
]
]

View file

@ -1,6 +1,7 @@
# From django.core.paginator
from math import ceil
class Paginator(object):
def __init__(self, object_list, per_page, orphans=0):
self.object_list = object_list
@ -39,6 +40,7 @@ class Paginator(object):
return range(1, self.num_pages + 1)
page_range = property(_get_page_range)
class Page(object):
def __init__(self, object_list, number, paginator):
self.object_list = object_list
@ -82,4 +84,3 @@ class Page(object):
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page

View file

@ -2,39 +2,39 @@ import hashlib
from pelican import signals
"""
Gravata plugin for Pelican
==========================
Gravatar plugin for Pelican
===========================
Simply add author_gravatar variable in article's context, which contain
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
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
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():
def register():
signals.article_generate_context.connect(add_gravatar)

View file

@ -10,7 +10,7 @@ Directives
----------
.. html::
(HTML code)
@ -25,12 +25,12 @@ A search engine:
<input type="hidden" name="lang" value="en" />
<input type="submit" value="Seeks !" id="search_button" />
</form>
A contact form:
.. html::
<form method="GET" action="mailto:some email">
<p>
<input type="text" placeholder="Subject" name="subject">

View file

@ -6,32 +6,39 @@ try:
from docutils.writers.html4css1 import HTMLTranslator
# import the directives to have pygments support
from pelican import rstdirectives
from pelican import rstdirectives # NOQA
except ImportError:
core = False
try:
from markdown import Markdown
except ImportError:
Markdown = False
Markdown = False # NOQA
import re
from pelican.contents import Category, Tag, Author
from pelican.utils import get_date, open
_METADATA_PROCESSORS = {
'tags': lambda x: map(unicode.strip, x.split(',')),
'date': lambda x: get_date(x),
'status': unicode.strip,
'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')],
'date': lambda x, y: get_date(x),
'status': lambda x, y: unicode.strip(x),
'category': Category,
'author': Author,
}
def _process_metadata(name, value):
if name.lower() in _METADATA_PROCESSORS:
return _METADATA_PROCESSORS[name.lower()](value)
return value
class Reader(object):
enabled = True
extensions = None
def __init__(self, settings):
self.settings = settings
def process_metadata(self, name, value):
if name in _METADATA_PROCESSORS:
return _METADATA_PROCESSORS[name](value, self.settings)
return value
class _FieldBodyTranslator(HTMLTranslator):
@ -51,33 +58,35 @@ def render_node_to_html(document, node):
node.walkabout(visitor)
return visitor.astext()
def get_metadata(document):
"""Return the dict containing document metadata"""
output = {}
for docinfo in document.traverse(docutils.nodes.docinfo):
for element in docinfo.children:
if element.tagname == 'field': # custom fields (e.g. summary)
name_elem, body_elem = element.children
name = name_elem.astext()
value = render_node_to_html(document, body_elem)
else: # standard fields (e.g. address)
name = element.tagname
value = element.astext()
output[name] = _process_metadata(name, value)
return output
class RstReader(Reader):
enabled = bool(docutils)
extension = "rst"
def _parse_metadata(self, document):
return get_metadata(document)
"""Return the dict containing document metadata"""
output = {}
for docinfo in document.traverse(docutils.nodes.docinfo):
for element in docinfo.children:
if element.tagname == 'field': # custom fields (e.g. summary)
name_elem, body_elem = element.children
name = name_elem.astext()
if name == 'summary':
value = render_node_to_html(document, body_elem)
else:
value = body_elem.astext()
else: # standard fields (e.g. address)
name = element.tagname
value = element.astext()
name = name.lower()
output[name] = self.process_metadata(name, value)
return output
def _get_publisher(self, filename):
extra_params = {'initial_header_level': '2'}
pub = docutils.core.Publisher(destination_class=docutils.io.StringOutput)
pub = docutils.core.Publisher(
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
pub.process_programmatic_settings(None, extra_params, None)
pub.set_source(source_path=filename)
@ -99,17 +108,18 @@ class RstReader(Reader):
class MarkdownReader(Reader):
enabled = bool(Markdown)
extension = "md"
extensions = ['codehilite', 'extra']
def read(self, filename):
"""Parse content and metadata of markdown files"""
text = open(filename)
md = Markdown(extensions = ['meta', 'codehilite'])
md = Markdown(extensions=set(self.extensions + ['meta']))
content = md.convert(text)
metadata = {}
for name, value in md.Meta.items():
name = name.lower()
metadata[name] = _process_metadata(name, value[0])
metadata[name] = self.process_metadata(name, value[0])
return content, metadata
@ -119,27 +129,42 @@ class HtmlReader(Reader):
def read(self, filename):
"""Parse content and metadata of (x)HTML files"""
content = open(filename)
metadata = {'title':'unnamed'}
for i in self._re.findall(content):
key = i.split(':')[0][5:].strip()
value = i.split(':')[-1][:-3].strip()
name = key.lower()
metadata[name] = _process_metadata(name, value)
return content, metadata
with open(filename) as content:
metadata = {'title': 'unnamed'}
for i in self._re.findall(content):
key = i.split(':')[0][5:].strip()
value = i.split(':')[-1][:-3].strip()
name = key.lower()
metadata[name] = self.process_metadata(name, value)
return content, metadata
_EXTENSIONS = dict((cls.extension, cls) for cls in Reader.__subclasses__())
def read_file(filename, fmt=None):
def read_file(filename, fmt=None, settings=None):
"""Return a reader object using the given format."""
if not fmt:
fmt = filename.split('.')[-1]
if fmt not in _EXTENSIONS.keys():
if fmt not in _EXTENSIONS:
raise TypeError('Pelican does not know how to parse %s' % filename)
reader = _EXTENSIONS[fmt]()
reader = _EXTENSIONS[fmt](settings)
settings_key = '%s_EXTENSIONS' % fmt.upper()
if settings and settings_key in settings:
reader.extensions = settings[settings_key]
if not reader.enabled:
raise ValueError("Missing dependencies for %s" % fmt)
return reader.read(filename)
content, metadata = reader.read(filename)
# eventually filter the content with typogrify if asked so
if settings and settings['TYPOGRIFY']:
from typogrify import Typogrify
content = Typogrify.typogrify(content)
return content, metadata

View file

@ -35,3 +35,4 @@ class Pygments(Directive):
return [nodes.raw('', parsed, format='html')]
directives.register_directive('code-block', Pygments)
directives.register_directive('sourcecode', Pygments)

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
from os.path import isabs
import locale
from pelican import log
@ -7,24 +8,43 @@ from pelican import log
DEFAULT_THEME = os.sep.join([os.path.dirname(os.path.abspath(__file__)),
"themes/notmyidea"])
_DEFAULT_CONFIG = {'PATH': None,
'ARTICLE_DIR': '',
'ARTICLE_EXCLUDES': ('pages',),
'PAGE_DIR': 'pages',
'PAGE_EXCLUDES': (),
'THEME': DEFAULT_THEME,
'OUTPUT_PATH': 'output/',
'MARKUP': ('rst', 'md'),
'STATIC_PATHS': ['images',],
'THEME_STATIC_PATHS': ['static',],
'STATIC_PATHS': ['images', ],
'THEME_STATIC_PATHS': ['static', ],
'FEED': 'feeds/all.atom.xml',
'CATEGORY_FEED': 'feeds/%s.atom.xml',
'TRANSLATION_FEED': 'feeds/all-%s.atom.xml',
'FEED_MAX_ITEMS': '',
'SITENAME': 'A Pelican Blog',
'DISPLAY_PAGES_ON_MENU': True,
'PDF_GENERATOR': False,
'DEFAULT_CATEGORY': 'misc',
'FALLBACK_ON_FS_DATE': True,
'WITH_FUTURE_DATES': True,
'CSS_FILE': 'main.css',
'REVERSE_ARCHIVE_ORDER': False,
'REVERSE_CATEGORY_ORDER': False,
'DELETE_OUTPUT_DIRECTORY': False,
'CLEAN_URLS': False, # use /blah/ instead /blah.html in urls
'ARTICLE_URL': '{slug}.html',
'ARTICLE_SAVE_AS': '{slug}.html',
'ARTICLE_LANG_URL': '{slug}-{lang}.html',
'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html',
'PAGE_URL': 'pages/{slug}.html',
'PAGE_SAVE_AS': 'pages/{slug}.html',
'PAGE_LANG_URL': 'pages/{slug}-{lang}.html',
'PAGE_LANG_SAVE_AS': 'pages/{slug}-{lang}.html',
'CATEGORY_URL': 'category/{name}.html',
'CATEGORY_SAVE_AS': 'category/{name}.html',
'TAG_URL': 'tag/{slug}.html',
'TAG_SAVE_AS': 'tag/{slug}.html',
'AUTHOR_URL': u'author/{slug}.html',
'AUTHOR_SAVE_AS': u'author/{slug}.html',
'RELATIVE_URLS': True,
'DEFAULT_LANG': 'en',
'TAG_CLOUD_STEPS': 4,
@ -35,17 +55,19 @@ _DEFAULT_CONFIG = {'PATH': None,
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
'DATE_FORMATS': {},
'JINJA_EXTENSIONS': [],
'LOCALE': '', # default to user locale
'WITH_PAGINATION': False,
'DEFAULT_PAGINATION': 5,
'LOCALE': '', # default to user locale
'DEFAULT_PAGINATION': False,
'DEFAULT_ORPHANS': 0,
'DEFAULT_METADATA': (),
'FILES_TO_COPY': (),
'DEFAULT_STATUS': 'published',
'ARTICLE_PERMALINK_STRUCTURE': '',
'TYPOGRIFY': False,
'PLUGINS': [],
}
}
def read_settings(filename):
def read_settings(filename=None):
"""Load a Python file into a dictionary.
"""
context = _DEFAULT_CONFIG.copy()
@ -56,6 +78,14 @@ def read_settings(filename):
if key.isupper():
context[key] = tempdict[key]
# Make the paths relative to the settings file
for path in ['PATH', 'OUTPUT_PATH']:
if path in context:
if context[path] is not None and not isabs(context[path]):
context[path] = os.path.abspath(os.path.normpath(
os.path.join(os.path.dirname(filename), context[path]))
)
# if locales is not a list, make it one
locales = context['LOCALE']
@ -69,17 +99,17 @@ def read_settings(filename):
for locale_ in locales:
try:
locale.setlocale(locale.LC_ALL, locale_)
break # break if it is successfull
break # break if it is successfull
except locale.Error:
pass
else:
log.warn("LOCALE option doesn't contain a correct value")
# Make the paths relative to the settings file
for path in ['PATH', 'OUTPUT_PATH']:
if path in context:
if context[path] is not None and not os.path.isabs(context[path]):
context[path] = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(filename), context[path])))
if not 'TIMEZONE' in context:
log.warn("No timezone information specified in the settings. Assuming"
" your timezone is UTC for feed generation. Check "
"http://docs.notmyidea.org/alexis/pelican/settings.html#timezone "
"for more information")
# set the locale
return context

View file

@ -1,52 +0,0 @@
from unittest2 import TestCase
from pelican.contents import Page
from pelican.settings import _DEFAULT_CONFIG
class TestPage(TestCase):
def test_use_args(self):
# creating a page with arguments passed to the connstructor should use
# them to initialise object's attributes
metadata = {'foo': 'bar', 'foobar': 'baz'}
page = Page('content', metadata=metadata)
for key, value in metadata.items():
self.assertTrue(hasattr(page, key))
self.assertEqual(value, getattr(page, key))
self.assertEqual(page.content, "content")
def test_mandatory_properties(self):
# if the title is not set, must throw an exception
page = Page('content')
with self.assertRaises(NameError) as cm:
page.check_properties()
page = Page('content', metadata={'title': 'foobar'})
page.check_properties()
def test_slug(self):
# if a title is given, it should be used to generate the slug
page = Page('content', {'title': 'foobar is foo'})
self.assertEqual(page.slug, 'foobar-is-foo')
def test_defaultlang(self):
# if no lang is given, default to the default one
page = Page('content')
self.assertEqual(page.lang, _DEFAULT_CONFIG['DEFAULT_LANG'])
# it is possible to specify the lang in the metadata infos
page = Page('content', {'lang': 'fr'})
self.assertEqual(page.lang, 'fr')
def test_save_as(self):
# if a lang is not the default lang, save_as should be set accordingly
page = Page('content', {'title': 'foobar', 'lang': 'fr'}) #default lang is en
self.assertEqual(page.save_as, "foobar-fr.html")
# otherwise, if a title is defined, save_as should be set
page = Page('content', {'title': 'foobar'})
page.save_as = 'foobar.html'
# if no title is given, there is no save_as
page = Page('content')
self.assertFalse(hasattr(page, 'save_as'))

View file

@ -1,27 +0,0 @@
# coding: utf-8
import unittest2
import os
import datetime
from pelican import readers
CUR_DIR = os.path.dirname(__file__)
CONTENT_PATH = os.path.join(CUR_DIR, '..', '..', 'samples', 'content')
def _filename(*args):
return os.path.join(CONTENT_PATH, *args)
class RstReaderTest(unittest2.TestCase):
def test_metadata(self):
reader = readers.RstReader()
content, metadata = reader.read(_filename('super_article.rst'))
expected = {
'category': 'yeah',
'author': u'Alexis Métaireau',
'title': 'This is a super article !',
'summary': 'Multi-line metadata should be supported\nas well as <strong>inline markup</strong>.',
'date': datetime.datetime(2010, 12, 2, 10, 14),
'tags': ['foo', 'bar', 'foobar'],
}
self.assertDictEqual(metadata, expected)

View file

@ -1,34 +0,0 @@
from unittest2 import TestCase
import os
from pelican.settings import read_settings, _DEFAULT_CONFIG
SETTINGS = os.sep.join([os.path.dirname(os.path.abspath(__file__)),
"../../samples/pelican.conf.py"])
class SettingsTest(TestCase):
def test_read_settings(self):
# providing a file, it should read it, replace the default values and append
# new values to the settings, if any
settings = read_settings(SETTINGS)
# overwrite existing settings
self.assertEqual(settings.get('SITENAME'), u"Alexis' log")
# add new settings
self.assertEqual(settings.get('SITEURL'), 'http://blog.notmyidea.org')
# keep default settings if not defined
self.assertEqual(settings.get('DEFAULT_CATEGORY'),
_DEFAULT_CONFIG['DEFAULT_CATEGORY'])
# do not copy keys not in caps
self.assertNotIn('foobar', settings)
def test_empty_read_settings(self):
# providing no file should return the default values
settings = read_settings(None)
self.assertDictEqual(settings, _DEFAULT_CONFIG)

View file

@ -10,12 +10,13 @@
/* Imports */
@import url("reset.css");
@import url("pygment.css");
@import url("typogrify.css");
@import url(http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
/***** Global *****/
/* Body */
body {
background: #F5F4EF url('../images/bg.png');
background: #F5F4EF;
color: #000305;
font-size: 87.5%; /* Base font size: 14px */
font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;

View file

@ -0,0 +1,3 @@
.caps {font-size:.92em;}
.amp {color:#666; font-size:1.05em;font-family:"Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua",serif; font-style:italic;}
.dquo {margin-left:-.38em;}

View file

@ -6,7 +6,7 @@
<dl>
{% for article in dates %}
<dt>{{ article.locale_date }}</dt>
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
{% endfor %}
</dl>
</section>

View file

@ -3,7 +3,7 @@
{% block content %}
<section id="content" class="body">
<article>
<header> <h1 class="entry-title"><a href="{{ article.url }}"
<header> <h1 class="entry-title"><a href="{{ pagename }}"
rel="bookmark" title="Permalink to {{ article.title }}">{{ article.title
}}</a></h1> {% include 'twitter.html' %} </header>
<div class="entry-content">

View file

@ -5,10 +5,10 @@
{% if article.author %}
<address class="vcard author">
By <a class="url fn" href="#">{{ article.author }}</a>
By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a>
</address>
{% endif %}
<p>In <a href="{{ SITEURL }}/category/{{ article.category }}.html">{{ article.category }}</a>. {% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a>{% endif %}</p>
<p>In <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>. {% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a>{% endif %}</p>
{% include 'taglist.html' %}
{% include 'translations.html' %}
</footer><!-- /.post-info -->

View file

@ -0,0 +1,2 @@
{% extends "index.html" %}
{% block title %}{{ SITENAME }} - {{ author }}{% endblock %}

View file

@ -9,8 +9,6 @@
<link href="{{ SITEURL }}/{{ FEED_RSS }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
{% endif %}
{% include 'skribit_tab_script.html' %}
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
@ -33,7 +31,7 @@
{% endfor %}
{% if DISPLAY_PAGES_ON_MENU %}
{% for page in PAGES %}
<li><a href="{{ SITEURL }}/pages/{{ page.url }}">{{ page.title }}</a></li>
<li><a href="{{ SITEURL }}/{{ page.url }}">{{ page.title }}</a></li>
{% endfor %}
{% endif %}
{% for cat, null in categories %}
@ -54,7 +52,6 @@
</ul>
</div><!-- /.blogroll -->
{% endif %}
{% include 'skribit_widget_script.html' %}
{% if SOCIAL %}
<div class="social">
<h2>social</h2>
@ -74,7 +71,7 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="http://alexis.notmyidea.org/pelican/">pelican</a>, which takes great advantages of <a href="http://python.org">python</a>.
Proudly powered by <a href="http://pelican.notmyidea.org/">Pelican</a>, which takes great advantage of <a href="http://python.org">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="http://coding.smashingmagazine.com/2009/08/04/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>

View file

@ -2,7 +2,7 @@
{% block content %}
<ul>
{% for category, articles in categories %}
<li>{{ category }}</li>
<li><a href="{{ category.url }}">{{ category }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1 +1 @@
{% if DISQUS_SITENAME %}<p>There are <a href="{{ SITEURL }}/{{ article.slug }}.html#disqus_thread">comments</a>.</p>{% endif %}
{% if DISQUS_SITENAME %}<p>There are <a href="{{ SITEURL }}/{{ article.url }}#disqus_thread">comments</a>.</p>{% endif %}

View file

@ -53,7 +53,7 @@
<section id="content" class="body">
<h2>Pages</h2>
{% for page in PAGES %}
<li><a href="{{ SITEURL }}/pages/{{ page.url }}">{{ page.title }}</a></li>
<li><a href="{{ SITEURL }}/{{ page.url }}">{{ page.title }}</a></li>
{% endfor %}
</section>
{% endif %}

View file

@ -1,3 +1,4 @@
{% if DEFAULT_PAGINATION %}
<p class="paginator">
{% if articles_page.has_previous() %}
{% if articles_page.previous_page_number() == 1 %}
@ -11,3 +12,4 @@
<a href="{{ SITEURL }}/{{ page_name }}{{ articles_page.next_page_number() }}.html">&raquo;</a>
{% endif %}
</p>
{% endif %}

View file

@ -1,14 +0,0 @@
{% if SKRIBIT_TYPE and SKRIBIT_TYPE == 'TAB' and SKRIBIT_TAB_SITENAME %}
<link rel="stylesheet" type="text/css" media="screen" charset="utf-8" href="http://assets.skribit.com/stylesheets/SkribitSuggest.css"></link>
<style type="text/css" media="print" charset="utf-8">a#sk_tab{display:none !important;}</style>
<script src="http://assets.skribit.com/javascripts/SkribitSuggest.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
var skribit_settings = {};
skribit_settings.placement = "{{ SKRIBIT_TAB_PLACEMENT or 'right' }}";
skribit_settings.color = "{{ SKRIBIT_TAB_COLOR or '#333333' }}";
skribit_settings.text_color = "{{ SKRIBIT_TAB_TEXT_COLOR or 'white' }}";
skribit_settings.distance_vert = "{{ SKRIBIT_TAB_VERT or '20%' }}";
skribit_settings.distance_horiz = "{{ SKRIBIT_TAB_HORIZ or '' }}";
SkribitSuggest.suggest('http://skribit.com/lightbox/{{ SKRIBIT_TAB_SITENAME }}', skribit_settings);
</script>
{% endif %}

View file

@ -1,8 +0,0 @@
{% if SKRIBIT_TYPE == 'WIDGET' and SKRIBIT_WIDGET_ID %}
<div id="writeSkribitHere"></div>
<script src="http://assets.skribit.com/javascripts/SkribitWidget.js?renderTo=writeSkribitHere&amp;blog={{ SKRIBIT_WIDGET_ID }}&amp;cnt=5"></script>
<noscript>Sorry, but the
<a href="http://skribit.com" title="Skribit - Cure Writer's Block">Skribit</a> widget only works on browsers with JavaScript support.
<a href="http://skribit.com/blogs/think-different-think-open" title="Skribit Suggestions for Think Different, Think Open">View suggestions for this blog here.</a>
</noscript>
{% endif %}

View file

@ -1,2 +1,2 @@
{% if article.tags %}<p>tags: {% for tag in article.tags %}<a href="{{ SITEURL }}/tag/{{ tag }}.html">{{ tag }}</a>{% endfor %}</p>{% endif %}
{% if article.tags %}<p>tags: {% for tag in article.tags %}<a href="{{ SITEURL }}/{{ tag.url }}">{{ tag }}</a>{% endfor %}</p>{% endif %}
{% if PDF_PROCESSOR %}<p><a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a></p>{% endif %}

View file

@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block content %}
<h1>Archives for {{ SITENAME }}</h2>
<h1>Archives for {{ SITENAME }}</h1>
<dl>
{% for article in dates %}
<dt>{{ article.locale_date }}</dt>
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
{% endfor %}
</dl>
{% endblock %}

View file

@ -1,14 +1,14 @@
{% extends "base.html" %}
{% block content %}
<section id="content" class="body">
<header> <h2 class="entry-title"><a href="{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title}}">{{ article.title }}</a></h2> </header>
<header> <h2 class="entry-title"><a href="{{ pagename }}" rel="bookmark" title="Permalink to {{ article.title}}">{{ article.title }}</a></h2> </header>
<footer class="post-info">
<abbr class="published" title="{{ article.date.isoformat() }}">
{{ article.locale_date }}
</abbr>
{% if article.author %}
<address class="vcard author">
By <a class="url fn" href="#">{{ article.author }}</a>
By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a>
</address>
{% endif %}
</footer><!-- /.post-info -->

View file

@ -0,0 +1,7 @@
{% extends "index.html" %}
{% block title %}{{ SITENAME }} - Articles by {{ author }}{% endblock %}
{% block content_title %}
<h2>Articles by {{ author }}</h2>
{% endblock %}

View file

@ -1,25 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ DEFAULT_LANG }}">
<head>
<title>{% block title %}{{ SITENAME }}{%endblock%}</title>
{% block head %}
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<meta charset="utf-8" />
{% endblock head %}
</head>
<body id="index" class="home">
<header id="banner" class="body">
<h1><a href="{{ SITEURL }}">{{ SITENAME }} <strong>{{ SITESUBTITLE }}</strong></a></h1>
</header><!-- /#banner -->
{% if categories %}<ul>
{% for category, articles in categories %}
<li><a href="{{ SITEURL }}/category/{{category}}.html">{{ category }}</a></li>
<nav id="menu"><ul>
{% for title, link in MENUITEMS %}
<li><a href="{{ link }}">{{ title }}</a></li>
{% endfor %}
</ul> {% endif %}
{% if DISPLAY_PAGES_ON_MENU %}
{% for p in PAGES %}
<li{% if p == page %} class="active"{% endif %}><a href="{{ SITEURL }}/{{ p.url }}">{{ p.title }}</a></li>
{% endfor %}
{% else %}
{% for cat, null in categories %}
<li{% if cat == category %} class="active"{% endif %}><a href="{{ SITEURL }}/category/{{ cat }}.html">{{ cat }}</a></li>
{% endfor %}
{% endif %}
</ul></nav><!-- /#menu -->
{% block content %}
{% endblock %}
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="http://docs.notmyidea.org/alexis/pelican/">pelican</a>,
and obviously <a href="http://python.org">python</a>!
Proudly powered by <a href="http://pelican.notmyidea.org/">Pelican</a>,
which takes great advantage of <a href="http://python.org">Python</a>.
</address><!-- /#about -->
</footer><!-- /#contentinfo -->
</body>

View file

@ -2,7 +2,7 @@
{% block content %}
<ul>
{% for category, articles in categories %}
<li>{{ category }}</li>
<li><a href="{{ SITEURL }}/{{ category.url }}">{{ category }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -8,10 +8,10 @@
<ol id="post-list">
{% for article in articles_page.object_list %}
<li><article class="hentry">
<header> <h2 class="entry-title"><a href="{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title}}">{{ article.title }}</a></h2> </header>
<header> <h2 class="entry-title"><a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title}}">{{ article.title }}</a></h2> </header>
<footer class="post-info">
<abbr class="published" title="{{ article.date.isoformat() }}"> {{ article.locale_date }} </abbr>
{% if article.author %}<address class="vcard author">By <a class="url fn" href="#">{{ article.author }}</a></address>{% endif %}
{% if article.author %}<address class="vcard author">By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a></address>{% endif %}
</footer><!-- /.post-info -->
<div class="entry-content"> {{ article.summary }} </div><!-- /.entry-content -->
</article></li>

View file

295
pelican/tools/pelican_import.py Executable file
View file

@ -0,0 +1,295 @@
#!/usr/bin/env python
import argparse
import os
import subprocess
import sys
import time
from codecs import open
from pelican.utils import slugify
def wp2fields(xml):
"""Opens a wordpress XML file, and yield pelican fields"""
from BeautifulSoup import BeautifulStoneSoup
xmlfile = open(xml, encoding='utf-8').read()
soup = BeautifulStoneSoup(xmlfile)
items = soup.rss.channel.findAll('item')
for item in items:
if item.fetch('wp:status')[0].contents[0] == "publish":
title = item.title.contents[0]
content = item.fetch('content:encoded')[0].contents[0]
filename = item.fetch('wp:post_name')[0].contents[0]
raw_date = item.fetch('wp:post_date')[0].contents[0]
date_object = time.strptime(raw_date, "%Y-%m-%d %H:%M:%S")
date = time.strftime("%Y-%m-%d %H:%M", date_object)
author = item.fetch('dc:creator')[0].contents[0].title()
categories = [cat.contents[0] for cat in item.fetch(domain='category')]
# caturl = [cat['nicename'] for cat in item.fetch(domain='category')]
tags = [tag.contents[0].title() for tag in item.fetch(domain='tag', nicename=None)]
yield (title, content, filename, date, author, categories, tags, "html")
def dc2fields(file):
"""Opens a Dotclear export file, and yield pelican fields"""
from BeautifulSoup import BeautifulStoneSoup
in_cat = False
in_post = False
category_list = {}
posts = []
with open(file, 'r', encoding='utf-8') as f:
for line in f:
# remove final \n
line = line[:-1]
if line.startswith('[category'):
in_cat = True
elif line.startswith('[post'):
in_post = True
elif in_cat:
fields = line.split('","')
if not line:
in_cat = False
else:
# remove 1st and last ""
fields[0] = fields[0][1:]
# fields[-1] = fields[-1][:-1]
category_list[fields[0]]=fields[2]
elif in_post:
if not line:
in_post = False
break
else:
posts.append(line)
print("%i posts read." % len(posts))
for post in posts:
fields = post.split('","')
# post_id = fields[0][1:]
# blog_id = fields[1]
# user_id = fields[2]
cat_id = fields[3]
# post_dt = fields[4]
# post_tz = fields[5]
post_creadt = fields[6]
# post_upddt = fields[7]
# post_password = fields[8]
# post_type = fields[9]
post_format = fields[10]
# post_url = fields[11]
# post_lang = fields[12]
post_title = fields[13]
post_excerpt = fields[14]
post_excerpt_xhtml = fields[15]
post_content = fields[16]
post_content_xhtml = fields[17]
# post_notes = fields[18]
# post_words = fields[19]
# post_status = fields[20]
# post_selected = fields[21]
# post_position = fields[22]
# post_open_comment = fields[23]
# post_open_tb = fields[24]
# nb_comment = fields[25]
# nb_trackback = fields[26]
post_meta = fields[27]
# redirect_url = fields[28][:-1]
# remove seconds
post_creadt = ':'.join(post_creadt.split(':')[0:2])
author = ""
categories = []
tags = []
if cat_id:
categories = [category_list[id].strip() for id in cat_id.split(',')]
# Get tags related to a post
tag = post_meta.replace('{', '').replace('}', '').replace('a:1:s:3:\\"tag\\";a:', '').replace('a:0:', '')
if len(tag) > 1:
if int(tag[:1]) == 1:
newtag = tag.split('"')[1]
tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
else:
i=1
j=1
while(i <= int(tag[:1])):
newtag = tag.split('"')[j].replace('\\','')
tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
i=i+1
if j < int(tag[:1])*2:
j=j+2
"""
dotclear2 does not use markdown by default unless you use the markdown plugin
Ref: http://plugins.dotaddict.org/dc2/details/formatting-markdown
"""
if post_format == "markdown":
content = post_excerpt + post_content
else:
content = post_excerpt_xhtml + post_content_xhtml
content = content.replace('\\n', '')
post_format = "html"
yield (post_title, content, slugify(post_title), post_creadt, author, categories, tags, post_format)
def feed2fields(file):
"""Read a feed and yield pelican fields"""
import feedparser
d = feedparser.parse(file)
for entry in d.entries:
date = (time.strftime("%Y-%m-%d %H:%M", entry.updated_parsed)
if hasattr(entry, "updated_parsed") else None)
author = entry.author if hasattr(entry, "author") else None
tags = [e['term'] for e in entry.tags] if hasattr(entry, "tags") else None
slug = slugify(entry.title)
yield (entry.title, entry.description, slug, date, author, [], tags, "html")
def build_header(title, date, author, categories, tags):
"""Build a header from a list of fields"""
header = '%s\n%s\n' % (title, '#' * len(title))
if date:
header += ':date: %s\n' % date
if categories:
header += ':category: %s\n' % ', '.join(categories)
if tags:
header += ':tags: %s\n' % ', '.join(tags)
header += '\n'
return header
def build_markdown_header(title, date, author, categories, tags):
"""Build a header from a list of fields"""
header = 'Title: %s\n' % title
if date:
header += 'Date: %s\n' % date
if categories:
header += 'Category: %s\n' % ', '.join(categories)
if tags:
header += 'Tags: %s\n' % ', '.join(tags)
header += '\n'
return header
def fields2pelican(fields, out_markup, output_path, dircat=False):
for title, content, filename, date, author, categories, tags, in_markup in fields:
if (in_markup == "markdown") or (out_markup == "markdown") :
ext = '.md'
header = build_markdown_header(title, date, author, categories, tags)
else:
out_markup = "rst"
ext = '.rst'
header = build_header(title, date, author, categories, tags)
filename = os.path.basename(filename)
# option to put files in directories with categories names
if dircat and (len(categories) == 1):
catname = slugify(categories[0])
out_filename = os.path.join(output_path, catname, filename+ext)
if not os.path.isdir(os.path.join(output_path, catname)):
os.mkdir(os.path.join(output_path, catname))
else:
out_filename = os.path.join(output_path, filename+ext)
print(out_filename)
if in_markup == "html":
html_filename = os.path.join(output_path, filename+'.html')
with open(html_filename, 'w', encoding='utf-8') as fp:
# Replace simple newlines with <br />+newline so that the HTML file
# represents the original post more accurately
content = content.replace("\n", "<br />\n")
fp.write(content)
cmd = 'pandoc --normalize --reference-links --from=html --to={0} -o "{1}" "{2}"'.format(
out_markup, out_filename, html_filename)
try:
rc = subprocess.call(cmd, shell=True)
if rc < 0:
print("Child was terminated by signal %d" % -rc)
exit()
elif rc > 0:
print("Please, check your Pandoc installation.")
exit()
except OSError, e:
print("Pandoc execution failed: %s" % e)
exit()
os.remove(html_filename)
with open(out_filename, 'r', encoding='utf-8') as fs:
content = fs.read()
if out_markup == "markdown":
# In markdown, to insert a <br />, end a line with two or more spaces & then a end-of-line
content = content.replace("\\\n ", " \n")
content = content.replace("\\\n", " \n")
with open(out_filename, 'w', encoding='utf-8') as fs:
fs.write(header + content)
def main():
parser = argparse.ArgumentParser(
description="Transform feed, Wordpress or Dotclear files to rst files."
"Be sure to have pandoc installed",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(dest='input', help='The input file to read')
parser.add_argument('--wpfile', action='store_true', dest='wpfile',
help='Wordpress XML export')
parser.add_argument('--dotclear', action='store_true', dest='dotclear',
help='Dotclear export')
parser.add_argument('--feed', action='store_true', dest='feed',
help='Feed to parse')
parser.add_argument('-o', '--output', dest='output', default='output',
help='Output path')
parser.add_argument('-m', '--markup', dest='markup', default='rst',
help='Output markup format (supports rst & markdown)')
parser.add_argument('--dir-cat', action='store_true', dest='dircat',
help='Put files in directories with categories name')
args = parser.parse_args()
input_type = None
if args.wpfile:
input_type = 'wordpress'
elif args.dotclear:
input_type = 'dotclear'
elif args.feed:
input_type = 'feed'
else:
print("You must provide either --wpfile, --dotclear or --feed options")
exit()
if not os.path.exists(args.output):
try:
os.mkdir(args.output)
except OSError:
print("Unable to create the output folder: " + args.output)
exit()
if input_type == 'wordpress':
fields = wp2fields(args.input)
elif input_type == 'dotclear':
fields = dc2fields(args.input)
elif input_type == 'feed':
fields = feed2fields(args.input)
fields2pelican(fields, args.markup, args.output, dircat=args.dircat or False)

View file

@ -0,0 +1,264 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
import os
import string
import argparse
from pelican import __version__
TEMPLATES = {
'Makefile' : '''
PELICAN=$pelican
PELICANOPTS=$pelicanopts
BASEDIR=$$(PWD)
INPUTDIR=$$(BASEDIR)/src
OUTPUTDIR=$$(BASEDIR)/output
CONFFILE=$$(BASEDIR)/pelican.conf.py
FTP_HOST=$ftp_host
FTP_USER=$ftp_user
FTP_TARGET_DIR=$ftp_target_dir
SSH_HOST=$ssh_host
SSH_USER=$ssh_user
SSH_TARGET_DIR=$ssh_target_dir
DROPBOX_DIR=$dropbox_dir
help:
\t@echo 'Makefile for a pelican Web site '
\t@echo ' '
\t@echo 'Usage: '
\t@echo ' make html (re)generate the web site '
\t@echo ' make clean remove the generated files '
\t@echo ' ftp_upload upload the web site using FTP '
\t@echo ' ssh_upload upload the web site using SSH '
\t@echo ' dropbox_upload upload the web site using Dropbox '
\t@echo ' '
html: clean $$(OUTPUTDIR)/index.html
\t@echo 'Done'
$$(OUTPUTDIR)/%.html:
\t$$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS)
clean:
\trm -fr $$(OUTPUTDIR)
\tmkdir $$(OUTPUTDIR)
dropbox_upload: $$(OUTPUTDIR)/index.html
\tcp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR)
ssh_upload: $$(OUTPUTDIR)/index.html
\tscp -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR)
ftp_upload: $$(OUTPUTDIR)/index.html
\tlftp ftp://$$(FTP_USER)@$$(FTP_HOST) -e "mirror -R $$(OUTPUT_DIR)/* $$(FTP_TARGET_DIR) ; quit"
github: $$(OUTPUTDIR)/index.html
\tghp-import $$(OUTPUTDIR)
\tgit push origin gh-pages
.PHONY: html help clean ftp_upload ssh_upload dropbox_upload github
''',
'pelican.conf.py': '''#!/usr/bin/env python
# -*- coding: utf-8 -*- #
AUTHOR = u"$author"
SITENAME = u"$sitename"
SITEURL = '/'
TIMEZONE = 'Europe/Paris'
DEFAULT_LANG='$lang'
# Blogroll
LINKS = (
('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'),
('Python.org', 'http://python.org'),
('Jinja2', 'http://jinja.pocoo.org'),
('You can modify those links in your config file', '#')
)
# Social widget
SOCIAL = (
('You can add links in your config file', '#'),
)
DEFAULT_PAGINATION = $default_pagination
'''
}
CONF = {
'pelican' : 'pelican',
'pelicanopts' : '',
'basedir': '.',
'ftp_host': 'localhost',
'ftp_user': 'anonymous',
'ftp_target_dir': '/',
'ssh_host': 'locahost',
'ssh_user': 'root',
'ssh_target_dir': '/var/www',
'dropbox_dir' : '~/Dropbox/Public/',
'default_pagination' : 10,
'lang': 'en'
}
def ask(question, answer=str, default=None, l=None):
if answer == str:
r = ''
while True:
if default:
r = raw_input('> {0} [{1}] '.format(question, default))
else:
r = raw_input('> {0} '.format(question, default))
r = r.strip()
if len(r) <= 0:
if default:
r = default
break
else:
print('You must enter something')
else:
if l and len(r) != l:
print('You must enter a {0} letters long string'.format(l))
else:
break
return r
elif answer == bool:
r = None
while True:
if default is True:
r = raw_input('> {0} (Y/n) '.format(question))
elif default is False:
r = raw_input('> {0} (y/N) '.format(question))
else:
r = raw_input('> {0} (y/n) '.format(question))
r = r.strip().lower()
if r in ('y', 'yes'):
r = True
break
elif r in ('n', 'no'):
r = False
break
elif not r:
r = default
break
else:
print("You must answer `yes' or `no'")
return r
elif answer == int:
r = None
while True:
if default:
r = raw_input('> {0} [{1}] '.format(question, default))
else:
r = raw_input('> {0} '.format(question))
r = r.strip()
if not r:
r = default
break
try:
r = int(r)
break
except:
print('You must enter an integer')
return r
else:
raise NotImplemented('Arguent `answer` must be str, bool or integer')
def main():
parser = argparse.ArgumentParser(
description="A kickstarter for pelican",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-p', '--path', default=".",
help="The path to generate the blog into")
parser.add_argument('-t', '--title', metavar="title",
help='Set the title of the website')
parser.add_argument('-a', '--author', metavar="author",
help='Set the author name of the website')
parser.add_argument('-l', '--lang', metavar="lang",
help='Set the default lang of the website')
args = parser.parse_args()
print('''Welcome to pelican-quickstart v{v}.
This script will help you creating a new Pelican based website.
Please answer the following questions so this script can generate the files needed by Pelican.
'''.format(v=__version__))
CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new Web site ?', answer=str, default=args.path))
CONF['sitename'] = ask('How will you call your Web site ?', answer=str, default=args.title)
CONF['author'] = ask('Who will be the author of this Web site ?', answer=str, default=args.author)
CONF['lang'] = ask('What will be the default language of this Web site ?', str, args.lang or CONF['lang'], 2)
CONF['with_pagination'] = ask('Do you want to enable article pagination ?', bool, bool(CONF['default_pagination']))
if CONF['with_pagination']:
CONF['default_pagination'] = ask('So how many articles per page do you want ?', int, CONF['default_pagination'])
else:
CONF['default_pagination'] = False
mkfile = ask('Do you want to generate a Makefile to easily manage your website ?', bool, True)
if mkfile:
if ask('Do you want to upload your website using FTP ?', answer=bool, default=False):
CONF['ftp_host'] = ask('What is the hostname of your FTP server ?', str, CONF['ftp_host'])
CONF['ftp_user'] = ask('What is your username on this server ?', str, CONF['ftp_user'])
CONF['ftp_traget_dir'] = ask('Where do you want to put your website on this server ?', str, CONF['ftp_target_dir'])
if ask('Do you want to upload your website using SSH ?', answer=bool, default=False):
CONF['ssh_host'] = ask('What is the hostname of your SSH server ?', str, CONF['ssh_host'])
CONF['ssh_user'] = ask('What is your username on this server ?', str, CONF['ssh_user'])
CONF['ssh_traget_dir'] = ask('Where do you want to put your website on this server ?', str, CONF['ssh_target_dir'])
if ask('Do you want to upload your website using Dropbox ?', answer=bool, default=False):
CONF['dropbox_dir'] = ask('Where is your Dropbox directory ?', str, CONF['dropbox_dir'])
try:
os.makedirs(os.path.join(CONF['basedir'], 'src'))
except OSError, e:
print('Error: {0}'.format(e))
try:
os.makedirs(os.path.join(CONF['basedir'], 'output'))
except OSError, e:
print('Error: {0}'.format(e))
conf = string.Template(TEMPLATES['pelican.conf.py'])
try:
with open(os.path.join(CONF['basedir'], 'pelican.conf.py'), 'w') as fd:
fd.write(conf.safe_substitute(CONF))
fd.close()
except OSError, e:
print('Error: {0}'.format(e))
if mkfile:
Makefile = string.Template(TEMPLATES['Makefile'])
try:
with open(os.path.join(CONF['basedir'], 'Makefile'), 'w') as fd:
fd.write(Makefile.safe_substitute(CONF))
fd.close()
except OSError, e:
print('Error: {0}'.format(e))
print('Done. Your new project is available at %s' % CONF['basedir'])

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, sys, shutil
import argparse
import os
import shutil
import sys
try:
import pelican
@ -38,11 +40,11 @@ def main():
excl= parser.add_mutually_exclusive_group()
excl.add_argument('-l', '--list', dest='action', action="store_const", const='list',
help="Show the themes already installed and exit")
help="Show the themes already installed and exit")
excl.add_argument('-p', '--path', dest='action', action="store_const", const='path',
help="Show the themes path and exit")
help="Show the themes path and exit")
excl.add_argument('-V', '--version', action='version', version='pelican-themes v{0}'.format(__version__),
help='Print the version of this script')
help='Print the version of this script')
parser.add_argument('-i', '--install', dest='to_install', nargs='+', metavar="theme path",
@ -52,16 +54,16 @@ def main():
parser.add_argument('-s', '--symlink', dest='to_symlink', nargs='+', metavar="theme path",
help="Same as `--install', but create a symbolic link instead of copying the theme. Useful for theme development")
parser.add_argument('-c', '--clean', dest='clean', action="store_true",
help="Remove the broken symbolic links of the theme path")
help="Remove the broken symbolic links of the theme path")
parser.add_argument('-v', '--verbose', dest='verbose', action="store_true",
help="Verbose output")
help="Verbose output")
args = parser.parse_args()
if args.action:
if args.action is 'list':
list_themes(args.verbose)
@ -93,7 +95,7 @@ def main():
if args.clean:
if args.verbose:
print('Cleaning the themes directory...')
clean(v=args.verbose)
else:
print('No argument given... exiting.')
@ -142,7 +144,7 @@ def remove(theme_name, v=False):
print('Removing directory `' + target + "'")
shutil.rmtree(target)
elif os.path.exists(target):
err(target + ' : not a valid theme')
err(target + ' : not a valid theme')
else:
err(target + ' : no such file or directory')
@ -210,6 +212,3 @@ def clean(v=False):
c+=1
print("\nRemoved {0} broken links".format(c))
if __name__ == '__main__':
main()

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import re
import os
import pytz
import re
import shutil
from datetime import datetime
from codecs import open as _open
from datetime import datetime
from itertools import groupby
from operator import attrgetter
from pelican.log import warning, info
@ -12,10 +14,14 @@ from pelican.log import warning, info
def get_date(string):
"""Return a datetime object from a string.
If no format matches the given date, raise a ValuEerror
If no format matches the given date, raise a ValueError.
"""
formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M', '%Y-%m-%d', '%Y/%m/%d',
'%d/%m/%Y', '%d.%m.%Y', '%d.%m.%Y %H:%M']
string = re.sub(' +', ' ', string)
formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M',
'%Y-%m-%d', '%Y/%m/%d',
'%d-%m-%Y', '%Y-%d-%m', # Weird ones
'%d/%m/%Y', '%d.%m.%Y',
'%d.%m.%Y %H:%M', '%Y-%m-%d %H:%M:%S']
for date_format in formats:
try:
return datetime.strptime(string, date_format)
@ -42,6 +48,7 @@ def slugify(value):
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
return re.sub('[-\s]+', '-', value)
def copy(path, source, destination, destination_path=None, overwrite=False):
"""Copy path from origin to destination.
@ -51,15 +58,15 @@ def copy(path, source, destination, destination_path=None, overwrite=False):
:param source: the source dir
:param destination: the destination dir
:param destination_path: the destination path (optional)
:param overwrite: wether to overwrite the destination if already exists or not
:param overwrite: whether to overwrite the destination if already exists
or not
"""
if not destination_path:
destination_path = path
source_ = os.path.abspath(os.path.expanduser(os.path.join(source, path)))
destination_ = os.path.abspath(
os.path.expanduser(os.path.join(destination, destination_path)))
os.path.expanduser(os.path.join(destination, destination_path)))
if os.path.isdir(source_):
try:
@ -75,6 +82,7 @@ def copy(path, source, destination, destination_path=None, overwrite=False):
shutil.copy(source_, destination_)
info('copying %s to %s' % (source_, destination_))
def clean_output_dir(path):
"""Remove all the files from the output directory"""
@ -102,7 +110,8 @@ def truncate_html_words(s, num, end_text='...'):
length = int(num)
if length <= 0:
return u''
html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area',
'hr', 'input')
# Set up regular expressions
re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
@ -140,8 +149,9 @@ def truncate_html_words(s, num, end_text='...'):
except ValueError:
pass
else:
# SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
open_tags = open_tags[i+1:]
# SGML: An end tag closes, back to the matching start tag,
# all unclosed intervening start tags with omitted end tags
open_tags = open_tags[i + 1:]
else:
# Add it to the start of the open tags list
open_tags.insert(0, tagname)
@ -159,13 +169,11 @@ def truncate_html_words(s, num, end_text='...'):
def process_translations(content_list):
""" Finds all translation and returns
tuple with two lists (index, translations).
Index list includes items in default language
or items which have no variant in default language.
""" Finds all translation and returns tuple with two lists (index,
translations). Index list includes items in default language or items
which have no variant in default language.
Also, for each content_list item, it
sets attribute 'translations'
Also, for each content_list item, it sets attribute 'translations'
"""
content_list.sort(key=attrgetter('slug'))
grouped_by_slugs = groupby(content_list, attrgetter('slug'))
@ -175,10 +183,7 @@ def process_translations(content_list):
for slug, items in grouped_by_slugs:
items = list(items)
# find items with default language
default_lang_items = filter(
attrgetter('in_default_lang'),
items
)
default_lang_items = filter(attrgetter('in_default_lang'), items)
len_ = len(default_lang_items)
if len_ > 1:
warning(u'there are %s variants of "%s"' % (len_, slug))
@ -188,7 +193,7 @@ def process_translations(content_list):
default_lang_items = items[:1]
if not slug:
warning('empty slug for %r' %( default_lang_items[0].filename,))
warning('empty slug for %r' % (default_lang_items[0].filename,))
index.extend(default_lang_items)
translations.extend(filter(
lambda x: x not in default_lang_items,
@ -205,9 +210,6 @@ LAST_MTIME = 0
def files_changed(path, extensions):
"""Return True if the files have changed since the last check"""
def with_extension(f):
return any(f.endswith(ext) for ext in extensions)
def file_times(path):
"""Return the last time files have been modified"""
for root, dirs, files in os.walk(path):
@ -222,3 +224,15 @@ def files_changed(path, extensions):
LAST_MTIME = mtime
return True
return False
def set_date_tzinfo(d, tz_name=None):
""" Date without tzinfo shoudbe utc.
This function set the right tz to date that aren't utc and don't have
tzinfo.
"""
if tz_name is not None:
tz = pytz.timezone(tz_name)
return tz.localize(d)
else:
return d

View file

@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
import os
import re
from codecs import open
from functools import partial
import locale
import re
from feedgenerator import Atom1Feed, Rss201rev2Feed
from pelican.utils import get_relative_path
from pelican.paginator import Paginator
from pelican.log import *
from pelican.log import info
from pelican.utils import get_relative_path, set_date_tzinfo
class Writer(object):
@ -28,22 +28,23 @@ class Writer(object):
description=context.get('SITESUBTITLE', ''))
return feed
def _add_item_to_the_feed(self, feed, item):
feed.add_item(
title=item.title,
link='%s/%s' % (self.site_url, item.url),
unique_id='%s/%s' % (self.site_url, item.url),
description=item.content,
categories=item.tags if hasattr(item, 'tags') else None,
author_name=getattr(item, 'author', 'John Doe'),
pubdate=item.date)
pubdate=set_date_tzinfo(item.date,
self.settings.get('TIMEZONE', None)))
def write_feed(self, elements, context, filename=None, feed_type='atom'):
"""Generate a feed with the list of articles provided
Return the feed. If no output_path or filename is specified, just return
the feed object.
Return the feed. If no output_path or filename is specified, just
return the feed object.
:param elements: the articles to put on the feed.
:param context: the context to get the feed metadata.
@ -54,12 +55,15 @@ class Writer(object):
locale.setlocale(locale.LC_ALL, 'C')
try:
self.site_url = context.get('SITEURL', get_relative_path(filename))
self.feed_url= '%s/%s' % (self.site_url, filename)
self.feed_url = '%s/%s' % (self.site_url, filename)
feed = self._create_new_feed(feed_type, context)
for item in elements:
self._add_item_to_the_feed(feed, item)
max_items = len(elements)
if self.settings['FEED_MAX_ITEMS']:
max_items = min(self.settings['FEED_MAX_ITEMS'], max_items)
for i in xrange(max_items):
self._add_item_to_the_feed(feed, elements[i])
if filename:
complete_path = os.path.join(self.output_path, filename)
@ -85,7 +89,7 @@ class Writer(object):
:param context: dict to pass to the templates.
:param relative_urls: use relative urls or absolutes ones
:param paginated: dict of article list to paginate - must have the
same length (same list in different orders)
same length (same list in different orders)
:param **kwargs: additional variables to pass to the templates
"""
@ -111,7 +115,8 @@ class Writer(object):
localcontext['SITEURL'] = get_relative_path(name)
localcontext.update(kwargs)
self.update_context_contents(name, localcontext)
if relative_urls:
self.update_context_contents(name, localcontext)
# check paginated
paginated = paginated or {}
@ -121,12 +126,12 @@ class Writer(object):
for key in paginated.iterkeys():
object_list = paginated[key]
if self.settings.get('WITH_PAGINATION'):
if self.settings.get('DEFAULT_PAGINATION'):
paginators[key] = Paginator(object_list,
self.settings.get('DEFAULT_PAGINATION'),
self.settings.get('DEFAULT_ORPHANS'))
else:
paginators[key] = Paginator(object_list, len(object_list), 0)
paginators[key] = Paginator(object_list, len(object_list))
# generated pages, and write
for page_num in range(paginators.values()[0].num_pages):
@ -134,16 +139,17 @@ class Writer(object):
paginated_name = name
for key in paginators.iterkeys():
paginator = paginators[key]
page = paginator.page(page_num+1)
paginated_localcontext.update({'%s_paginator' % key: paginator,
'%s_page' % key: page})
page = paginator.page(page_num + 1)
paginated_localcontext.update(
{'%s_paginator' % key: paginator,
'%s_page' % key: page})
if page_num > 0:
ext = '.' + paginated_name.rsplit('.')[-1]
paginated_name = paginated_name.replace(ext,
'%s%s' % (page_num + 1, ext))
'%s%s' % (page_num + 1, ext))
_write_file(template, paginated_localcontext, self.output_path,
paginated_name)
paginated_name)
else:
# no pagination
_write_file(template, localcontext, self.output_path, name)
@ -154,8 +160,8 @@ class Writer(object):
relative paths.
:param name: name of the file to output.
:param context: dict that will be passed to the templates, which need to
be updated.
:param context: dict that will be passed to the templates, which need
to be updated.
"""
def _update_content(name, input):
"""Change all the relatives paths of the input content to relatives
@ -166,25 +172,27 @@ class Writer(object):
"""
content = input._content
hrefs = re.compile(r'<\s*[^\>]*href\s*=(^!#)\s*(["\'])(.*?)\1')
srcs = re.compile(r'<\s*[^\>]*src\s*=\s*(["\'])(.*?)\1')
hrefs = re.compile(r"""
(?P<markup><\s*[^\>]* # match tag with src and href attr
(?:href|src)\s*=\s*
)
(?P<quote>["\']) # require value to be quoted
(?![#?]) # don't match fragment or query URLs
(?![a-z]+:) # don't match protocol URLS
(?P<path>.*?) # the url value
\2""", re.X)
matches = hrefs.findall(content)
matches.extend(srcs.findall(content))
relative_paths = []
for found in matches:
found = found[1]
if found not in relative_paths:
relative_paths.append(found)
def replacer(m):
relative_path = m.group('path')
dest_path = os.path.normpath(
os.sep.join((get_relative_path(name), "static",
relative_path)))
for relative_path in relative_paths:
if not ":" in relative_path: # we don't want to rewrite protocols
dest_path = os.sep.join((get_relative_path(name), "static",
relative_path))
content = content.replace(relative_path, dest_path)
return m.group('markup') + m.group('quote') + dest_path \
+ m.group('quote')
return hrefs.sub(replacer, content)
return content
if context is None:
return
if hasattr(context, 'values'):
@ -203,4 +211,4 @@ class Writer(object):
if relative_path not in paths:
paths.append(relative_path)
setattr(item, "_get_content",
partial(_update_content, name, item))
partial(_update_content, name, item))

View file

@ -0,0 +1,9 @@
Deuxième article
################
:tags: foo, bar, baz
:date: 2012-02-29
:lang: fr
:slug: second-article
Ceci est un article, en français.

View file

@ -0,0 +1,9 @@
Second article
##############
:tags: foo, bar, baz
:date: 2012-02-29
:lang: en
:slug: second-article
This is some article, in english

View file

@ -26,7 +26,7 @@ And here comes the cool stuff_.
:width: 600 px
:alt: alternate text
.. code-block:: python
::
>>> from ipdb import set_trace
>>> set_trace()

View file

@ -2,13 +2,14 @@
AUTHOR = u'Alexis Métaireau'
SITENAME = u"Alexis' log"
SITEURL = 'http://blog.notmyidea.org'
TIMEZONE = "Europe/Paris"
GITHUB_URL = 'http://github.com/ametaireau/'
DISQUS_SITENAME = "blog-notmyidea"
PDF_GENERATOR = False
REVERSE_CATEGORY_ORDER = True
LOCALE = ""
DEFAULT_PAGINATION = 2
DEFAULT_PAGINATION = 4
FEED_RSS = 'feeds/all.rss.xml'
CATEGORY_FEED_RSS = 'feeds/%s.rss.xml'
@ -36,3 +37,4 @@ FILES_TO_COPY = (('extra/robots.txt', 'robots.txt'),)
# foobar will not be used, because it's not in caps. All configuration keys
# have to be in caps
foobar = "barbaz"

View file

@ -1,25 +1,34 @@
#!/usr/bin/env python
from setuptools import setup
import sys
VERSION = "2.7.2" # find a better way to do so.
requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz', 'blinker']
requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'blinker']
if sys.version_info < (2,7):
try:
import argparse
except ImportError:
requires.append('argparse')
entry_points = {
'console_scripts': [
'pelican = pelican:main',
'pelican-import = pelican.tools.pelican_import:main',
'pelican-quickstart = pelican.tools.pelican_quickstart:main',
'pelican-themes = pelican.tools.pelican_themes:main'
]
}
setup(
name = "pelican",
version = VERSION,
url = 'http://alexis.notmyidea.org/pelican/',
version = "3.0",
url = 'http://pelican.notmyidea.org/',
author = 'Alexis Metaireau',
author_email = 'alexis@notmyidea.org',
description = "A tool to generate a static blog, with restructured text (or markdown) input files.",
description = "A tool to generate a static blog from reStructuredText or Markdown input files.",
long_description=open('README.rst').read(),
packages = ['pelican', 'pelican.plugins'],
packages = ['pelican', 'pelican.tools', 'pelican.plugins'],
include_package_data = True,
install_requires = requires,
scripts = ['bin/pelican', 'tools/pelican-themes'],
entry_points = entry_points,
classifiers = ['Development Status :: 5 - Production/Stable',
'Environment :: Console',
'License :: OSI Approved :: GNU Affero General Public License v3',

0
tests/__init__.py Normal file
View file

View file

@ -0,0 +1,4 @@
Article title
#############
This is some content. With some stuff to "typogrify".

View file

@ -0,0 +1,12 @@
This is a super article !
#########################
:tags: foo, bar, foobar
:date: 2010-12-02 10:14
:category: yeah
:author: Alexis Métaireau
:summary:
Multi-line metadata should be supported
as well as **inline markup**.
:custom_field: http://notmyidea.org

View file

@ -0,0 +1,6 @@
This is a super article !
#########################
:Category: Yeah

39
tests/default_conf.py Normal file
View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
AUTHOR = u'Alexis Métaireau'
SITENAME = u"Alexis' log"
SITEURL = 'http://blog.notmyidea.org'
TIMEZONE = 'UTC'
GITHUB_URL = 'http://github.com/ametaireau/'
DISQUS_SITENAME = "blog-notmyidea"
PDF_GENERATOR = False
REVERSE_CATEGORY_ORDER = True
LOCALE = ""
DEFAULT_PAGINATION = 2
FEED_RSS = 'feeds/all.rss.xml'
CATEGORY_FEED_RSS = 'feeds/%s.rss.xml'
LINKS = (('Biologeek', 'http://biologeek.org'),
('Filyb', "http://filyb.info/"),
('Libert-fr', "http://www.libert-fr.com"),
('N1k0', "http://prendreuncafe.com/blog/"),
(u'Tarek Ziadé', "http://ziade.org/blog"),
('Zubin Mithra', "http://zubin71.wordpress.com/"),)
SOCIAL = (('twitter', 'http://twitter.com/ametaireau'),
('lastfm', 'http://lastfm.com/user/akounet'),
('github', 'http://github.com/ametaireau'),)
# global metadata to all the contents
DEFAULT_METADATA = (('yeah', 'it is'),)
# static paths will be copied under the same name
STATIC_PATHS = ["pictures",]
# A list of files to copy from the source to the destination
FILES_TO_COPY = (('extra/robots.txt', 'robots.txt'),)
# foobar will not be used, because it's not in caps. All configuration keys
# have to be in caps
foobar = "barbaz"

26
tests/support.py Normal file
View file

@ -0,0 +1,26 @@
from contextlib import contextmanager
from tempfile import mkdtemp
from shutil import rmtree
from pelican.contents import Article
@contextmanager
def temporary_folder():
"""creates a temporary folder, return it and delete it afterwards.
This allows to do something like this in tests:
>>> with temporary_folder() as d:
# do whatever you want
"""
tempdir = mkdtemp()
yield tempdir
rmtree(tempdir)
def get_article(title, slug, content, lang, extra_metadata=None):
metadata = {'slug': slug, 'title': title, 'lang': lang}
if extra_metadata is not None:
metadata.update(extra_metadata)
return Article(content, metadata=metadata)

129
tests/test_contents.py Normal file
View file

@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
try:
from unittest2 import TestCase, skip
except ImportError, e:
from unittest import TestCase, skip # NOQA
from pelican.contents import Page
from pelican.settings import _DEFAULT_CONFIG
from jinja2.utils import generate_lorem_ipsum
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
class TestPage(TestCase):
def setUp(self):
super(TestPage, self).setUp()
self.page_kwargs = {
'content': TEST_CONTENT,
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
'author': 'Blogger',
},
}
def test_use_args(self):
"""Creating a page with arguments passed to the constructor should use
them to initialise object's attributes.
"""
metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', }
page = Page(TEST_CONTENT, metadata=metadata)
for key, value in metadata.items():
self.assertTrue(hasattr(page, key))
self.assertEqual(value, getattr(page, key))
self.assertEqual(page.content, TEST_CONTENT)
def test_mandatory_properties(self):
"""If the title is not set, must throw an exception."""
self.assertRaises(AttributeError, Page, 'content')
page = Page(**self.page_kwargs)
page.check_properties()
def test_summary_from_metadata(self):
"""If a :summary: metadata is given, it should be used."""
page = Page(**self.page_kwargs)
self.assertEqual(page.summary, TEST_SUMMARY)
def test_slug(self):
"""If a title is given, it should be used to generate the slug."""
page = Page(**self.page_kwargs)
self.assertEqual(page.slug, 'foo-bar')
def test_defaultlang(self):
"""If no lang is given, default to the default one."""
page = Page(**self.page_kwargs)
self.assertEqual(page.lang, _DEFAULT_CONFIG['DEFAULT_LANG'])
# it is possible to specify the lang in the metadata infos
self.page_kwargs['metadata'].update({'lang': 'fr', })
page = Page(**self.page_kwargs)
self.assertEqual(page.lang, 'fr')
def test_save_as(self):
"""If a lang is not the default lang, save_as should be set
accordingly.
"""
# if a title is defined, save_as should be set
page = Page(**self.page_kwargs)
self.assertEqual(page.save_as, "pages/foo-bar.html")
# if a language is defined, save_as should include it accordingly
self.page_kwargs['metadata'].update({'lang': 'fr', })
page = Page(**self.page_kwargs)
self.assertEqual(page.save_as, "pages/foo-bar-fr.html")
def test_datetime(self):
"""If DATETIME is set to a tuple, it should be used to override LOCALE
"""
from datetime import datetime
from sys import platform
dt = datetime(2015, 9, 13)
# make a deep copy of page_kawgs
page_kwargs = dict([(key, self.page_kwargs[key]) for key in
self.page_kwargs])
for key in page_kwargs:
if not isinstance(page_kwargs[key], dict):
break
page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey])
for subkey in page_kwargs[key]])
# set its date to dt
page_kwargs['metadata']['date'] = dt
page = Page(**page_kwargs)
self.assertEqual(page.locale_date,
unicode(dt.strftime(_DEFAULT_CONFIG['DEFAULT_DATE_FORMAT']),
'utf-8'))
page_kwargs['settings'] = dict([(x, _DEFAULT_CONFIG[x]) for x in
_DEFAULT_CONFIG])
# I doubt this can work on all platforms ...
if platform == "win32":
locale = 'jpn'
else:
locale = 'ja_JP.utf8'
page_kwargs['settings']['DATE_FORMATS'] = {'jp': (locale,
'%Y-%m-%d(%a)')}
page_kwargs['metadata']['lang'] = 'jp'
import locale as locale_module
try:
page = Page(**page_kwargs)
self.assertEqual(page.locale_date, u'2015-09-13(\u65e5)')
# above is unicode in Japanese: 2015-09-13(“ú)
except locale_module.Error:
# The constructor of ``Page`` will try to set the locale to
# ``ja_JP.utf8``. But this attempt will failed when there is no
# such locale in the system. You can see which locales there are
# in your system with ``locale -a`` command.
#
# Until we find some other method to test this functionality, we
# will simply skip this test.
skip("There is no locale %s in this system." % locale)

28
tests/test_generators.py Normal file
View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
try:
import unittest2 as unittest
except ImportError, e:
import unittest # NOQA
from pelican.generators import ArticlesGenerator
from pelican.settings import _DEFAULT_CONFIG
from mock import MagicMock
class TestArticlesGenerator(unittest.TestCase):
def test_generate_feeds(self):
generator = ArticlesGenerator(None, {'FEED': _DEFAULT_CONFIG['FEED']},
None, _DEFAULT_CONFIG['THEME'], None,
None)
writer = MagicMock()
generator.generate_feeds(writer)
writer.write_feed.assert_called_with([], None, 'feeds/all.atom.xml')
generator = ArticlesGenerator(None, {'FEED': None}, None,
_DEFAULT_CONFIG['THEME'], None, None)
writer = MagicMock()
generator.generate_feeds(writer)
self.assertFalse(writer.write_feed.called)

31
tests/test_pelican.py Normal file
View file

@ -0,0 +1,31 @@
import unittest
import os
from support import temporary_folder
from pelican import Pelican
from pelican.settings import read_settings
SAMPLES_PATH = os.path.abspath(os.sep.join(
(os.path.dirname(os.path.abspath(__file__)), "..", "samples")))
INPUT_PATH = os.path.join(SAMPLES_PATH, "content")
SAMPLE_CONFIG = os.path.join(SAMPLES_PATH, "pelican.conf.py")
class TestPelican(unittest.TestCase):
# general functional testing for pelican. Basically, this test case tries
# to run pelican in different situations and see how it behaves
def test_basic_generation_works(self):
# when running pelican without settings, it should pick up the default
# ones and generate the output without raising any exception / issuing
# any warning.
with temporary_folder() as temp_path:
pelican = Pelican(path=INPUT_PATH, output_path=temp_path)
pelican.run()
# the same thing with a specified set of settins should work
with temporary_folder() as temp_path:
pelican = Pelican(path=INPUT_PATH, output_path=temp_path,
settings=read_settings(SAMPLE_CONFIG))

65
tests/test_readers.py Normal file
View file

@ -0,0 +1,65 @@
# coding: utf-8
try:
import unittest2 as unittest
except ImportError, e:
import unittest
import datetime
import os
from pelican import readers
CUR_DIR = os.path.dirname(__file__)
CONTENT_PATH = os.path.join(CUR_DIR, 'content')
def _filename(*args):
return os.path.join(CONTENT_PATH, *args)
class RstReaderTest(unittest.TestCase):
def test_article_with_metadata(self):
reader = readers.RstReader({})
content, metadata = reader.read(_filename('article_with_metadata.rst'))
expected = {
'category': 'yeah',
'author': u'Alexis Métaireau',
'title': 'This is a super article !',
'summary': 'Multi-line metadata should be supported\nas well as'\
' <strong>inline markup</strong>.',
'date': datetime.datetime(2010, 12, 2, 10, 14),
'tags': ['foo', 'bar', 'foobar'],
'custom_field': 'http://notmyidea.org',
}
for key, value in expected.items():
self.assertEquals(value, metadata[key], key)
def test_article_metadata_key_lowercase(self):
"""Keys of metadata should be lowercase."""
reader = readers.RstReader({})
content, metadata = reader.read(_filename('article_with_uppercase_metadata.rst'))
self.assertIn('category', metadata, "Key should be lowercase.")
self.assertEquals('Yeah', metadata.get('category'), "Value keeps cases.")
def test_typogrify(self):
# if nothing is specified in the settings, the content should be
# unmodified
content, _ = readers.read_file(_filename('article.rst'))
expected = "<p>This is some content. With some stuff to "\
"&quot;typogrify&quot;.</p>\n"
self.assertEqual(content, expected)
try:
# otherwise, typogrify should be applied
content, _ = readers.read_file(_filename('article.rst'),
settings={'TYPOGRIFY': True})
expected = "<p>This is some content. With some stuff to&nbsp;"\
"&#8220;typogrify&#8221;.</p>\n"
self.assertEqual(content, expected)
except ImportError:
return unittest.skip('need the typogrify distribution')

37
tests/test_settings.py Normal file
View file

@ -0,0 +1,37 @@
try:
import unittest2
except ImportError, e:
import unittest as unittest2
from os.path import dirname, abspath, join
from pelican.settings import read_settings, _DEFAULT_CONFIG
class TestSettingsFromFile(unittest2.TestCase):
"""Providing a file, it should read it, replace the default values and
append new values to the settings, if any
"""
def setUp(self):
self.PATH = abspath(dirname(__file__))
default_conf = join(self.PATH, 'default_conf.py')
self.settings = read_settings(default_conf)
def test_overwrite_existing_settings(self):
self.assertEqual(self.settings.get('SITENAME'), u"Alexis' log")
self.assertEqual(self.settings.get('SITEURL'),
'http://blog.notmyidea.org')
def test_keep_default_settings(self):
"""keep default settings if not defined"""
self.assertEqual(self.settings.get('DEFAULT_CATEGORY'),
_DEFAULT_CONFIG['DEFAULT_CATEGORY'])
def test_dont_copy_small_keys(self):
"""do not copy keys not in caps."""
self.assertNotIn('foobar', self.settings)
def test_read_empty_settings(self):
"""providing no file should return the default values."""
settings = read_settings(None)
self.assertDictEqual(settings, _DEFAULT_CONFIG)

93
tests/test_utils.py Normal file
View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
try:
import unittest2 as unittest
except ImportError:
import unittest # NOQA
import os
import datetime
import time
from pelican import utils
from support import get_article
class TestUtils(unittest.TestCase):
def test_get_date(self):
# valid ones
date = datetime.datetime(year=2012, month=11, day=22)
date_hour = datetime.datetime(year=2012, month=11, day=22, hour=22,
minute=11)
date_hour_sec = datetime.datetime(year=2012, month=11, day=22, hour=22,
minute=11, second=10)
dates = {'2012-11-22': date,
'2012/11/22': date,
'2012-11-22 22:11': date_hour,
'2012/11/22 22:11': date_hour,
'22-11-2012': date,
'22/11/2012': date,
'22.11.2012': date,
'2012-22-11': date,
'22.11.2012 22:11': date_hour,
'2012-11-22 22:11:10': date_hour_sec}
for value, expected in dates.items():
self.assertEquals(utils.get_date(value), expected, value)
# invalid ones
invalid_dates = ('2010-110-12', 'yay')
for item in invalid_dates:
self.assertRaises(ValueError, utils.get_date, item)
def test_slugify(self):
samples = (('this is a test', 'this-is-a-test'),
('this is a test', 'this-is-a-test'),
(u'this → is ← a ↑ test', 'this-is-a-test'),
('this--is---a test', 'this-is-a-test'))
for value, expected in samples:
self.assertEquals(utils.slugify(value), expected)
def test_get_relative_path(self):
samples = (('/test/test', '../../.'),
('/test/test/', '../../../.'),
('/', '../.'))
for value, expected in samples:
self.assertEquals(utils.get_relative_path(value), expected)
def test_process_translations(self):
# create a bunch of articles
fr_article1 = get_article(lang='fr', slug='yay', title='Un titre',
content='en français')
en_article1 = get_article(lang='en', slug='yay', title='A title',
content='in english')
articles = [fr_article1, en_article1]
index, trans = utils.process_translations(articles)
self.assertIn(en_article1, index)
self.assertIn(fr_article1, trans)
self.assertNotIn(en_article1, trans)
self.assertNotIn(fr_article1, index)
def test_files_changed(self):
"Test if file changes are correctly detected"
path = os.path.join(os.path.dirname(__file__), 'content')
filename = os.path.join(path, 'article_with_metadata.rst')
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, True)
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, False)
t = time.time()
os.utime(filename, (t, t))
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, True)
self.assertAlmostEqual(utils.LAST_MTIME, t, places=2)

View file

@ -1,121 +0,0 @@
#! /usr/bin/env python
from pelican.utils import slugify
from codecs import open
import os
import argparse
import time
def wp2fields(xml):
"""Opens a wordpress XML file, and yield pelican fields"""
from BeautifulSoup import BeautifulStoneSoup
xmlfile = open(xml, encoding='utf-8').read()
soup = BeautifulStoneSoup(xmlfile)
items = soup.rss.channel.findAll('item')
for item in items:
if item.fetch('wp:status')[0].contents[0] == "publish":
title = item.title.contents[0]
content = item.fetch('content:encoded')[0].contents[0]
filename = item.fetch('wp:post_name')[0].contents[0]
raw_date = item.fetch('wp:post_date')[0].contents[0]
date_object = time.strptime(raw_date, "%Y-%m-%d %H:%M:%S")
date = time.strftime("%Y-%m-%d %H:%M", date_object)
author = item.fetch('dc:creator')[0].contents[0].title()
categories = [(cat['nicename'],cat.contents[0]) for cat in item.fetch(domain='category')]
tags = [tag.contents[0].title() for tag in item.fetch(domain='tag', nicename=None)]
yield (title, content, filename, date, author, categories, tags)
def feed2fields(file):
"""Read a feed and yield pelican fields"""
import feedparser
d = feedparser.parse(file)
for entry in d.entries:
date = (time.strftime("%Y-%m-%d %H:%M", entry.updated_parsed)
if hasattr(entry, "updated_parsed") else None)
author = entry.author if hasattr(entry, "author") else None
tags = [e['term'] for e in entry.tags] if hasattr(entry, "tags") else None
slug = slugify(entry.title)
yield (entry.title, entry.description, slug, date, author, [], tags)
def build_header(title, date, author, categories, tags):
"""Build a header from a list of fields"""
header = '%s\n%s\n' % (title, '#' * len(title))
if date:
header += ':date: %s\n' % date
if categories:
header += ':category: %s\n' % ', '.join(categories)
if tags:
header += ':tags: %s\n' % ', '.join(tags)
header += '\n'
return header
def fields2pelican(fields, output_path):
for title, content, filename, date, author, categories, tags in fields:
html_filename = os.path.join(output_path, filename+'.html')
if(len(categories) == 1):
rst_filename = os.path.join(output_path, categories[0][0], filename+'.rst')
if not os.path.isdir(os.path.join(output_path, categories[0][0])):
os.mkdir(os.path.join(output_path, categories[0][0]))
else:
rst_filename = os.path.join(output_path, filename+'.rst')
with open(html_filename, 'w', encoding='utf-8') as fp:
fp.write(content)
os.system('pandoc --from=html --to=rst -o %s %s' % (rst_filename,
html_filename))
os.remove(html_filename)
with open(rst_filename, 'r', encoding='utf-8') as fs:
content = fs.read()
with open(rst_filename, 'w', encoding='utf-8') as fs:
categories = [x[1] for x in categories]
header = build_header(title, date, author, categories, tags)
fs.write(header + content)
def main(input_type, input, output_path):
if input_type == 'wordpress':
fields = wp2fields(input)
elif input_type == 'feed':
fields = feed2fields(input)
fields2pelican(fields, output_path)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Transform even feed or XML files to rst files."
"Be sure to have pandoc installed")
parser.add_argument(dest='input', help='The input file to read')
parser.add_argument('--wpfile', action='store_true', dest='wpfile',
help='Wordpress XML export')
parser.add_argument('--feed', action='store_true', dest='feed',
help='feed to parse')
parser.add_argument('-o', '--output', dest='output', default='output',
help='Output path')
args = parser.parse_args()
input_type = None
if args.wpfile:
input_type = 'wordpress'
elif args.feed:
input_type = 'feed'
else:
print "you must provide either --wpfile or --feed options"
exit()
main(input_type, args.input, args.output)

13
tox.ini Normal file
View file

@ -0,0 +1,13 @@
[tox]
envlist = py26,py27
[testenv]
commands = nosetests -s tests
deps =
nose
Jinja2
Pygments
docutils
feedgenerator
unittest2
mock