mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
commit
97d8d0daa6
88 changed files with 2998 additions and 1305 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ docs/fr/_build
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
output
|
output
|
||||||
|
tags
|
||||||
|
.tox
|
||||||
|
|
|
||||||
13
.travis.yml
Normal file
13
.travis.yml
Normal 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
100
CHANGELOG
Normal 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
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
include *.rst
|
include *.rst
|
||||||
|
global-include *.py
|
||||||
recursive-include pelican *.html *.css *png
|
recursive-include pelican *.html *.css *png
|
||||||
include LICENSE
|
include LICENSE
|
||||||
|
|
|
||||||
52
README.rst
52
README.rst
|
|
@ -1,53 +1,59 @@
|
||||||
Pelican
|
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/>`_.
|
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
|
* Write your weblog entries directly with your editor of choice (vim!)
|
||||||
directly in `reStructuredText <http://docutils.sourceforge.net/rst.html>`_, or `Markdown <http://daringfireball.net/projects/markdown/>`_.
|
in `reStructuredText <http://docutils.sourceforge.net/rst.html>`_ or `Markdown <http://daringfireball.net/projects/markdown/>`_
|
||||||
* A simple cli-tool to (re)generate the weblog.
|
* Includes a simple CLI tool to (re)generate the weblog
|
||||||
* Easy to interface with DVCSes and web hooks
|
* 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
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Pelican currently supports:
|
Pelican currently supports:
|
||||||
|
|
||||||
* blog articles and pages
|
* Blog articles and pages
|
||||||
* comments, via an external service (disqus). Please notice that while
|
* Comments, via an external service (Disqus). (Please note that while
|
||||||
it's useful, it's an external service, and you'll not manage the
|
useful, Disqus is an external service, and thus the comment data will be
|
||||||
comments by yourself. It could potentially eat your data.
|
somewhat outside of your control and potentially subject to data loss.)
|
||||||
* theming support (themes are done using `jinja2 <http://jinjna.pocoo.org>`_)
|
* Theming support (themes are created using `jinja2 <http://jinja.pocoo.org/>`_)
|
||||||
* PDF generation of the articles/pages (optional).
|
* PDF generation of the articles/pages (optional)
|
||||||
* Translations
|
* Publication of articles in multiple languages
|
||||||
* Syntactic recognition
|
* 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
|
Have a look at `the documentation <http://pelican.notmyidea.org/en/latest/>`_ for
|
||||||
more informations.
|
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
|
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
|
If you feel hackish, have a look at the explanation of `Pelican's internals
|
||||||
<http://alexis.notmyidea.org/pelican/internals.html>`_.
|
<http://pelican.notmyidea.org/en/latest/internals.html>`_.
|
||||||
|
|
||||||
Feedback / Contact us
|
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 offer suggestions,
|
||||||
the repository, etc. That's open source, dude!
|
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
|
Contact me at "alexis at notmyidea dot org" for any request/feedback! You can
|
||||||
also join the team at `#pelican on irc.freenode.org
|
also join the team at `#pelican on irc.freenode.org
|
||||||
<irc://irc.freenode.net/pelican>`_
|
<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>`_)
|
<http://webchat.freenode.net/?channels=pelican&uio=d4>`_)
|
||||||
for quick feedback.
|
for quick feedback.
|
||||||
|
|
|
||||||
1
THANKS
1
THANKS
|
|
@ -16,3 +16,4 @@ bugs or giving ideas. Thanks to them !
|
||||||
- Marcus Fredriksson
|
- Marcus Fredriksson
|
||||||
- Günter Kolousek
|
- Günter Kolousek
|
||||||
- Simon Liedtke
|
- Simon Liedtke
|
||||||
|
- Manuel F. Viera
|
||||||
|
|
|
||||||
3
TODO
3
TODO
|
|
@ -1,8 +1,9 @@
|
||||||
* Add a way to support pictures (see how sphinx makes that)
|
* 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?)
|
* Make the program support UTF8-encoded files as input (and later: any encoding?)
|
||||||
* Add status support (draft, published, hidden)
|
* Add status support (draft, published, hidden)
|
||||||
* Add a serve + automatic generation behaviour.
|
* Add a serve + automatic generation behaviour.
|
||||||
* Recompile only the changed files, not all.
|
* Recompile only the changed files, not all.
|
||||||
* Add a way to make the coffee (or not)
|
* Add a way to make the coffee (or not)
|
||||||
* Add a sitemap generator.
|
* Add a sitemap generator.
|
||||||
|
* read templates from the templates folder per default
|
||||||
|
* add support of github via ghg import
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
from pelican import main
|
|
||||||
main()
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
Jinja2==2.5.5
|
Jinja2
|
||||||
Pygments==1.4
|
Pygments
|
||||||
docutils==0.7
|
docutils
|
||||||
feedgenerator==1.2.1
|
feedgenerator
|
||||||
unittest2
|
unittest2
|
||||||
|
pytz
|
||||||
|
mock
|
||||||
|
|
|
||||||
BIN
docs/_static/overall.png
vendored
Normal file
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
BIN
docs/_static/theme-basic.zip
vendored
Normal file
Binary file not shown.
BIN
docs/_static/uml.jpg
vendored
Normal file
BIN
docs/_static/uml.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
15
docs/conf.py
15
docs/conf.py
|
|
@ -1,6 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath('..'))
|
||||||
|
|
||||||
|
from pelican import __version__, __major__
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
extensions = ['sphinx.ext.autodoc',]
|
extensions = ['sphinx.ext.autodoc',]
|
||||||
|
|
@ -9,12 +13,11 @@ master_doc = 'index'
|
||||||
project = u'Pelican'
|
project = u'Pelican'
|
||||||
copyright = u'2010, Alexis Metaireau and contributors'
|
copyright = u'2010, Alexis Metaireau and contributors'
|
||||||
exclude_patterns = ['_build']
|
exclude_patterns = ['_build']
|
||||||
version = "2"
|
version = __version__
|
||||||
release = version
|
release = __major__
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
sys.path.append(os.path.abspath('_themes'))
|
|
||||||
html_theme_path = ['_themes']
|
html_theme_path = ['_themes']
|
||||||
html_theme = 'pelican'
|
html_theme = 'pelican'
|
||||||
|
|
||||||
|
|
@ -40,7 +43,7 @@ man_pages = [
|
||||||
('index', 'pelican', u'pelican documentation',
|
('index', 'pelican', u'pelican documentation',
|
||||||
[u'Alexis Métaireau'], 1),
|
[u'Alexis Métaireau'], 1),
|
||||||
('pelican-themes', 'pelican-themes', u'A theme manager for Pelican',
|
('pelican-themes', 'pelican-themes', u'A theme manager for Pelican',
|
||||||
[u'Mickaël Raybaud'], 'en.1'),
|
[u'Mickaël Raybaud'], 1),
|
||||||
('fr/pelican-themes', 'pelican-themes', u'Un gestionnaire de thèmes pour Pelican',
|
('themes', 'pelican-theming', u'How to create themes for Pelican',
|
||||||
[u'Mickaël Raybaud'], 'fr.1')
|
[u'The Pelican contributors'], 1)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,49 @@
|
||||||
How to contribute ?
|
How to contribute?
|
||||||
###################
|
###################
|
||||||
There are many ways to contribute to pelican. You can enhance the
|
There are many ways to contribute to Pelican. You can enhance the
|
||||||
documentation, add missing features, fix bugs or just report them.
|
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
|
You're free to set up your development environment any way you like. Here is a
|
||||||
using virtualenv and virtualenvwrapper. If you don't have them, you can install
|
way using virtualenv and virtualenvwrapper. If you don't have them, you can
|
||||||
them using::
|
install these packages via::
|
||||||
|
|
||||||
$ pip install virtualenvwrapper
|
$ pip install virtualenvwrapper
|
||||||
|
|
||||||
Virtual environments allow you to work on an installation of python which is
|
Virtual environments allow you to work on Python projects which are isolated
|
||||||
not the one installed on your system. Especially, it will install the different
|
from one another so you can use different packages (and package versions) with
|
||||||
projects under a different location.
|
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
|
$ pip install -r dev_requirements.txt
|
||||||
|
$ python setup.py develop
|
||||||
|
|
||||||
Running the test suite
|
Running the test suite
|
||||||
======================
|
======================
|
||||||
|
|
||||||
Each time you add a feature, there are two things to do regarding tests:
|
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
|
checking that the existing tests pass, and adding tests for your new feature
|
||||||
the feature you are working on or the bug you're fixing.
|
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::
|
"discover" feature of unittest2::
|
||||||
|
|
||||||
$ unit2 discover
|
$ 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/)
|
||||||
|
|
|
||||||
57
docs/faq.rst
57
docs/faq.rst
|
|
@ -1,38 +1,51 @@
|
||||||
Frequently Asked Questions (FAQ)
|
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.
|
No, it's not. Configuration files are just an easy way to configure Pelican.
|
||||||
For the basic operations, it's possible to specify options while invoking
|
For basic operations, it's possible to specify options while invoking Pelican
|
||||||
pelican with the command line (see `pelican --help` for more informations about
|
via the command line. See `pelican --help` for more information.
|
||||||
that)
|
|
||||||
|
|
||||||
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
|
Pygments adds some classes to the generated content. These classes are used by
|
||||||
will be done thanks to a css file. You can have a look to the one proposed by
|
themes to style code syntax highlighting via CSS. Specifically, you can
|
||||||
default `on the project website <http://pygments.org/demo/15101/>`_
|
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
|
There are several ways to help out. First, you can use Pelican and report any
|
||||||
idea or problem you have on `the bugtracker
|
suggestions or problems you might have on `the bugtracker
|
||||||
<http://github.com/ametaireau/pelican/issues>`_.
|
<http://github.com/ametaireau/pelican/issues>`_.
|
||||||
|
|
||||||
If you want to contribute, please have a look to `the git repository
|
If you want to contribute, please fork `the git repository
|
||||||
<https://github.com/ametaireau/pelican/>`_, fork it, add your changes and do
|
<https://github.com/ametaireau/pelican/>`_, make your changes, and issue
|
||||||
a pull request, I'll review them as soon as possible.
|
a pull request. I'll review your changes as soon as possible.
|
||||||
|
|
||||||
You can also contribute by creating themes, and making the documentation
|
You can also contribute by creating themes and improving the documentation.
|
||||||
better.
|
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,4 @@ Documentation
|
||||||
parametres_article
|
parametres_article
|
||||||
astuces
|
astuces
|
||||||
faq
|
faq
|
||||||
|
pelican-themes
|
||||||
|
|
|
||||||
|
|
@ -169,19 +169,3 @@ dates_pages La page actuelle d'articles, ordonnée par date
|
||||||
croissante.
|
croissante.
|
||||||
page_name 'tag/`nom du tag`'.
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -4,45 +4,44 @@ Getting started
|
||||||
Installing
|
Installing
|
||||||
==========
|
==========
|
||||||
|
|
||||||
You're ready? Let's go ! You can install pelican in a lot of different ways,
|
You're ready? Let's go! You can install Pelican via several different methods. The simplest is via `pip <http://pip.openplans.org/>`_::
|
||||||
the simpler one is via `pip <http://pip.openplans.org/>`_::
|
|
||||||
|
|
||||||
$ pip install pelican
|
$ pip install pelican
|
||||||
|
|
||||||
If you have the sources, you can install pelican using the distutils command
|
If you have the project source, you can install Pelican using the distutils
|
||||||
install. I recommend to do so in a virtualenv::
|
method. I recommend doing so in a virtualenv::
|
||||||
|
|
||||||
$ virtualenv .
|
$ virtualenv pelican_venv
|
||||||
$ source bin/activate
|
$ source bin/activate
|
||||||
$ python setup.py install
|
$ python setup.py install
|
||||||
|
|
||||||
Dependencies
|
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.
|
* feedgenerator, to generate the Atom feeds
|
||||||
* jinja2, for templating support.
|
* 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:
|
Optionally:
|
||||||
|
|
||||||
* docutils, for reST support
|
* pygments, for syntax highlighting
|
||||||
* pygments, to have syntactic colorization with resT input
|
* docutils, for supporting reStructuredText as an input format
|
||||||
* Markdown, for Markdown 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
|
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 you need to
|
file system (for instance, about the category of your articles), but some
|
||||||
provide by hand some of those informations in your files.
|
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)::
|
following syntax (give your file the `.rst` extension)::
|
||||||
|
|
||||||
My super title
|
My super title
|
||||||
|
|
@ -54,35 +53,46 @@ following syntax (give your file the `.rst` extension)::
|
||||||
:author: Alexis Metaireau
|
: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
|
Date: 2010-12-03
|
||||||
Title: My super title
|
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
|
Note that, aside from the title, none of this metadata is mandatory: if the date
|
||||||
rely on the mtime of your file, and the category can also be determined by the
|
is not specified, Pelican will rely on the file's "mtime" timestamp, and the
|
||||||
directory where the rst file is. For instance, the category of
|
category can be determined by the directory in which the file resides. For
|
||||||
`python/foobar/myfoobar.rst` is `foobar`.
|
example, a file located at `python/foobar/myfoobar.rst` will have a category of
|
||||||
|
`foobar`.
|
||||||
|
|
||||||
Generate your blog
|
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]
|
$ 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
|
The above command will use the default theme to produce a simple site. It's not
|
||||||
really sexy, as it's a simple HTML output (without any style).
|
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::
|
the options you can use::
|
||||||
|
|
||||||
$ pelican --help
|
$ 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
|
Pages
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
@ -95,34 +105,26 @@ the menu.
|
||||||
Importing an existing blog
|
Importing an existing blog
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
It is possible to import wordpress themes and RSS themes using a script which
|
It is possible to import your blog from Dotclear, WordPress, and RSS feeds using
|
||||||
is living in `tools`: importer.
|
a simple script. See :ref:`import`.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Translations
|
Translations
|
||||||
------------
|
------------
|
||||||
|
|
||||||
It is possible to translate articles. To do so, you need to add a `lang` meta
|
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
|
attribute to your articles/pages and set a `DEFAULT_LANG` setting (which is
|
||||||
default).
|
English [en] by default). With those settings in place, only articles with the
|
||||||
Then, only articles with this default language will be listed, and
|
default language will be listed, and each article will be accompanied by a list
|
||||||
each article will have a translation list.
|
of available translations for that article.
|
||||||
|
|
||||||
Pelican uses the "slug" of two articles to compare if they are translations of
|
Pelican uses the article's URL "slug" to determine if two or more articles are
|
||||||
each others. So it's possible to define (in restructured text) the slug
|
translations of one another. The slug can be set manually in the file's
|
||||||
directly.
|
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
|
Here is an example of two articles, one in English and the other in French.
|
||||||
french).
|
|
||||||
|
|
||||||
The english one::
|
The English article::
|
||||||
|
|
||||||
Foobar is not dead
|
Foobar is not dead
|
||||||
##################
|
##################
|
||||||
|
|
@ -130,9 +132,9 @@ The english one::
|
||||||
:slug: foobar-is-not-dead
|
:slug: foobar-is-not-dead
|
||||||
:lang: en
|
: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 !
|
Foobar n'est pas mort !
|
||||||
#######################
|
#######################
|
||||||
|
|
@ -142,34 +144,67 @@ And the french one::
|
||||||
|
|
||||||
Oui oui, foobar est toujours vivant !
|
Oui oui, foobar est toujours vivant !
|
||||||
|
|
||||||
Despite the text quality, you can see that only the slug is the same here.
|
Post content quality notwithstanding, you can see that only item in common
|
||||||
You're not forced to define the slug that way, and it's completely possible to
|
between the two articles is the slug, which is functioning here as an
|
||||||
have two translations with the same title (which defines the slug)
|
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
|
Pelican is able to provide colorized syntax highlighting for your code blocks.
|
||||||
right way your block codes. To do so, you have to use the following syntax::
|
To do so, you have to use the following convention for reStructuredText::
|
||||||
|
|
||||||
.. code-block:: identifier
|
.. code-block:: identifier
|
||||||
|
|
||||||
your code goes here
|
your code goes here
|
||||||
|
|
||||||
The identifier is one of the lexers available `here
|
For Markdown, format your code blocks thusly:
|
||||||
<http://pygments.org/docs/lexers/>`_.
|
|
||||||
|
::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
|
Autoreload
|
||||||
----------
|
----------
|
||||||
|
|
||||||
It's possible to tell pelican to watch for your modifications, instead of
|
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
|
manually launching it every time you want to see your changes. To enable this,
|
||||||
`--autoreload`.
|
run the `pelican` command with the `-r` or `--autoreload` options.
|
||||||
|
|
||||||
Publishing drafts
|
Publishing drafts
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
If you want to publish an article as a draft, for friends to review it for
|
If you want to publish an article as a draft (for friends to review before
|
||||||
instance, you can add a ``status: draft`` to its metadata, it will then be
|
publishing, for example), you can add a ``status: draft`` attribute to its
|
||||||
available under the ``drafts`` folder, and not be listed under the index page nor
|
metadata. That article will then be output to the ``drafts`` folder and not
|
||||||
any category page.
|
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
57
docs/importer.rst
Normal 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
|
||||||
|
|
@ -1,53 +1,58 @@
|
||||||
Pelican
|
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
|
* Write your weblog entries directly with your editor of choice (vim!) in
|
||||||
directly in restructured text, or markdown.
|
reStructuredText or Markdown
|
||||||
* A simple cli-tool to (re)generate the weblog.
|
* A simple CLI tool to (re)generate the weblog
|
||||||
* Easy to interface with DVCSes and web hooks
|
* 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
|
Features
|
||||||
========
|
========
|
||||||
|
|
||||||
Pelican currently supports:
|
Pelican currently supports:
|
||||||
|
|
||||||
* blog articles
|
* Blog articles and pages
|
||||||
* comments, via an external service (disqus). Please notice that while
|
* Comments, via an external service (Disqus). (Please note that while
|
||||||
it's useful, it's an external service, and you'll not manage the
|
useful, Disqus is an external service, and thus the comment data will be
|
||||||
comments by yourself. It could potentially eat your data.
|
somewhat outside of your control and potentially subject to data loss.)
|
||||||
* theming support (themes are done using `jinja2 <http://jinjna.pocoo.org>`_)
|
* Theming support (themes are created using `jinja2 <http://jinja.pocoo.org/>`_)
|
||||||
* PDF generation of the articles/pages (optional).
|
* 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" ?
|
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
|
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
|
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!
|
the repository, etc. That's open source, dude!
|
||||||
|
|
||||||
Contact me at "alexis at notmyidea dot org" for any request/feedback! You can
|
Contact me at "alexis at notmyidea dot org" for any request/feedback! You can
|
||||||
also join the team at `#pelican on irc.freenode.org
|
also join the team at `#pelican on irc.freenode.org
|
||||||
<irc://irc.freenode.net/pelican>`_
|
<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>`_)
|
<http://webchat.freenode.net/?channels=pelican&uio=d4>`_)
|
||||||
for quick feedback.
|
for quick feedback.
|
||||||
|
|
||||||
Documentation
|
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::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -55,8 +60,11 @@ A french version of the documentation is available at :doc:`fr/index`.
|
||||||
getting_started
|
getting_started
|
||||||
settings
|
settings
|
||||||
themes
|
themes
|
||||||
pelican-themes
|
|
||||||
plugins
|
plugins
|
||||||
internals
|
internals
|
||||||
|
pelican-themes
|
||||||
|
importer
|
||||||
faq
|
faq
|
||||||
|
tips
|
||||||
contribute
|
contribute
|
||||||
|
report
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,48 @@
|
||||||
Pelican internals
|
Pelican internals
|
||||||
#################
|
#################
|
||||||
|
|
||||||
This section describe how pelican is working internally. As you'll see, it's
|
This section describe how Pelican works internally. As you'll see, it's
|
||||||
quite simple, but a bit of documentation doesn't hurt :)
|
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
|
Overall structure
|
||||||
=================
|
=================
|
||||||
|
|
||||||
What `pelican` does, is taking a list of files, and processing them, to some
|
What `pelican` does is take a list of files and process them into some
|
||||||
sort of output. Usually, the files are restructured text and markdown files,
|
sort of output. Usually, the input files are reStructuredText and Markdown
|
||||||
and the output is a blog, but it can be anything you want.
|
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
|
* `writers` are responsible for writing files: .html files, RSS feeds, and so
|
||||||
files. It's writing .html files, RSS feeds and so on. Since those operations
|
on. Since those operations are commonly used, the object is created once and
|
||||||
are commonly used, the object is created once, and then passed to the
|
then passed to the generators.
|
||||||
generators.
|
|
||||||
|
|
||||||
* `readers` are used to read from various formats (Markdown, and Restructured
|
* `readers` are used to read from various formats (Markdown and
|
||||||
Text for now, but the system is extensible). Given a file, they return
|
reStructuredText for now, but the system is extensible). Given a file, they return
|
||||||
metadata (author, tags, category etc) and content (HTML formated)
|
metadata (author, tags, category, etc.) and content (HTML-formatted).
|
||||||
|
|
||||||
* `generators` generate the different outputs. For instance, pelican comes with
|
* `generators` generate the different outputs. For instance, Pelican comes with
|
||||||
`ArticlesGenerator` and `PageGenerator`, into others. Given
|
`ArticlesGenerator` and `PageGenerator`. Given a configuration, they can do
|
||||||
a configurations, they can do whatever they want. Most of the time it's
|
whatever they want. Most of the time, it's generating files from inputs.
|
||||||
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
|
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 ?
|
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 that have a `read`
|
Well, the only thing you have to do is to create a class with a `read`
|
||||||
method, that is returning an HTML content and some metadata.
|
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):
|
class MarkdownReader(Reader):
|
||||||
enabled = bool(Markdown)
|
enabled = bool(Markdown)
|
||||||
|
|
@ -58,31 +62,31 @@ Take a look to the Markdown reader::
|
||||||
metadata[name.lower()] = meta
|
metadata[name.lower()] = meta
|
||||||
return content, metadata
|
return content, metadata
|
||||||
|
|
||||||
Simple isn't it ?
|
Simple, isn't it?
|
||||||
|
|
||||||
If your new reader requires additional Python dependencies then you should wrap
|
If your new reader requires additional Python dependencies, then you should wrap
|
||||||
their `import` statements in `try...except`. Then inside the reader's class
|
their `import` statements in a `try...except` block. Then inside the reader's
|
||||||
set the `enabled` class attribute to mark import success or failure. This makes
|
class, set the `enabled` class attribute to mark import success or failure.
|
||||||
it possible for users to continue using their favourite markup method without
|
This makes it possible for users to continue using their favourite markup method
|
||||||
needing to install modules for all the additional formats they don't use.
|
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
|
Generators have two important methods. You're not forced to create
|
||||||
both, only the existing ones will be called.
|
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
|
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
|
context is shared between all generators, and will be passed to the
|
||||||
templates. For instance, the `PageGenerator` `generate_context` method find
|
templates. For instance, the `PageGenerator` `generate_context` method finds
|
||||||
all the pages, transform them into objects, and populate the context with
|
all the pages, transforms them into objects, and populates the context with
|
||||||
them. Be careful to *not* output anything using this context at this stage,
|
them. Be careful *not* to output anything using this context at this stage,
|
||||||
as it is likely to change by the effect of others generators.
|
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,
|
* `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
|
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 at the first
|
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
|
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.
|
the disk (using the writer method `write_file`) for each page encountered.
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,11 @@ With ``pelican-themes``, you can see the available themes by using the ``-l`` or
|
||||||
two-column@
|
two-column@
|
||||||
simple
|
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
|
.. 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
|
Removing themes
|
||||||
"""""""""""""""
|
"""""""""""""""
|
||||||
|
|
||||||
Pelican themes can also removes themes from the Pelican themes path.
|
The ``pelican-themes`` command can also remove 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 ``-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
|
.. 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
|
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:
|
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 \
|
--symlink ~/Dev/Python/pelican-themes/two-column \
|
||||||
--verbose
|
--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``
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,18 @@
|
||||||
Plugins
|
Plugins
|
||||||
#######
|
#######
|
||||||
|
|
||||||
Since version 2.8, pelican manages plugins. Plugins are a way to add feature to
|
Since version 3.0, pelican manages plugins. Plugins are a way to add features
|
||||||
pelican without having to directly hack pelican code.
|
to pelican without having to directly hack pelican code.
|
||||||
|
|
||||||
Pelican is shipped with a set of core plugins, but you can easily implement
|
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?
|
How to use plugins?
|
||||||
====================
|
====================
|
||||||
|
|
||||||
To load plugins, you have to specify a them in your settings file. You have two
|
To load plugins, you have to specify them in your settings file. You have two
|
||||||
ways to do so: by specifying strings with the path to the callables: ::
|
ways to do so.
|
||||||
|
Either by specifying strings with the path to the callables::
|
||||||
|
|
||||||
PLUGINS = ['pelican.plugins.gravatar',]
|
PLUGINS = ['pelican.plugins.gravatar',]
|
||||||
|
|
||||||
|
|
@ -76,28 +77,18 @@ Translation
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
Github Activity
|
Github Activity
|
||||||
_______________
|
---------------
|
||||||
|
|
||||||
This plugins introduces a new depencency, you have to install feedparser
|
This plugin makes use of the ``feedparser`` library that you'll need to
|
||||||
if you want to use it, these are some ways to do it::
|
install.
|
||||||
|
|
||||||
apt-get install python-feedparser # on debian based distributions like ubuntu
|
Set the GITHUB_ACTIVITY_FEED parameter to your github activity feed.
|
||||||
sudo easy_install feedparser
|
For example, my setting would look like::
|
||||||
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::
|
|
||||||
|
|
||||||
GITHUB_ACTIVITY_FEED = 'https://github.com/kpanic.atom'
|
GITHUB_ACTIVITY_FEED = 'https://github.com/kpanic.atom'
|
||||||
|
|
||||||
in your template just write a for in jinja2 syntax against the
|
On the templates side, you just have to iterate over the ``github_activity``
|
||||||
github_activity variable, like for example::
|
variable, as in the example::
|
||||||
|
|
||||||
{% if GITHUB_ACTIVITY_FEED %}
|
{% if GITHUB_ACTIVITY_FEED %}
|
||||||
<div class="social">
|
<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
|
``github_activity`` is a list of lists. The first element is the title
|
||||||
the second element is the raw html from github so you can include it directly
|
and the second element is the raw html from github.
|
||||||
in your (for example base.html) template and style it in a way that your prefer
|
|
||||||
using your css skills
|
|
||||||
|
|
|
||||||
122
docs/report.rst
Normal file
122
docs/report.rst
Normal 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.
|
||||||
|
|
||||||
|
I’ve 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 it’s 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
|
||||||
|
|
@ -6,110 +6,252 @@ the command line::
|
||||||
|
|
||||||
$ pelican -s path/to/your/settingsfile.py path
|
$ 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
|
example by looking at `/samples/pelican.conf.py
|
||||||
<https://github.com/ametaireau/pelican/raw/master/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.
|
processed.
|
||||||
|
|
||||||
The settings you define in the configuration file will be passed to the
|
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
|
Basic settings
|
||||||
==============
|
==============
|
||||||
|
|
||||||
================================================ =====================================================
|
================================================ =====================================================
|
||||||
Setting name (default value) what does it do?
|
Setting name (default value) What does it do?
|
||||||
================================================ =====================================================
|
================================================ =====================================================
|
||||||
`AUTHOR` Default author (put your name)
|
`AUTHOR` Default author (put your name)
|
||||||
`SITENAME` (``'A Pelican Blog'``) Your site name
|
|
||||||
`DATE_FORMATS` (``{}``) If you do manage multiple languages, you can
|
`DATE_FORMATS` (``{}``) If you do manage multiple languages, you can
|
||||||
set the date formatting here.
|
set the date formatting here. See "Date format and locales"
|
||||||
`DEFAULT_CATEGORY` (``'misc'``) The default category to fallback on.
|
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.
|
`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
|
`DISPLAY_PAGES_ON_MENU` (``True``) Whether to display pages on the menu of the
|
||||||
template. Templates can follow or not this
|
template. Templates may or not honor this
|
||||||
settings.
|
setting.
|
||||||
`FALLBACK_ON_FS_DATE` (``True``) If True, pelican will use the file system
|
`FALLBACK_ON_FS_DATE` (``True``) If True, Pelican will use the file system
|
||||||
dates infos (mtime) if it can't get
|
timestamp information (mtime) if it can't get
|
||||||
informations from the metadata
|
date information from the metadata.
|
||||||
`JINJA_EXTENSIONS` (``[]``) A list of any Jinja2 extensions you want to use.
|
`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.
|
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.
|
here or a single string representing one locale.
|
||||||
When providing a list, all the locales will be tried
|
When providing a list, all the locales will be tried
|
||||||
until one works.
|
until one works.
|
||||||
`MARKUP` (``('rst', 'md')``) A list of available markup languages you want
|
`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`.
|
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.
|
`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
|
`PDF_GENERATOR` (``False``) Set to True if you want to have PDF versions
|
||||||
of your documents. You will need to install
|
of your documents. You will need to install
|
||||||
`rst2pdf`.
|
`rst2pdf`.
|
||||||
`PLUGINS` (``[]``) The list of plugins to load. See :ref:`plugins`.
|
`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.
|
not.
|
||||||
`SITEURL` base URL of your website. Note that this is
|
`SITENAME` (``'A Pelican Blog'``) Your site name
|
||||||
not a way to tell pelican to use relative urls
|
`SITEURL` Base URL of your website. Note that this is
|
||||||
or static ones. You should rather use the
|
not a way to tell Pelican whether to use relative URLs
|
||||||
`RELATIVE_URL` setting for such use.
|
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
|
`STATIC_PATHS` (``['images']``) The static paths you want to have accessible
|
||||||
on the output path "static". By default,
|
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.
|
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
|
You can customize the URL's and locations where files will be saved. The URL's and
|
||||||
instead, at your covenience.
|
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
|
Note: If you specify a datetime directive, it will be substituted using the
|
||||||
not generate feeds for tags per default, but it is possible to do so using
|
input files' date metadata attribute. If the date is not specified for a
|
||||||
the ``TAG_FEED`` and ``TAG_FEED_RSS`` settings:
|
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?
|
Setting name (default value) what does it do?
|
||||||
================================================ =====================================================
|
================================================ =====================================================
|
||||||
`CATEGORY_FEED` ('feeds/%s.atom.xml'[2]_) Where to put the atom categories feeds.
|
`ARTICLE_URL` ('{slug}.html') The URL to refer to an ARTICLE.
|
||||||
`CATEGORY_FEED_RSS` (``None``, i.e. no RSS) Where to put the categories rss feeds.
|
`ARTICLE_SAVE_AS` ('{slug}.html') The place where we will save an article.
|
||||||
`FEED` (``'feeds/all.atom.xml'``) relative url to output the atom feed.
|
`ARTICLE_LANG_URL` ('{slug}-{lang}.html') The URL to refer to an ARTICLE which doesn't use the
|
||||||
`FEED_RSS` (``None``, i.e. no RSS) relative url to output the rss feed.
|
default language.
|
||||||
`TAG_FEED` (``None``, ie no tag feed) relative url to output the tags atom feed. It should
|
`ARTICLE_LANG_SAVE_AS` ('{slug}-{lang}.html' The place where we will save an article which
|
||||||
be defined using a "%s" matchin the tag name
|
doesn't use the default language.
|
||||||
`TAG_FEED_RSS` (``None``, ie no RSS tag feed) relative url to output the tag RSS feed
|
`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.
|
.. [2] %s is the name of the category.
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
==========
|
==========
|
||||||
|
|
||||||
The default behaviour of pelican is to list all the articles titles alongside
|
The default behaviour of Pelican is to list all the article titles along
|
||||||
with a short description of them on the index page. While it works pretty well
|
with a short description on the index page. While it works pretty well
|
||||||
for little to medium blogs, it is convenient to have a way to paginate this.
|
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.
|
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
|
`DEFAULT_ORPHANS` (0) The minimum number of articles allowed on the
|
||||||
last page. Use this when you don't want to
|
last page. Use this when you don't want to
|
||||||
have a last page with very few articles.
|
have a last page with very few articles.
|
||||||
`DEFAULT_PAGINATION` (5) The maximum number of articles to include on a
|
`DEFAULT_PAGINATION` (False) The maximum number of articles to include on a
|
||||||
page, not including orphans.
|
page, not including orphans. False to disable
|
||||||
`WITH_PAGINATION` (``False``) Activate pagination.
|
pagination.
|
||||||
================================================ =====================================================
|
================================================ =====================================================
|
||||||
|
|
||||||
Tag cloud
|
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.
|
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
|
`TAG_CLOUD_STEPS` (4) Count of different font sizes in the tag
|
||||||
cloud.
|
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::
|
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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
You should then also define a CSS with the appropriate classes (tag-0 to tag-N, where
|
You should then also define a CSS style with the appropriate classes (tag-0 to tag-N, where
|
||||||
N matches `TAG_CLOUD_STEPS` -1.
|
N matches `TAG_CLOUD_STEPS` -1).
|
||||||
|
|
||||||
Translations
|
Translations
|
||||||
============
|
============
|
||||||
|
|
||||||
Pelican offers a way to translate articles. See the section on getting started for
|
Pelican offers a way to translate articles. See the Getting Started section for
|
||||||
more information about that.
|
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.
|
`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
|
.. [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
|
`REVERSE_ARCHIVE_ORDER` (``False``) Reverse the archives list order. (True: orders by date
|
||||||
descending order: the newer first)
|
in descending order, with newer articles first.)
|
||||||
`REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True makes it in
|
`REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse
|
||||||
descending order, default is alphabetically)
|
alphabetical order; default lists alphabetically.)
|
||||||
================================================ =====================================================
|
================================================ =====================================================
|
||||||
|
|
||||||
Theming
|
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.
|
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
|
complete static path to a theme folder, or
|
||||||
chosen between the list of default themes (see
|
chosen between the list of default themes (see
|
||||||
below)
|
below)
|
||||||
`THEME_STATIC_PATHS` (``['static']``) Static theme paths you want to copy. Default
|
`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.
|
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
|
* notmyidea
|
||||||
* simple (a synonym for "full text" :)
|
* simple (a synonym for "full text" :)
|
||||||
|
|
||||||
You can define your own theme too, and specify it's emplacement in the same
|
You can define your own theme too, and specify its placement in the same
|
||||||
way (be sure to specify the full absolute path to it).
|
manner. (Be sure to specify the full absolute path to it.)
|
||||||
|
|
||||||
Here is `a guide on how to create your theme
|
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.
|
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
|
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
|
`DISQUS_SITENAME` Pelican can handle Disqus comments. Specify the
|
||||||
sitename you've filled in on disqus
|
Disqus sitename identifier here.
|
||||||
`GITHUB_URL` Your github URL (if you have one), it will then
|
`GITHUB_URL` Your GitHub URL (if you have one). It will then
|
||||||
use it to create a github ribbon.
|
use this information to create a GitHub ribbon.
|
||||||
`GOOGLE_ANALYTICS` 'UA-XXXX-YYYY' to activate google analytics.
|
`GOOGLE_ANALYTICS` 'UA-XXXX-YYYY' to activate Google Analytics.
|
||||||
`LINKS` A list of tuples (Title, Url) for links to appear on
|
`MENUITEMS` A list of tuples (Title, URL) for additional menu
|
||||||
the header.
|
items to appear at the beginning of the main menu.
|
||||||
`PIWIK_URL` URL to your Piwik server - without 'http://' at the
|
`PIWIK_URL` URL to your Piwik server - without 'http://' at the
|
||||||
beginning.
|
beginning.
|
||||||
`PIWIK_SSL_URL` If the SSL-URL differs from the normal Piwik-URL
|
`PIWIK_SSL_URL` If the SSL-URL differs from the normal Piwik-URL
|
||||||
you have to include this setting too. (optional)
|
you have to include this setting too. (optional)
|
||||||
`PIWIK_SITE_ID` ID for the monitored website. You can find the ID
|
`PIWIK_SITE_ID` ID for the monitored website. You can find the ID
|
||||||
in the Piwik admin interface > settings > websites.
|
in the Piwik admin interface > settings > websites.
|
||||||
`SOCIAL` A list of tuples (Title, Url) to appear in the "social"
|
`LINKS` A list of tuples (Title, URL) for links to appear on
|
||||||
section.
|
the header.
|
||||||
`TWITTER_USERNAME` Allows to add a button on the articles to tweet about
|
`SOCIAL` A list of tuples (Title, URL) to appear in the
|
||||||
them. Add you twitter username if you want this
|
"social" section.
|
||||||
button to appear.
|
`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
|
In addition, you can use the "wide" version of the `notmyidea` theme by
|
||||||
adding that in your configuration::
|
adding the following to your configuration::
|
||||||
|
|
||||||
CSS_FILE = "wide.css"
|
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&blog=(.*)&.*')
|
|
||||||
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`
|
.. _pelican-themes: :doc:`pelican-themes`
|
||||||
|
|
||||||
|
Example settings
|
||||||
|
================
|
||||||
|
|
||||||
|
.. literalinclude:: ../samples/pelican.conf.py
|
||||||
|
:language: python
|
||||||
|
|
|
||||||
260
docs/themes.rst
260
docs/themes.rst
|
|
@ -1,10 +1,10 @@
|
||||||
.. _theming-pelican:
|
.. _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
|
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,
|
create your own theme, feel free to take inspiration from the "simple" theme,
|
||||||
which is available `here
|
which is available `here
|
||||||
<https://github.com/ametaireau/pelican/tree/master/pelican/themes/simple/templates>`_
|
<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
|
└── templates
|
||||||
├── archives.html // to display archives
|
├── archives.html // to display archives
|
||||||
├── article.html // processed for each article
|
├── 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
|
├── categories.html // must list all the categories
|
||||||
├── category.html // processed for each category
|
├── category.html // processed for each category
|
||||||
├── index.html // the index. List all the articles
|
├── 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
|
├── tag.html // processed for each tag
|
||||||
└── tags.html // must list all the tags. Can be a tag cloud.
|
└── 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
|
* `static` contains all the static assets, which will be copied to the output
|
||||||
`theme/static` folder then. I've put the css and image folders, but they are
|
`theme/static` folder. I've put the CSS and image folders here, but they are
|
||||||
just examples. Put what you need here.
|
just examples. Put what you need here.
|
||||||
|
|
||||||
* `templates` contains all the templates that will be used to generate the content.
|
* `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
|
I've just put the mandatory templates here; you can define your own if it helps
|
||||||
you to organize yourself while doing the theme.
|
you keep things organized while creating your theme.
|
||||||
|
|
||||||
Templates and variables
|
Templates and variables
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
It's using a simple syntax, that you can embbed into your html pages.
|
The idea is to use a simple syntax that you can embed into your HTML pages.
|
||||||
This document describes which templates should exist on a theme, and which
|
This document describes which templates should exist in a theme, and which
|
||||||
variables will be passed to each template, while generating it.
|
variables will be passed to each template at generation time.
|
||||||
|
|
||||||
All templates will receive the variables defined in your settings file, if they
|
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
|
Common variables
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
All of those settings will be given to all templates.
|
All of these settings will be available to all templates.
|
||||||
|
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
articles That's the list of articles, ordered desc. by date
|
articles The list of articles, ordered descending by date
|
||||||
all the elements are `Article` objects, so you can
|
All the elements are `Article` objects, so you can
|
||||||
access their properties (e.g. title, summary, author
|
access their attributes (e.g. title, summary, author
|
||||||
etc.).
|
etc.)
|
||||||
dates The same list of article, but ordered by date,
|
dates The same list of articles, but ordered by date,
|
||||||
ascending.
|
ascending
|
||||||
tags A dict containing each tags (keys), and the list of
|
tags A key-value dict containing the tags (the keys) and
|
||||||
relative articles.
|
the list of respective articles (the values)
|
||||||
categories A dict containing each category (keys), and the
|
categories A key-value dict containing the categories (keys)
|
||||||
list of relative articles.
|
and the list of respective articles (values)
|
||||||
pages The list of pages.
|
pages The list of pages
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
|
|
||||||
index.html
|
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
|
Variable Description
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
articles_paginator A paginator object of article list.
|
articles_paginator A paginator object for the list of articles
|
||||||
articles_page The current page of articles.
|
articles_page The current page of articles
|
||||||
dates_paginator A paginator object of article list, ordered by date,
|
dates_paginator A paginator object for the article list, ordered by
|
||||||
ascending.
|
date, ascending.
|
||||||
dates_page The current page of articles, ordered by date,
|
dates_page The current page of articles, ordered by date,
|
||||||
ascending.
|
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
|
category.html
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
This template will be processed for each of the existing categories, and will
|
This template will be processed for each of the existing categories, with
|
||||||
finally remain at output/category/`category_name`.html.
|
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.
|
output/category/`category_name``n`.html.
|
||||||
|
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
category The name of the category being processed.
|
category The name of the category being processed
|
||||||
articles Articles of this category.
|
articles Articles for this category
|
||||||
dates Articles of this category, but ordered by date,
|
dates Articles for this category, but ordered by date,
|
||||||
ascending.
|
ascending
|
||||||
articles_paginator A paginator object of article list.
|
articles_paginator A paginator object for the list of articles
|
||||||
articles_page The current page of articles.
|
articles_page The current page of articles
|
||||||
dates_paginator A paginator object of article list, ordered by date,
|
dates_paginator A paginator object for the list of articles,
|
||||||
ascending.
|
ordered by date, ascending
|
||||||
dates_page The current page of articles, ordered by date,
|
dates_page The current page of articles, ordered by date,
|
||||||
ascending.
|
ascending
|
||||||
page_name 'category/`category_name`'. Useful for pagination
|
page_name 'category/`category_name`' -- useful for pagination
|
||||||
links.
|
links
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
|
|
||||||
article.html
|
article.html
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
This template will be processed for each article. .html files will be output
|
This template will be processed for each article, with .html files saved
|
||||||
in output/`article_name`.html. Here are the specific variables it gets.
|
as output/`article_name`.html. Here are the specific variables it gets.
|
||||||
|
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
article The article object to be displayed.
|
article The article object to be displayed
|
||||||
category The name of the category of the current article.
|
category The name of the category for the current article
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
|
|
||||||
page.html
|
page.html
|
||||||
---------
|
---------
|
||||||
|
|
||||||
For each page, this template will be processed. It will create .html files in
|
This template will be processed for each page, with corresponding .html files
|
||||||
output/`page_name`.html.
|
saved as output/`page_name`.html.
|
||||||
|
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
page The page object to be displayed. You can access to
|
page The page object to be displayed. You can access its
|
||||||
its title, slug and content.
|
title, slug, and content.
|
||||||
============= ===================================================
|
============= ===================================================
|
||||||
|
|
||||||
tag.html
|
tag.html
|
||||||
--------
|
--------
|
||||||
|
|
||||||
For each tag, this template will be processed. It will create .html files in
|
This template will be processed for each tag, with corresponding .html files
|
||||||
output/tag/`tag_name`.html.
|
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.
|
output/tag/`tag_name``n`.html.
|
||||||
|
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
Variable Description
|
Variable Description
|
||||||
=================== ===================================================
|
=================== ===================================================
|
||||||
tag The name of the tag being processed.
|
tag The name of the tag being processed
|
||||||
articles Articles related to this tag.
|
articles Articles related to this tag
|
||||||
dates Articles related to this tag, but ordered by date,
|
dates Articles related to this tag, but ordered by date,
|
||||||
ascending.
|
ascending
|
||||||
articles_paginator A paginator object of article list.
|
articles_paginator A paginator object for the list of articles
|
||||||
articles_page The current page of articles.
|
articles_page The current page of articles
|
||||||
dates_paginator A paginator object of article list, ordered by date,
|
dates_paginator A paginator object for the list of articles,
|
||||||
ascending.
|
ordered by date, ascending
|
||||||
dates_page The current page of articles, ordered by date,
|
dates_page The current page of articles, ordered by date,
|
||||||
ascending.
|
ascending
|
||||||
page_name 'tag/`tag_name`'. Useful for pagination links.
|
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
|
Since version 3.0, Pelican supports inheritance from the ``simple`` theme, so
|
||||||
actions:
|
you can re-use the ``simple`` theme templates in your own themes.
|
||||||
|
|
||||||
* Copy `skribit_tab_script.html` and `skribit_widget_script.html` in your
|
If one of the mandatory files in the ``templates/`` directory of your theme is
|
||||||
templates directory.
|
missing, it will be replaced by the matching template from the ``simple`` theme.
|
||||||
* Add {% include 'skribit_tab_script.html' %} in your <head> part in order to
|
So if the HTML structure of a template in the ``simple`` theme is right for you,
|
||||||
support suggestions tab.
|
you don't have to write a new template from scratch.
|
||||||
* Add {% include 'skribit_widget_script.html' %} where you want in order to
|
|
||||||
support sidebar widget.
|
|
||||||
|
|
||||||
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
33
docs/tips.rst
Normal 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
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from pelican import signals
|
from pelican import signals
|
||||||
|
|
||||||
from pelican.generators import (ArticlesGenerator, PagesGenerator,
|
from pelican.generators import (ArticlesGenerator, PagesGenerator,
|
||||||
StaticGenerator, PdfGenerator)
|
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.utils import clean_output_dir, files_changed
|
||||||
from pelican.writers import Writer
|
from pelican.writers import Writer
|
||||||
from pelican import log
|
from pelican import log
|
||||||
|
|
||||||
VERSION = "2.7.2"
|
__major__ = 3
|
||||||
|
__minor__ = 0
|
||||||
|
__version__ = "{0}.{1}".format(__major__, __minor__)
|
||||||
|
|
||||||
|
|
||||||
class Pelican(object):
|
class Pelican(object):
|
||||||
|
|
@ -20,9 +24,12 @@ class Pelican(object):
|
||||||
"""Read the settings, and performs some checks on the environment
|
"""Read the settings, and performs some checks on the environment
|
||||||
before doing anything else.
|
before doing anything else.
|
||||||
"""
|
"""
|
||||||
|
if settings is None:
|
||||||
|
settings = _DEFAULT_CONFIG
|
||||||
|
|
||||||
self.path = path or settings['PATH']
|
self.path = path or settings['PATH']
|
||||||
if not self.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)')
|
' (see pelican --help for more information)')
|
||||||
|
|
||||||
if self.path.endswith('/'):
|
if self.path.endswith('/'):
|
||||||
|
|
@ -30,11 +37,15 @@ class Pelican(object):
|
||||||
|
|
||||||
# define the default settings
|
# define the default settings
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
|
self._handle_deprecation()
|
||||||
|
|
||||||
self.theme = theme or settings['THEME']
|
self.theme = theme or settings['THEME']
|
||||||
output_path = output_path or settings['OUTPUT_PATH']
|
output_path = output_path or settings['OUTPUT_PATH']
|
||||||
self.output_path = os.path.realpath(output_path)
|
self.output_path = os.path.realpath(output_path)
|
||||||
self.markup = markup or settings['MARKUP']
|
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
|
# find the theme in pelican.theme if the given one does not exists
|
||||||
if not os.path.exists(self.theme):
|
if not os.path.exists(self.theme):
|
||||||
|
|
@ -52,13 +63,52 @@ class Pelican(object):
|
||||||
self.plugins = self.settings['PLUGINS']
|
self.plugins = self.settings['PLUGINS']
|
||||||
for plugin in self.plugins:
|
for plugin in self.plugins:
|
||||||
# if it's a string, then import it
|
# if it's a string, then import it
|
||||||
if isinstance(plugin, str):
|
if isinstance(plugin, basestring):
|
||||||
log.debug("Loading plugin `{0}' ...".format(plugin))
|
log.debug("Loading plugin `{0}' ...".format(plugin))
|
||||||
plugin = __import__(plugin, globals(), locals(), 'module')
|
plugin = __import__(plugin, globals(), locals(), 'module')
|
||||||
|
|
||||||
log.debug("Registering plugin `{0}' ...".format(plugin.__name__))
|
log.debug("Registering plugin `{0}' ...".format(plugin.__name__))
|
||||||
plugin.register()
|
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):
|
def run(self):
|
||||||
"""Run the generators and return"""
|
"""Run the generators and return"""
|
||||||
|
|
||||||
|
|
@ -81,7 +131,7 @@ class Pelican(object):
|
||||||
|
|
||||||
# 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
|
# explicitely asked
|
||||||
if (self.delete_outputdir and
|
if (self.delete_outputdir and not
|
||||||
os.path.realpath(self.path).startswith(self.output_path)):
|
os.path.realpath(self.path).startswith(self.output_path)):
|
||||||
clean_output_dir(self.output_path)
|
clean_output_dir(self.output_path)
|
||||||
|
|
||||||
|
|
@ -91,7 +141,6 @@ class Pelican(object):
|
||||||
if hasattr(p, 'generate_output'):
|
if hasattr(p, 'generate_output'):
|
||||||
p.generate_output(writer)
|
p.generate_output(writer)
|
||||||
|
|
||||||
|
|
||||||
def get_generator_classes(self):
|
def get_generator_classes(self):
|
||||||
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator]
|
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator]
|
||||||
if self.settings['PDF_GENERATOR']:
|
if self.settings['PDF_GENERATOR']:
|
||||||
|
|
@ -102,43 +151,49 @@ class Pelican(object):
|
||||||
return Writer(self.output_path, settings=self.settings)
|
return Writer(self.output_path, settings=self.settings)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="""A tool to generate a
|
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='?',
|
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',
|
parser.add_argument('-t', '--theme-path', dest='theme',
|
||||||
help='Path where to find the theme templates. If not specified, it'
|
help='Path where to find the theme templates. If not specified, it'
|
||||||
'will use the default one included with pelican.')
|
'will use the default one included with pelican.')
|
||||||
parser.add_argument('-o', '--output', dest='output',
|
parser.add_argument('-o', '--output', dest='output',
|
||||||
help='Where to output the generated files. If not specified, a directory'
|
help='Where to output the generated files. If not specified, a '
|
||||||
' will be created, named "output" in the current path.')
|
'directory will be created, named "output" in the current path.')
|
||||||
parser.add_argument('-m', '--markup', default=None, dest='markup',
|
parser.add_argument('-m', '--markup', dest='markup',
|
||||||
help='the list of markup language to use (rst or md). Please indicate '
|
help='The list of markup language to use (rst or md). Please indicate '
|
||||||
'them separated by commas')
|
'them separated by commas.')
|
||||||
parser.add_argument('-s', '--settings', dest='settings', default='',
|
parser.add_argument('-s', '--settings', dest='settings',
|
||||||
help='the settings of the application.')
|
help='The settings of the application.')
|
||||||
parser.add_argument('-d', '--delete-output-directory', dest='delete_outputdir',
|
parser.add_argument('-d', '--delete-output-directory',
|
||||||
|
dest='delete_outputdir',
|
||||||
action='store_true', help='Delete the output directory.')
|
action='store_true', help='Delete the output directory.')
|
||||||
parser.add_argument('-v', '--verbose', action='store_const', const=log.INFO, dest='verbosity',
|
parser.add_argument('-v', '--verbose', action='store_const',
|
||||||
help='Show all messages')
|
const=log.INFO, dest='verbosity',
|
||||||
parser.add_argument('-q', '--quiet', action='store_const', const=log.CRITICAL, dest='verbosity',
|
help='Show all messages.')
|
||||||
help='Show only critical errors')
|
parser.add_argument('-q', '--quiet', action='store_const',
|
||||||
parser.add_argument('-D', '--debug', action='store_const', const=log.DEBUG, dest='verbosity',
|
const=log.CRITICAL, dest='verbosity',
|
||||||
help='Show all message, including debug messages')
|
help='Show only critical errors.')
|
||||||
parser.add_argument('--version', action='version', version=VERSION,
|
parser.add_argument('-D', '--debug', action='store_const',
|
||||||
help='Print the pelican version and exit')
|
const=log.DEBUG, dest='verbosity',
|
||||||
parser.add_argument('-r', '--autoreload', dest='autoreload', action='store_true',
|
help='Show all message, including debug messages.')
|
||||||
help="Relaunch pelican each time a modification occurs on the content"
|
parser.add_argument('--version', action='version', version=__version__,
|
||||||
"files")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
log.init(args.verbosity)
|
log.init(args.verbosity)
|
||||||
# Split the markup languages only if some have been given. Otherwise, populate
|
# Split the markup languages only if some have been given. Otherwise,
|
||||||
# the variable with None.
|
# populate the variable with None.
|
||||||
markup = [a.strip().lower() for a in args.markup.split(',')] if args.markup else None
|
markup = [a.strip().lower() for a in args.markup.split(',')]\
|
||||||
|
if args.markup else None
|
||||||
|
|
||||||
settings = read_settings(args.settings)
|
settings = read_settings(args.settings)
|
||||||
|
|
||||||
|
|
@ -168,8 +223,9 @@ def main():
|
||||||
else:
|
else:
|
||||||
pelican.run()
|
pelican.run()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.critical(str(e))
|
log.critical(unicode(e))
|
||||||
|
|
||||||
|
if (args.verbosity == log.DEBUG):
|
||||||
if __name__ == '__main__':
|
raise
|
||||||
main()
|
else:
|
||||||
|
sys.exit(getattr(e, 'exitcode', 1))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from pelican.utils import slugify, truncate_html_words
|
from datetime import datetime
|
||||||
from pelican.log import *
|
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.settings import _DEFAULT_CONFIG
|
||||||
|
from pelican.utils import slugify, truncate_html_words
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
class Page(object):
|
||||||
"""Represents a page
|
"""Represents a page
|
||||||
|
|
@ -11,18 +18,18 @@ class Page(object):
|
||||||
"""
|
"""
|
||||||
mandatory_properties = ('title',)
|
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
|
# init parameters
|
||||||
if not metadata:
|
if not metadata:
|
||||||
metadata = {}
|
metadata = {}
|
||||||
if not settings:
|
if not settings:
|
||||||
settings = _DEFAULT_CONFIG
|
settings = _DEFAULT_CONFIG
|
||||||
|
|
||||||
|
self.settings = settings
|
||||||
self._content = content
|
self._content = content
|
||||||
self.translations = []
|
self.translations = []
|
||||||
|
|
||||||
self.status = "published" # default value
|
|
||||||
|
|
||||||
local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
|
local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
|
||||||
local_metadata.update(metadata)
|
local_metadata.update(metadata)
|
||||||
|
|
||||||
|
|
@ -33,7 +40,12 @@ class Page(object):
|
||||||
# default author to the one in settings if not defined
|
# default author to the one in settings if not defined
|
||||||
if not hasattr(self, 'author'):
|
if not hasattr(self, 'author'):
|
||||||
if 'AUTHOR' in settings:
|
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
|
# manage languages
|
||||||
self.in_default_lang = True
|
self.in_default_lang = True
|
||||||
|
|
@ -48,21 +60,6 @@ class Page(object):
|
||||||
if not hasattr(self, 'slug') and hasattr(self, 'title'):
|
if not hasattr(self, 'slug') and hasattr(self, 'title'):
|
||||||
self.slug = slugify(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:
|
if filename:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
|
||||||
|
|
@ -73,16 +70,29 @@ class Page(object):
|
||||||
else:
|
else:
|
||||||
self.date_format = settings['DEFAULT_DATE_FORMAT']
|
self.date_format = settings['DEFAULT_DATE_FORMAT']
|
||||||
|
|
||||||
if hasattr(self, 'date'):
|
if isinstance(self.date_format, tuple):
|
||||||
self.locale_date = self.date.strftime(self.date_format.encode('ascii','xmlcharrefreplace')).decode('utf')
|
locale.setlocale(locale.LC_ALL, self.date_format[0])
|
||||||
|
self.date_format = self.date_format[1]
|
||||||
|
|
||||||
# manage summary
|
if hasattr(self, 'date'):
|
||||||
if not hasattr(self, 'summary'):
|
encoded_date = self.date.strftime(
|
||||||
self.summary = property(lambda self: truncate_html_words(self.content, 50)).__get__(self, Page)
|
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
|
# manage status
|
||||||
if not hasattr(self, 'status'):
|
if not hasattr(self, 'status'):
|
||||||
self.status = settings['DEFAULT_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):
|
def check_properties(self):
|
||||||
"""test that each mandatory property is set."""
|
"""test that each mandatory property is set."""
|
||||||
|
|
@ -90,6 +100,24 @@ class Page(object):
|
||||||
if not hasattr(self, prop):
|
if not hasattr(self, prop):
|
||||||
raise NameError(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
|
@property
|
||||||
def content(self):
|
def content(self):
|
||||||
if hasattr(self, "_get_content"):
|
if hasattr(self, "_get_content"):
|
||||||
|
|
@ -98,6 +126,24 @@ class Page(object):
|
||||||
content = self._content
|
content = self._content
|
||||||
return 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):
|
class Article(Page):
|
||||||
mandatory_properties = ('title', 'date', 'category')
|
mandatory_properties = ('title', 'date', 'category')
|
||||||
|
|
@ -107,10 +153,53 @@ class Quote(Page):
|
||||||
base_properties = ('author', 'date')
|
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):
|
def is_valid_content(content, f):
|
||||||
try:
|
try:
|
||||||
content.check_properties()
|
content.check_properties()
|
||||||
return True
|
return True
|
||||||
except NameError, e:
|
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
|
return False
|
||||||
|
|
|
||||||
212
pelican/generators.py
Executable file → Normal file
212
pelican/generators.py
Executable file → Normal file
|
|
@ -1,20 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 os
|
||||||
|
import datetime
|
||||||
import math
|
import math
|
||||||
import random
|
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 jinja2.exceptions import TemplateNotFound
|
||||||
|
|
||||||
from pelican.utils import copy, get_relative_path, process_translations, open
|
from pelican.contents import Article, Page, Category, is_valid_content
|
||||||
from pelican.contents import Article, Page, is_valid_content
|
from pelican.log import warning, error, debug, info
|
||||||
from pelican.readers import read_file
|
from pelican.readers import read_file
|
||||||
from pelican.log import *
|
from pelican.utils import copy, process_translations, open
|
||||||
from pelican import signals
|
from pelican import signals
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,12 +32,24 @@ class Generator(object):
|
||||||
|
|
||||||
# templates cache
|
# templates cache
|
||||||
self._templates = {}
|
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(
|
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', []),
|
extensions=self.settings.get('JINJA_EXTENSIONS', []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
debug('template list: {0}'.format(self._env.list_templates()))
|
||||||
|
|
||||||
# get custom Jinja filters from user settings
|
# get custom Jinja filters from user settings
|
||||||
custom_filters = self.settings.get('JINJA_FILTERS', {})
|
custom_filters = self.settings.get('JINJA_FILTERS', {})
|
||||||
self._env.filters.update(custom_filters)
|
self._env.filters.update(custom_filters)
|
||||||
|
|
@ -50,8 +63,8 @@ class Generator(object):
|
||||||
try:
|
try:
|
||||||
self._templates[name] = self._env.get_template(name + '.html')
|
self._templates[name] = self._env.get_template(name + '.html')
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
raise Exception('[templates] unable to load %s.html from %s' % (
|
raise Exception('[templates] unable to load %s.html from %s' \
|
||||||
name, self._templates_path))
|
% (name, self._templates_path))
|
||||||
return self._templates[name]
|
return self._templates[name]
|
||||||
|
|
||||||
def get_files(self, path, exclude=[], extensions=None):
|
def get_files(self, path, exclude=[], extensions=None):
|
||||||
|
|
@ -67,7 +80,7 @@ class Generator(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
iter = os.walk(path, followlinks=True)
|
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)
|
iter = os.walk(path)
|
||||||
|
|
||||||
for root, dirs, temp_files in iter:
|
for root, dirs, temp_files in iter:
|
||||||
|
|
@ -94,11 +107,12 @@ class ArticlesGenerator(Generator):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""initialize properties"""
|
"""initialize properties"""
|
||||||
self.articles = [] # only articles in default language
|
self.articles = [] # only articles in default language
|
||||||
self.translations = []
|
self.translations = []
|
||||||
self.dates = {}
|
self.dates = {}
|
||||||
self.tags = defaultdict(list)
|
self.tags = defaultdict(list)
|
||||||
self.categories = defaultdict(list)
|
self.categories = defaultdict(list)
|
||||||
|
self.authors = defaultdict(list)
|
||||||
super(ArticlesGenerator, self).__init__(*args, **kwargs)
|
super(ArticlesGenerator, self).__init__(*args, **kwargs)
|
||||||
self.drafts = []
|
self.drafts = []
|
||||||
signals.article_generator_init.send(self)
|
signals.article_generator_init.send(self)
|
||||||
|
|
@ -106,49 +120,52 @@ class ArticlesGenerator(Generator):
|
||||||
def generate_feeds(self, writer):
|
def generate_feeds(self, writer):
|
||||||
"""Generate the feeds from the current context, and output files."""
|
"""Generate the feeds from the current context, and output files."""
|
||||||
|
|
||||||
writer.write_feed(self.articles, self.context, self.settings['FEED'])
|
if self.settings.get('FEED'):
|
||||||
|
|
||||||
if 'FEED_RSS' in self.settings:
|
|
||||||
writer.write_feed(self.articles, self.context,
|
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:
|
for cat, arts in self.categories:
|
||||||
arts.sort(key=attrgetter('date'), reverse=True)
|
arts.sort(key=attrgetter('date'), reverse=True)
|
||||||
writer.write_feed(arts, self.context,
|
if self.settings.get('CATEGORY_FEED'):
|
||||||
self.settings['CATEGORY_FEED'] % cat)
|
|
||||||
|
|
||||||
if 'CATEGORY_FEED_RSS' in self.settings:
|
|
||||||
writer.write_feed(arts, self.context,
|
writer.write_feed(arts, self.context,
|
||||||
self.settings['CATEGORY_FEED_RSS'] % cat,
|
self.settings['CATEGORY_FEED'] % cat)
|
||||||
feed_type='rss')
|
|
||||||
|
|
||||||
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():
|
for tag, arts in self.tags.items():
|
||||||
arts.sort(key=attrgetter('date'), reverse=True)
|
arts.sort(key=attrgetter('date'), reverse=True)
|
||||||
writer.write_feed(arts, self.context,
|
if self.settings.get('TAG_FEED'):
|
||||||
self.settings['TAG_FEED'] % tag)
|
|
||||||
|
|
||||||
if 'TAG_FEED_RSS' in self.settings:
|
|
||||||
writer.write_feed(arts, self.context,
|
writer.write_feed(arts, self.context,
|
||||||
self.settings['TAG_FEED_RSS'] % tag, feed_type='rss')
|
self.settings['TAG_FEED'] % tag)
|
||||||
|
|
||||||
translations_feeds = defaultdict(list)
|
if self.settings.get('TAG_FEED_RSS'):
|
||||||
for article in chain(self.articles, self.translations):
|
writer.write_feed(arts, self.context,
|
||||||
translations_feeds[article.lang].append(article)
|
self.settings['TAG_FEED_RSS'] % tag,
|
||||||
|
feed_type='rss')
|
||||||
|
|
||||||
for lang, items in translations_feeds.items():
|
if self.settings.get('TRANSLATION_FEED'):
|
||||||
items.sort(key=attrgetter('date'), reverse=True)
|
translations_feeds = defaultdict(list)
|
||||||
writer.write_feed(items, self.context,
|
for article in chain(self.articles, self.translations):
|
||||||
self.settings['TRANSLATION_FEED'] % lang)
|
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):
|
def generate_pages(self, writer):
|
||||||
"""Generate the pages on the disk"""
|
"""Generate the pages on the disk"""
|
||||||
|
|
||||||
write = partial(
|
write = partial(writer.write_file,
|
||||||
writer.write_file,
|
relative_urls=self.settings.get('RELATIVE_URLS'))
|
||||||
relative_urls = self.settings.get('RELATIVE_URLS')
|
|
||||||
)
|
|
||||||
|
|
||||||
# to minimize the number of relative path stuff modification
|
# to minimize the number of relative path stuff modification
|
||||||
# in writer, articles pass first
|
# in writer, articles pass first
|
||||||
|
|
@ -163,55 +180,69 @@ class ArticlesGenerator(Generator):
|
||||||
paginated = {}
|
paginated = {}
|
||||||
if template in PAGINATED_TEMPLATES:
|
if template in PAGINATED_TEMPLATES:
|
||||||
paginated = {'articles': self.articles, 'dates': self.dates}
|
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
|
# and subfolders after that
|
||||||
tag_template = self.get_template('tag')
|
tag_template = self.get_template('tag')
|
||||||
for tag, articles in self.tags.items():
|
for tag, articles in self.tags.items():
|
||||||
articles.sort(key=attrgetter('date'), reverse=True)
|
articles.sort(key=attrgetter('date'), reverse=True)
|
||||||
dates = [article for article in self.dates if article in articles]
|
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,
|
articles=articles, dates=dates,
|
||||||
paginated={'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')
|
category_template = self.get_template('category')
|
||||||
for cat, articles in self.categories:
|
for cat, articles in self.categories:
|
||||||
dates = [article for article in self.dates if article in articles]
|
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,
|
category=cat, articles=articles, dates=dates,
|
||||||
paginated={'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:
|
for article in self.drafts:
|
||||||
write('drafts/%s.html' % article.slug, article_template, self.context,
|
write('drafts/%s.html' % article.slug, article_template,
|
||||||
article=article, category=article.category)
|
self.context, article=article, category=article.category)
|
||||||
|
|
||||||
|
|
||||||
def generate_context(self):
|
def generate_context(self):
|
||||||
"""change the context"""
|
"""change the context"""
|
||||||
|
|
||||||
# return the list of files to use
|
|
||||||
files = self.get_files(self.path, exclude=['pages',])
|
|
||||||
all_articles = []
|
all_articles = []
|
||||||
for f in files:
|
for f in self.get_files(
|
||||||
content, metadata = read_file(f)
|
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 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:
|
if os.path.dirname(f) == self.path:
|
||||||
category = self.settings['DEFAULT_CATEGORY']
|
category = self.settings['DEFAULT_CATEGORY']
|
||||||
else:
|
else:
|
||||||
category = os.path.basename(os.path.dirname(f))
|
category = os.path.basename(os.path.dirname(f))\
|
||||||
|
.decode('utf-8')
|
||||||
|
|
||||||
if category != '':
|
if category != '':
|
||||||
metadata['category'] = unicode(category)
|
metadata['category'] = Category(category, self.settings)
|
||||||
|
|
||||||
if 'date' not in metadata.keys()\
|
if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']:
|
||||||
and self.settings['FALLBACK_ON_FS_DATE']:
|
metadata['date'] = datetime.datetime.fromtimestamp(
|
||||||
metadata['date'] = datetime.fromtimestamp(os.stat(f).st_ctime)
|
os.stat(f).st_ctime)
|
||||||
|
|
||||||
signals.article_generate_context.send(self, metadata=metadata)
|
signals.article_generate_context.send(self, metadata=metadata)
|
||||||
article = Article(content, metadata, settings=self.settings,
|
article = Article(content, metadata, settings=self.settings,
|
||||||
|
|
@ -232,7 +263,7 @@ class ArticlesGenerator(Generator):
|
||||||
for article in self.articles:
|
for article in self.articles:
|
||||||
# only main articles are listed in categories, not translations
|
# only main articles are listed in categories, not translations
|
||||||
self.categories[article.category].append(article)
|
self.categories[article.category].append(article)
|
||||||
|
self.authors[article.author].append(article)
|
||||||
|
|
||||||
# sort the articles by date
|
# sort the articles by date
|
||||||
self.articles.sort(key=attrgetter('date'), reverse=True)
|
self.articles.sort(key=attrgetter('date'), reverse=True)
|
||||||
|
|
@ -246,21 +277,20 @@ class ArticlesGenerator(Generator):
|
||||||
for tag in getattr(article, 'tags', []):
|
for tag in getattr(article, 'tags', []):
|
||||||
tag_cloud[tag] += 1
|
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')]
|
tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
|
||||||
|
|
||||||
tags = map(itemgetter(1), tag_cloud)
|
tags = map(itemgetter(1), tag_cloud)
|
||||||
if tags:
|
if tags:
|
||||||
max_count = max(tags)
|
max_count = max(tags)
|
||||||
steps = self.settings.get('TAG_CLOUD_STEPS')
|
steps = self.settings.get('TAG_CLOUD_STEPS')
|
||||||
|
|
||||||
# calculate word sizes
|
# calculate word sizes
|
||||||
self.tag_cloud = [
|
self.tag_cloud = [
|
||||||
(
|
(
|
||||||
tag,
|
tag,
|
||||||
int(
|
int(math.floor(steps - (steps - 1) * math.log(count)
|
||||||
math.floor(steps - (steps - 1) * math.log(count) / (math.log(max_count)or 1))
|
/ (math.log(max_count)or 1)))
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for tag, count in tag_cloud
|
for tag, count in tag_cloud
|
||||||
]
|
]
|
||||||
|
|
@ -271,9 +301,13 @@ class ArticlesGenerator(Generator):
|
||||||
|
|
||||||
# order the categories per name
|
# order the categories per name
|
||||||
self.categories = list(self.categories.items())
|
self.categories = list(self.categories.items())
|
||||||
self.categories.sort(reverse=self.settings.get('REVERSE_CATEGORY_ORDER'))
|
self.categories.sort(reverse=self.settings['REVERSE_CATEGORY_ORDER'])
|
||||||
self._update_context(('articles', 'dates', 'tags', 'categories', 'tag_cloud'))
|
|
||||||
|
|
||||||
|
self.authors = list(self.authors.items())
|
||||||
|
self.authors.sort()
|
||||||
|
|
||||||
|
self._update_context(('articles', 'dates', 'tags', 'categories',
|
||||||
|
'tag_cloud', 'authors'))
|
||||||
|
|
||||||
def generate_output(self, writer):
|
def generate_output(self, writer):
|
||||||
self.generate_feeds(writer)
|
self.generate_feeds(writer)
|
||||||
|
|
@ -289,8 +323,14 @@ class PagesGenerator(Generator):
|
||||||
|
|
||||||
def generate_context(self):
|
def generate_context(self):
|
||||||
all_pages = []
|
all_pages = []
|
||||||
for f in self.get_files(os.sep.join((self.path, 'pages'))):
|
for f in self.get_files(
|
||||||
content, metadata = read_file(f)
|
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,
|
page = Page(content, metadata, settings=self.settings,
|
||||||
filename=f)
|
filename=f)
|
||||||
if not is_valid_content(page, f):
|
if not is_valid_content(page, f):
|
||||||
|
|
@ -304,9 +344,9 @@ class PagesGenerator(Generator):
|
||||||
|
|
||||||
def generate_output(self, writer):
|
def generate_output(self, writer):
|
||||||
for page in chain(self.translations, self.pages):
|
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,
|
self.context, page=page,
|
||||||
relative_urls = self.settings.get('RELATIVE_URLS'))
|
relative_urls=self.settings.get('RELATIVE_URLS'))
|
||||||
|
|
||||||
|
|
||||||
class StaticGenerator(Generator):
|
class StaticGenerator(Generator):
|
||||||
|
|
@ -317,8 +357,8 @@ class StaticGenerator(Generator):
|
||||||
final_path=None):
|
final_path=None):
|
||||||
"""Copy all the paths from source to destination"""
|
"""Copy all the paths from source to destination"""
|
||||||
for path in paths:
|
for path in paths:
|
||||||
copy(path, source, os.path.join(output_path, destination), final_path,
|
copy(path, source, os.path.join(output_path, destination),
|
||||||
overwrite=True)
|
final_path, overwrite=True)
|
||||||
|
|
||||||
def generate_output(self, writer):
|
def generate_output(self, writer):
|
||||||
self._copy_paths(self.settings['STATIC_PATHS'], self.path,
|
self._copy_paths(self.settings['STATIC_PATHS'], self.path,
|
||||||
|
|
@ -328,7 +368,8 @@ class StaticGenerator(Generator):
|
||||||
|
|
||||||
# copy all the files needed
|
# copy all the files needed
|
||||||
for source, destination in self.settings['FILES_TO_COPY']:
|
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):
|
class PdfGenerator(Generator):
|
||||||
|
|
@ -337,7 +378,8 @@ class PdfGenerator(Generator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
from rst2pdf.createpdf import RstToPdf
|
from rst2pdf.createpdf import RstToPdf
|
||||||
self.pdfcreator = RstToPdf(breakside=0, stylesheets=['twelvepoint'])
|
self.pdfcreator = RstToPdf(breakside=0,
|
||||||
|
stylesheets=['twelvepoint'])
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise Exception("unable to find rst2pdf")
|
raise Exception("unable to find rst2pdf")
|
||||||
super(PdfGenerator, self).__init__(*args, **kwargs)
|
super(PdfGenerator, self).__init__(*args, **kwargs)
|
||||||
|
|
@ -345,9 +387,10 @@ class PdfGenerator(Generator):
|
||||||
def _create_pdf(self, obj, output_path):
|
def _create_pdf(self, obj, output_path):
|
||||||
if obj.filename.endswith(".rst"):
|
if obj.filename.endswith(".rst"):
|
||||||
filename = obj.slug + ".pdf"
|
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
|
# 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)
|
info(u' [ok] writing %s' % output_pdf)
|
||||||
|
|
||||||
def generate_context(self):
|
def generate_context(self):
|
||||||
|
|
@ -358,11 +401,12 @@ class PdfGenerator(Generator):
|
||||||
# since we write our own files
|
# since we write our own files
|
||||||
info(u' Generating PDF files...')
|
info(u' Generating PDF files...')
|
||||||
pdf_path = os.path.join(self.output_path, 'pdf')
|
pdf_path = os.path.join(self.output_path, 'pdf')
|
||||||
try:
|
if not os.path.exists(pdf_path):
|
||||||
os.mkdir(pdf_path)
|
try:
|
||||||
except OSError:
|
os.mkdir(pdf_path)
|
||||||
error("Couldn't create the pdf output folder in " + pdf_path)
|
except OSError:
|
||||||
pass
|
error("Couldn't create the pdf output folder in " + pdf_path)
|
||||||
|
pass
|
||||||
|
|
||||||
for article in self.context['articles']:
|
for article in self.context['articles']:
|
||||||
self._create_pdf(article, pdf_path)
|
self._create_pdf(article, pdf_path)
|
||||||
|
|
|
||||||
|
|
@ -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 critical, error, info, warning, warn, debug
|
||||||
from logging import Formatter, getLogger, StreamHandler
|
from logging import Formatter, getLogger, StreamHandler
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
global ANSI
|
|
||||||
ANSI = {
|
RESET_TERM = u'\033[0;m'
|
||||||
'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',
|
def start_color(index):
|
||||||
'yellow' : lambda(text) : u'\033[1;33m' + unicode(text) + u'\033[1;m',
|
return u'\033[1;{0}m'.format(index)
|
||||||
'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',
|
def term_color(color):
|
||||||
'white' : lambda(text) : u'\033[1;37m' + unicode(text) + u'\033[1;m',
|
code = COLOR_CODES[color]
|
||||||
'bgred' : lambda(text) : u'\033[1;41m' + unicode(text) + u'\033[1;m',
|
return lambda text: start_color(code) + unicode(text) + RESET_TERM
|
||||||
'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',
|
COLOR_CODES = {
|
||||||
'bgmagenta' : lambda(text) : u'\033[1;45m' + unicode(text) + u'\033[1;m',
|
'red': 31,
|
||||||
'bgcyan' : lambda(text) : u'\033[1;46m' + unicode(text) + u'\033[1;m',
|
'yellow': 33,
|
||||||
'bggray' : lambda(text) : u'\033[1;47m' + unicode(text) + u'\033[1;m',
|
'cyan': 36,
|
||||||
'bgyellow' : lambda(text) : u'\033[1;43m' + unicode(text) + u'\033[1;m',
|
'white': 37,
|
||||||
'bggrey' : lambda(text) : u'\033[1;100m' + unicode(text) + u'\033[1;m'
|
'bgred': 41,
|
||||||
|
'bggrey': 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ANSI = dict((col, term_color(col)) for col in COLOR_CODES)
|
||||||
|
|
||||||
|
|
||||||
class ANSIFormatter(Formatter):
|
class ANSIFormatter(Formatter):
|
||||||
"""
|
"""
|
||||||
|
|
@ -62,17 +65,16 @@ class TextFormatter(Formatter):
|
||||||
class DummyFormatter(object):
|
class DummyFormatter(object):
|
||||||
"""
|
"""
|
||||||
A dummy class.
|
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):
|
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)
|
return ANSIFormatter(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return TextFormatter( *args, **kwargs)
|
return TextFormatter(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init(level=None, logger=getLogger(), handler=StreamHandler()):
|
def init(level=None, logger=getLogger(), handler=StreamHandler()):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# From django.core.paginator
|
# From django.core.paginator
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
|
||||||
class Paginator(object):
|
class Paginator(object):
|
||||||
def __init__(self, object_list, per_page, orphans=0):
|
def __init__(self, object_list, per_page, orphans=0):
|
||||||
self.object_list = object_list
|
self.object_list = object_list
|
||||||
|
|
@ -39,6 +40,7 @@ class Paginator(object):
|
||||||
return range(1, self.num_pages + 1)
|
return range(1, self.num_pages + 1)
|
||||||
page_range = property(_get_page_range)
|
page_range = property(_get_page_range)
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
class Page(object):
|
||||||
def __init__(self, object_list, number, paginator):
|
def __init__(self, object_list, number, paginator):
|
||||||
self.object_list = object_list
|
self.object_list = object_list
|
||||||
|
|
@ -82,4 +84,3 @@ class Page(object):
|
||||||
if self.number == self.paginator.num_pages:
|
if self.number == self.paginator.num_pages:
|
||||||
return self.paginator.count
|
return self.paginator.count
|
||||||
return self.number * self.paginator.per_page
|
return self.number * self.paginator.per_page
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ import hashlib
|
||||||
|
|
||||||
from pelican import signals
|
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.
|
the gravatar url.
|
||||||
|
|
||||||
Settings:
|
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:
|
Article metadata:
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
:email: article's author email
|
: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.
|
article's context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,32 +6,39 @@ try:
|
||||||
from docutils.writers.html4css1 import HTMLTranslator
|
from docutils.writers.html4css1 import HTMLTranslator
|
||||||
|
|
||||||
# import the directives to have pygments support
|
# import the directives to have pygments support
|
||||||
from pelican import rstdirectives
|
from pelican import rstdirectives # NOQA
|
||||||
except ImportError:
|
except ImportError:
|
||||||
core = False
|
core = False
|
||||||
try:
|
try:
|
||||||
from markdown import Markdown
|
from markdown import Markdown
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Markdown = False
|
Markdown = False # NOQA
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from pelican.contents import Category, Tag, Author
|
||||||
from pelican.utils import get_date, open
|
from pelican.utils import get_date, open
|
||||||
|
|
||||||
|
|
||||||
_METADATA_PROCESSORS = {
|
_METADATA_PROCESSORS = {
|
||||||
'tags': lambda x: map(unicode.strip, x.split(',')),
|
'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')],
|
||||||
'date': lambda x: get_date(x),
|
'date': lambda x, y: get_date(x),
|
||||||
'status': unicode.strip,
|
'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):
|
class Reader(object):
|
||||||
enabled = True
|
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):
|
class _FieldBodyTranslator(HTMLTranslator):
|
||||||
|
|
@ -51,33 +58,35 @@ def render_node_to_html(document, node):
|
||||||
node.walkabout(visitor)
|
node.walkabout(visitor)
|
||||||
return visitor.astext()
|
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):
|
class RstReader(Reader):
|
||||||
enabled = bool(docutils)
|
enabled = bool(docutils)
|
||||||
extension = "rst"
|
extension = "rst"
|
||||||
|
|
||||||
def _parse_metadata(self, document):
|
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):
|
def _get_publisher(self, filename):
|
||||||
extra_params = {'initial_header_level': '2'}
|
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.set_components('standalone', 'restructuredtext', 'html')
|
||||||
pub.process_programmatic_settings(None, extra_params, None)
|
pub.process_programmatic_settings(None, extra_params, None)
|
||||||
pub.set_source(source_path=filename)
|
pub.set_source(source_path=filename)
|
||||||
|
|
@ -99,17 +108,18 @@ class RstReader(Reader):
|
||||||
class MarkdownReader(Reader):
|
class MarkdownReader(Reader):
|
||||||
enabled = bool(Markdown)
|
enabled = bool(Markdown)
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
extensions = ['codehilite', 'extra']
|
||||||
|
|
||||||
def read(self, filename):
|
def read(self, filename):
|
||||||
"""Parse content and metadata of markdown files"""
|
"""Parse content and metadata of markdown files"""
|
||||||
text = open(filename)
|
text = open(filename)
|
||||||
md = Markdown(extensions = ['meta', 'codehilite'])
|
md = Markdown(extensions=set(self.extensions + ['meta']))
|
||||||
content = md.convert(text)
|
content = md.convert(text)
|
||||||
|
|
||||||
metadata = {}
|
metadata = {}
|
||||||
for name, value in md.Meta.items():
|
for name, value in md.Meta.items():
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
metadata[name] = _process_metadata(name, value[0])
|
metadata[name] = self.process_metadata(name, value[0])
|
||||||
return content, metadata
|
return content, metadata
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,27 +129,42 @@ class HtmlReader(Reader):
|
||||||
|
|
||||||
def read(self, filename):
|
def read(self, filename):
|
||||||
"""Parse content and metadata of (x)HTML files"""
|
"""Parse content and metadata of (x)HTML files"""
|
||||||
content = open(filename)
|
with open(filename) as content:
|
||||||
metadata = {'title':'unnamed'}
|
metadata = {'title': 'unnamed'}
|
||||||
for i in self._re.findall(content):
|
for i in self._re.findall(content):
|
||||||
key = i.split(':')[0][5:].strip()
|
key = i.split(':')[0][5:].strip()
|
||||||
value = i.split(':')[-1][:-3].strip()
|
value = i.split(':')[-1][:-3].strip()
|
||||||
name = key.lower()
|
name = key.lower()
|
||||||
metadata[name] = _process_metadata(name, value)
|
metadata[name] = self.process_metadata(name, value)
|
||||||
|
|
||||||
return content, metadata
|
|
||||||
|
|
||||||
|
return content, metadata
|
||||||
|
|
||||||
|
|
||||||
_EXTENSIONS = dict((cls.extension, cls) for cls in Reader.__subclasses__())
|
_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."""
|
"""Return a reader object using the given format."""
|
||||||
if not fmt:
|
if not fmt:
|
||||||
fmt = filename.split('.')[-1]
|
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)
|
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:
|
if not reader.enabled:
|
||||||
raise ValueError("Missing dependencies for %s" % fmt)
|
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
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,4 @@ class Pygments(Directive):
|
||||||
return [nodes.raw('', parsed, format='html')]
|
return [nodes.raw('', parsed, format='html')]
|
||||||
|
|
||||||
directives.register_directive('code-block', Pygments)
|
directives.register_directive('code-block', Pygments)
|
||||||
|
directives.register_directive('sourcecode', Pygments)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
|
from os.path import isabs
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
from pelican import log
|
from pelican import log
|
||||||
|
|
@ -7,24 +8,43 @@ from pelican import log
|
||||||
DEFAULT_THEME = os.sep.join([os.path.dirname(os.path.abspath(__file__)),
|
DEFAULT_THEME = os.sep.join([os.path.dirname(os.path.abspath(__file__)),
|
||||||
"themes/notmyidea"])
|
"themes/notmyidea"])
|
||||||
_DEFAULT_CONFIG = {'PATH': None,
|
_DEFAULT_CONFIG = {'PATH': None,
|
||||||
|
'ARTICLE_DIR': '',
|
||||||
|
'ARTICLE_EXCLUDES': ('pages',),
|
||||||
|
'PAGE_DIR': 'pages',
|
||||||
|
'PAGE_EXCLUDES': (),
|
||||||
'THEME': DEFAULT_THEME,
|
'THEME': DEFAULT_THEME,
|
||||||
'OUTPUT_PATH': 'output/',
|
'OUTPUT_PATH': 'output/',
|
||||||
'MARKUP': ('rst', 'md'),
|
'MARKUP': ('rst', 'md'),
|
||||||
'STATIC_PATHS': ['images',],
|
'STATIC_PATHS': ['images', ],
|
||||||
'THEME_STATIC_PATHS': ['static',],
|
'THEME_STATIC_PATHS': ['static', ],
|
||||||
'FEED': 'feeds/all.atom.xml',
|
'FEED': 'feeds/all.atom.xml',
|
||||||
'CATEGORY_FEED': 'feeds/%s.atom.xml',
|
'CATEGORY_FEED': 'feeds/%s.atom.xml',
|
||||||
'TRANSLATION_FEED': 'feeds/all-%s.atom.xml',
|
'TRANSLATION_FEED': 'feeds/all-%s.atom.xml',
|
||||||
|
'FEED_MAX_ITEMS': '',
|
||||||
'SITENAME': 'A Pelican Blog',
|
'SITENAME': 'A Pelican Blog',
|
||||||
'DISPLAY_PAGES_ON_MENU': True,
|
'DISPLAY_PAGES_ON_MENU': True,
|
||||||
'PDF_GENERATOR': False,
|
'PDF_GENERATOR': False,
|
||||||
'DEFAULT_CATEGORY': 'misc',
|
'DEFAULT_CATEGORY': 'misc',
|
||||||
'FALLBACK_ON_FS_DATE': True,
|
'FALLBACK_ON_FS_DATE': True,
|
||||||
|
'WITH_FUTURE_DATES': True,
|
||||||
'CSS_FILE': 'main.css',
|
'CSS_FILE': 'main.css',
|
||||||
'REVERSE_ARCHIVE_ORDER': False,
|
'REVERSE_ARCHIVE_ORDER': False,
|
||||||
'REVERSE_CATEGORY_ORDER': False,
|
'REVERSE_CATEGORY_ORDER': False,
|
||||||
'DELETE_OUTPUT_DIRECTORY': 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,
|
'RELATIVE_URLS': True,
|
||||||
'DEFAULT_LANG': 'en',
|
'DEFAULT_LANG': 'en',
|
||||||
'TAG_CLOUD_STEPS': 4,
|
'TAG_CLOUD_STEPS': 4,
|
||||||
|
|
@ -35,17 +55,19 @@ _DEFAULT_CONFIG = {'PATH': None,
|
||||||
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
|
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
|
||||||
'DATE_FORMATS': {},
|
'DATE_FORMATS': {},
|
||||||
'JINJA_EXTENSIONS': [],
|
'JINJA_EXTENSIONS': [],
|
||||||
'LOCALE': '', # default to user locale
|
'LOCALE': '', # default to user locale
|
||||||
'WITH_PAGINATION': False,
|
'DEFAULT_PAGINATION': False,
|
||||||
'DEFAULT_PAGINATION': 5,
|
|
||||||
'DEFAULT_ORPHANS': 0,
|
'DEFAULT_ORPHANS': 0,
|
||||||
'DEFAULT_METADATA': (),
|
'DEFAULT_METADATA': (),
|
||||||
'FILES_TO_COPY': (),
|
'FILES_TO_COPY': (),
|
||||||
'DEFAULT_STATUS': 'published',
|
'DEFAULT_STATUS': 'published',
|
||||||
|
'ARTICLE_PERMALINK_STRUCTURE': '',
|
||||||
|
'TYPOGRIFY': False,
|
||||||
'PLUGINS': [],
|
'PLUGINS': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_settings(filename):
|
|
||||||
|
def read_settings(filename=None):
|
||||||
"""Load a Python file into a dictionary.
|
"""Load a Python file into a dictionary.
|
||||||
"""
|
"""
|
||||||
context = _DEFAULT_CONFIG.copy()
|
context = _DEFAULT_CONFIG.copy()
|
||||||
|
|
@ -56,6 +78,14 @@ def read_settings(filename):
|
||||||
if key.isupper():
|
if key.isupper():
|
||||||
context[key] = tempdict[key]
|
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
|
# if locales is not a list, make it one
|
||||||
locales = context['LOCALE']
|
locales = context['LOCALE']
|
||||||
|
|
||||||
|
|
@ -69,17 +99,17 @@ def read_settings(filename):
|
||||||
for locale_ in locales:
|
for locale_ in locales:
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, locale_)
|
locale.setlocale(locale.LC_ALL, locale_)
|
||||||
break # break if it is successfull
|
break # break if it is successfull
|
||||||
except locale.Error:
|
except locale.Error:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.warn("LOCALE option doesn't contain a correct value")
|
log.warn("LOCALE option doesn't contain a correct value")
|
||||||
|
|
||||||
# Make the paths relative to the settings file
|
if not 'TIMEZONE' in context:
|
||||||
for path in ['PATH', 'OUTPUT_PATH']:
|
log.warn("No timezone information specified in the settings. Assuming"
|
||||||
if path in context:
|
" your timezone is UTC for feed generation. Check "
|
||||||
if context[path] is not None and not os.path.isabs(context[path]):
|
"http://docs.notmyidea.org/alexis/pelican/settings.html#timezone "
|
||||||
context[path] = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(filename), context[path])))
|
"for more information")
|
||||||
|
|
||||||
# set the locale
|
# set the locale
|
||||||
return context
|
return context
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -10,12 +10,13 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
@import url("reset.css");
|
@import url("reset.css");
|
||||||
@import url("pygment.css");
|
@import url("pygment.css");
|
||||||
|
@import url("typogrify.css");
|
||||||
@import url(http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
|
@import url(http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz&subset=latin);
|
||||||
|
|
||||||
/***** Global *****/
|
/***** Global *****/
|
||||||
/* Body */
|
/* Body */
|
||||||
body {
|
body {
|
||||||
background: #F5F4EF url('../images/bg.png');
|
background: #F5F4EF;
|
||||||
color: #000305;
|
color: #000305;
|
||||||
font-size: 87.5%; /* Base font size: 14px */
|
font-size: 87.5%; /* Base font size: 14px */
|
||||||
font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
|
font-family: 'Trebuchet MS', Trebuchet, 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
|
||||||
|
|
|
||||||
3
pelican/themes/notmyidea/static/css/typogrify.css
Normal file
3
pelican/themes/notmyidea/static/css/typogrify.css
Normal 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;}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<dl>
|
<dl>
|
||||||
{% for article in dates %}
|
{% for article in dates %}
|
||||||
<dt>{{ article.locale_date }}</dt>
|
<dt>{{ article.locale_date }}</dt>
|
||||||
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
|
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section id="content" class="body">
|
<section id="content" class="body">
|
||||||
<article>
|
<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
|
rel="bookmark" title="Permalink to {{ article.title }}">{{ article.title
|
||||||
}}</a></h1> {% include 'twitter.html' %} </header>
|
}}</a></h1> {% include 'twitter.html' %} </header>
|
||||||
<div class="entry-content">
|
<div class="entry-content">
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
|
|
||||||
{% if article.author %}
|
{% if article.author %}
|
||||||
<address class="vcard 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>
|
</address>
|
||||||
{% endif %}
|
{% 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 'taglist.html' %}
|
||||||
{% include 'translations.html' %}
|
{% include 'translations.html' %}
|
||||||
</footer><!-- /.post-info -->
|
</footer><!-- /.post-info -->
|
||||||
|
|
|
||||||
2
pelican/themes/notmyidea/templates/author.html
Normal file
2
pelican/themes/notmyidea/templates/author.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}{{ SITENAME }} - {{ author }}{% endblock %}
|
||||||
|
|
@ -9,8 +9,6 @@
|
||||||
<link href="{{ SITEURL }}/{{ FEED_RSS }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
|
<link href="{{ SITEURL }}/{{ FEED_RSS }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'skribit_tab_script.html' %}
|
|
||||||
|
|
||||||
<!--[if IE]>
|
<!--[if IE]>
|
||||||
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
|
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
|
||||||
|
|
||||||
|
|
@ -33,7 +31,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if DISPLAY_PAGES_ON_MENU %}
|
{% if DISPLAY_PAGES_ON_MENU %}
|
||||||
{% for page in PAGES %}
|
{% 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 %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for cat, null in categories %}
|
{% for cat, null in categories %}
|
||||||
|
|
@ -54,7 +52,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- /.blogroll -->
|
</div><!-- /.blogroll -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'skribit_widget_script.html' %}
|
|
||||||
{% if SOCIAL %}
|
{% if SOCIAL %}
|
||||||
<div class="social">
|
<div class="social">
|
||||||
<h2>social</h2>
|
<h2>social</h2>
|
||||||
|
|
@ -74,7 +71,7 @@
|
||||||
|
|
||||||
<footer id="contentinfo" class="body">
|
<footer id="contentinfo" class="body">
|
||||||
<address id="about" class="vcard 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 -->
|
</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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for category, articles in categories %}
|
{% for category, articles in categories %}
|
||||||
<li>{{ category }}</li>
|
<li><a href="{{ category.url }}">{{ category }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
<section id="content" class="body">
|
<section id="content" class="body">
|
||||||
<h2>Pages</h2>
|
<h2>Pages</h2>
|
||||||
{% for page in PAGES %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
{% if DEFAULT_PAGINATION %}
|
||||||
<p class="paginator">
|
<p class="paginator">
|
||||||
{% if articles_page.has_previous() %}
|
{% if articles_page.has_previous() %}
|
||||||
{% if articles_page.previous_page_number() == 1 %}
|
{% if articles_page.previous_page_number() == 1 %}
|
||||||
|
|
@ -11,3 +12,4 @@
|
||||||
<a href="{{ SITEURL }}/{{ page_name }}{{ articles_page.next_page_number() }}.html">»</a>
|
<a href="{{ SITEURL }}/{{ page_name }}{{ articles_page.next_page_number() }}.html">»</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
|
||||||
|
|
@ -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&blog={{ SKRIBIT_WIDGET_ID }}&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 %}
|
|
||||||
|
|
@ -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 %}
|
{% if PDF_PROCESSOR %}<p><a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a></p>{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Archives for {{ SITENAME }}</h2>
|
<h1>Archives for {{ SITENAME }}</h1>
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
{% for article in dates %}
|
{% for article in dates %}
|
||||||
<dt>{{ article.locale_date }}</dt>
|
<dt>{{ article.locale_date }}</dt>
|
||||||
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
|
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section id="content" class="body">
|
<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">
|
<footer class="post-info">
|
||||||
<abbr class="published" title="{{ article.date.isoformat() }}">
|
<abbr class="published" title="{{ article.date.isoformat() }}">
|
||||||
{{ article.locale_date }}
|
{{ article.locale_date }}
|
||||||
</abbr>
|
</abbr>
|
||||||
{% if article.author %}
|
{% if article.author %}
|
||||||
<address class="vcard 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>
|
</address>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer><!-- /.post-info -->
|
</footer><!-- /.post-info -->
|
||||||
|
|
|
||||||
7
pelican/themes/simple/templates/author.html
Normal file
7
pelican/themes/simple/templates/author.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "index.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ SITENAME }} - Articles by {{ author }}{% endblock %}
|
||||||
|
{% block content_title %}
|
||||||
|
<h2>Articles by {{ author }}</h2>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
@ -1,25 +1,36 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ DEFAULT_LANG }}">
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}{{ SITENAME }}{%endblock%}</title>
|
{% block head %}
|
||||||
|
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
{% endblock head %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="index" class="home">
|
<body id="index" class="home">
|
||||||
<header id="banner" class="body">
|
<header id="banner" class="body">
|
||||||
<h1><a href="{{ SITEURL }}">{{ SITENAME }} <strong>{{ SITESUBTITLE }}</strong></a></h1>
|
<h1><a href="{{ SITEURL }}">{{ SITENAME }} <strong>{{ SITESUBTITLE }}</strong></a></h1>
|
||||||
</header><!-- /#banner -->
|
</header><!-- /#banner -->
|
||||||
{% if categories %}<ul>
|
<nav id="menu"><ul>
|
||||||
{% for category, articles in categories %}
|
{% for title, link in MENUITEMS %}
|
||||||
<li><a href="{{ SITEURL }}/category/{{category}}.html">{{ category }}</a></li>
|
<li><a href="{{ link }}">{{ title }}</a></li>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<footer id="contentinfo" class="body">
|
<footer id="contentinfo" class="body">
|
||||||
<address id="about" class="vcard body">
|
<address id="about" class="vcard body">
|
||||||
Proudly powered by <a href="http://docs.notmyidea.org/alexis/pelican/">pelican</a>,
|
Proudly powered by <a href="http://pelican.notmyidea.org/">Pelican</a>,
|
||||||
and obviously <a href="http://python.org">python</a>!
|
which takes great advantage of <a href="http://python.org">Python</a>.
|
||||||
</address><!-- /#about -->
|
</address><!-- /#about -->
|
||||||
</footer><!-- /#contentinfo -->
|
</footer><!-- /#contentinfo -->
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for category, articles in categories %}
|
{% for category, articles in categories %}
|
||||||
<li>{{ category }}</li>
|
<li><a href="{{ SITEURL }}/{{ category.url }}">{{ category }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
<ol id="post-list">
|
<ol id="post-list">
|
||||||
{% for article in articles_page.object_list %}
|
{% for article in articles_page.object_list %}
|
||||||
<li><article class="hentry">
|
<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">
|
<footer class="post-info">
|
||||||
<abbr class="published" title="{{ article.date.isoformat() }}"> {{ article.locale_date }} </abbr>
|
<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 -->
|
</footer><!-- /.post-info -->
|
||||||
<div class="entry-content"> {{ article.summary }} </div><!-- /.entry-content -->
|
<div class="entry-content"> {{ article.summary }} </div><!-- /.entry-content -->
|
||||||
</article></li>
|
</article></li>
|
||||||
|
|
|
||||||
0
pelican/tools/__init__.py
Normal file
0
pelican/tools/__init__.py
Normal file
295
pelican/tools/pelican_import.py
Executable file
295
pelican/tools/pelican_import.py
Executable 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)
|
||||||
264
pelican/tools/pelican_quickstart.py
Executable file
264
pelican/tools/pelican_quickstart.py
Executable 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'])
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os, sys, shutil
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pelican
|
import pelican
|
||||||
|
|
@ -38,11 +40,11 @@ def main():
|
||||||
|
|
||||||
excl= parser.add_mutually_exclusive_group()
|
excl= parser.add_mutually_exclusive_group()
|
||||||
excl.add_argument('-l', '--list', dest='action', action="store_const", const='list',
|
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',
|
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__),
|
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",
|
parser.add_argument('-i', '--install', dest='to_install', nargs='+', metavar="theme path",
|
||||||
|
|
@ -52,11 +54,11 @@ def main():
|
||||||
parser.add_argument('-s', '--symlink', dest='to_symlink', nargs='+', metavar="theme path",
|
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")
|
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",
|
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",
|
parser.add_argument('-v', '--verbose', dest='verbose', action="store_true",
|
||||||
help="Verbose output")
|
help="Verbose output")
|
||||||
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
@ -210,6 +212,3 @@ def clean(v=False):
|
||||||
c+=1
|
c+=1
|
||||||
|
|
||||||
print("\nRemoved {0} broken links".format(c))
|
print("\nRemoved {0} broken links".format(c))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
|
import pytz
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
|
||||||
from codecs import open as _open
|
from codecs import open as _open
|
||||||
|
from datetime import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from pelican.log import warning, info
|
from pelican.log import warning, info
|
||||||
|
|
@ -12,10 +14,14 @@ from pelican.log import warning, info
|
||||||
def get_date(string):
|
def get_date(string):
|
||||||
"""Return a datetime object from a 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',
|
string = re.sub(' +', ' ', string)
|
||||||
'%d/%m/%Y', '%d.%m.%Y', '%d.%m.%Y %H:%M']
|
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:
|
for date_format in formats:
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(string, date_format)
|
return datetime.strptime(string, date_format)
|
||||||
|
|
@ -42,6 +48,7 @@ def slugify(value):
|
||||||
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
|
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
|
||||||
return re.sub('[-\s]+', '-', value)
|
return re.sub('[-\s]+', '-', value)
|
||||||
|
|
||||||
|
|
||||||
def copy(path, source, destination, destination_path=None, overwrite=False):
|
def copy(path, source, destination, destination_path=None, overwrite=False):
|
||||||
"""Copy path from origin to destination.
|
"""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 source: the source dir
|
||||||
:param destination: the destination dir
|
:param destination: the destination dir
|
||||||
:param destination_path: the destination path (optional)
|
: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:
|
if not destination_path:
|
||||||
destination_path = path
|
destination_path = path
|
||||||
|
|
||||||
source_ = os.path.abspath(os.path.expanduser(os.path.join(source, path)))
|
source_ = os.path.abspath(os.path.expanduser(os.path.join(source, path)))
|
||||||
destination_ = os.path.abspath(
|
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_):
|
if os.path.isdir(source_):
|
||||||
try:
|
try:
|
||||||
|
|
@ -75,6 +82,7 @@ def copy(path, source, destination, destination_path=None, overwrite=False):
|
||||||
shutil.copy(source_, destination_)
|
shutil.copy(source_, destination_)
|
||||||
info('copying %s to %s' % (source_, destination_))
|
info('copying %s to %s' % (source_, destination_))
|
||||||
|
|
||||||
|
|
||||||
def clean_output_dir(path):
|
def clean_output_dir(path):
|
||||||
"""Remove all the files from the output directory"""
|
"""Remove all the files from the output directory"""
|
||||||
|
|
||||||
|
|
@ -102,7 +110,8 @@ def truncate_html_words(s, num, end_text='...'):
|
||||||
length = int(num)
|
length = int(num)
|
||||||
if length <= 0:
|
if length <= 0:
|
||||||
return u''
|
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
|
# Set up regular expressions
|
||||||
re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
|
re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
|
||||||
|
|
@ -140,8 +149,9 @@ def truncate_html_words(s, num, end_text='...'):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
|
# SGML: An end tag closes, back to the matching start tag,
|
||||||
open_tags = open_tags[i+1:]
|
# all unclosed intervening start tags with omitted end tags
|
||||||
|
open_tags = open_tags[i + 1:]
|
||||||
else:
|
else:
|
||||||
# Add it to the start of the open tags list
|
# Add it to the start of the open tags list
|
||||||
open_tags.insert(0, tagname)
|
open_tags.insert(0, tagname)
|
||||||
|
|
@ -159,13 +169,11 @@ def truncate_html_words(s, num, end_text='...'):
|
||||||
|
|
||||||
|
|
||||||
def process_translations(content_list):
|
def process_translations(content_list):
|
||||||
""" Finds all translation and returns
|
""" Finds all translation and returns tuple with two lists (index,
|
||||||
tuple with two lists (index, translations).
|
translations). Index list includes items in default language or items
|
||||||
Index list includes items in default language
|
which have no variant in default language.
|
||||||
or items which have no variant in default language.
|
|
||||||
|
|
||||||
Also, for each content_list item, it
|
Also, for each content_list item, it sets attribute 'translations'
|
||||||
sets attribute 'translations'
|
|
||||||
"""
|
"""
|
||||||
content_list.sort(key=attrgetter('slug'))
|
content_list.sort(key=attrgetter('slug'))
|
||||||
grouped_by_slugs = groupby(content_list, 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:
|
for slug, items in grouped_by_slugs:
|
||||||
items = list(items)
|
items = list(items)
|
||||||
# find items with default language
|
# find items with default language
|
||||||
default_lang_items = filter(
|
default_lang_items = filter(attrgetter('in_default_lang'), items)
|
||||||
attrgetter('in_default_lang'),
|
|
||||||
items
|
|
||||||
)
|
|
||||||
len_ = len(default_lang_items)
|
len_ = len(default_lang_items)
|
||||||
if len_ > 1:
|
if len_ > 1:
|
||||||
warning(u'there are %s variants of "%s"' % (len_, slug))
|
warning(u'there are %s variants of "%s"' % (len_, slug))
|
||||||
|
|
@ -188,7 +193,7 @@ def process_translations(content_list):
|
||||||
default_lang_items = items[:1]
|
default_lang_items = items[:1]
|
||||||
|
|
||||||
if not slug:
|
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)
|
index.extend(default_lang_items)
|
||||||
translations.extend(filter(
|
translations.extend(filter(
|
||||||
lambda x: x not in default_lang_items,
|
lambda x: x not in default_lang_items,
|
||||||
|
|
@ -205,9 +210,6 @@ LAST_MTIME = 0
|
||||||
def files_changed(path, extensions):
|
def files_changed(path, extensions):
|
||||||
"""Return True if the files have changed since the last check"""
|
"""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):
|
def file_times(path):
|
||||||
"""Return the last time files have been modified"""
|
"""Return the last time files have been modified"""
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
|
|
@ -222,3 +224,15 @@ def files_changed(path, extensions):
|
||||||
LAST_MTIME = mtime
|
LAST_MTIME = mtime
|
||||||
return True
|
return True
|
||||||
return False
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from codecs import open
|
from codecs import open
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import locale
|
import locale
|
||||||
|
import re
|
||||||
|
|
||||||
from feedgenerator import Atom1Feed, Rss201rev2Feed
|
from feedgenerator import Atom1Feed, Rss201rev2Feed
|
||||||
from pelican.utils import get_relative_path
|
|
||||||
from pelican.paginator import Paginator
|
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):
|
class Writer(object):
|
||||||
|
|
@ -28,22 +28,23 @@ class Writer(object):
|
||||||
description=context.get('SITESUBTITLE', ''))
|
description=context.get('SITESUBTITLE', ''))
|
||||||
return feed
|
return feed
|
||||||
|
|
||||||
|
|
||||||
def _add_item_to_the_feed(self, feed, item):
|
def _add_item_to_the_feed(self, feed, item):
|
||||||
|
|
||||||
feed.add_item(
|
feed.add_item(
|
||||||
title=item.title,
|
title=item.title,
|
||||||
link='%s/%s' % (self.site_url, item.url),
|
link='%s/%s' % (self.site_url, item.url),
|
||||||
|
unique_id='%s/%s' % (self.site_url, item.url),
|
||||||
description=item.content,
|
description=item.content,
|
||||||
categories=item.tags if hasattr(item, 'tags') else None,
|
categories=item.tags if hasattr(item, 'tags') else None,
|
||||||
author_name=getattr(item, 'author', 'John Doe'),
|
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'):
|
def write_feed(self, elements, context, filename=None, feed_type='atom'):
|
||||||
"""Generate a feed with the list of articles provided
|
"""Generate a feed with the list of articles provided
|
||||||
|
|
||||||
Return the feed. If no output_path or filename is specified, just return
|
Return the feed. If no output_path or filename is specified, just
|
||||||
the feed object.
|
return the feed object.
|
||||||
|
|
||||||
:param elements: the articles to put on the feed.
|
:param elements: the articles to put on the feed.
|
||||||
:param context: the context to get the feed metadata.
|
:param context: the context to get the feed metadata.
|
||||||
|
|
@ -54,12 +55,15 @@ class Writer(object):
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
locale.setlocale(locale.LC_ALL, 'C')
|
||||||
try:
|
try:
|
||||||
self.site_url = context.get('SITEURL', get_relative_path(filename))
|
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)
|
feed = self._create_new_feed(feed_type, context)
|
||||||
|
|
||||||
for item in elements:
|
max_items = len(elements)
|
||||||
self._add_item_to_the_feed(feed, item)
|
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:
|
if filename:
|
||||||
complete_path = os.path.join(self.output_path, 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 context: dict to pass to the templates.
|
||||||
:param relative_urls: use relative urls or absolutes ones
|
:param relative_urls: use relative urls or absolutes ones
|
||||||
:param paginated: dict of article list to paginate - must have the
|
: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
|
:param **kwargs: additional variables to pass to the templates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -111,7 +115,8 @@ class Writer(object):
|
||||||
localcontext['SITEURL'] = get_relative_path(name)
|
localcontext['SITEURL'] = get_relative_path(name)
|
||||||
|
|
||||||
localcontext.update(kwargs)
|
localcontext.update(kwargs)
|
||||||
self.update_context_contents(name, localcontext)
|
if relative_urls:
|
||||||
|
self.update_context_contents(name, localcontext)
|
||||||
|
|
||||||
# check paginated
|
# check paginated
|
||||||
paginated = paginated or {}
|
paginated = paginated or {}
|
||||||
|
|
@ -121,12 +126,12 @@ class Writer(object):
|
||||||
for key in paginated.iterkeys():
|
for key in paginated.iterkeys():
|
||||||
object_list = paginated[key]
|
object_list = paginated[key]
|
||||||
|
|
||||||
if self.settings.get('WITH_PAGINATION'):
|
if self.settings.get('DEFAULT_PAGINATION'):
|
||||||
paginators[key] = Paginator(object_list,
|
paginators[key] = Paginator(object_list,
|
||||||
self.settings.get('DEFAULT_PAGINATION'),
|
self.settings.get('DEFAULT_PAGINATION'),
|
||||||
self.settings.get('DEFAULT_ORPHANS'))
|
self.settings.get('DEFAULT_ORPHANS'))
|
||||||
else:
|
else:
|
||||||
paginators[key] = Paginator(object_list, len(object_list), 0)
|
paginators[key] = Paginator(object_list, len(object_list))
|
||||||
|
|
||||||
# generated pages, and write
|
# generated pages, and write
|
||||||
for page_num in range(paginators.values()[0].num_pages):
|
for page_num in range(paginators.values()[0].num_pages):
|
||||||
|
|
@ -134,16 +139,17 @@ class Writer(object):
|
||||||
paginated_name = name
|
paginated_name = name
|
||||||
for key in paginators.iterkeys():
|
for key in paginators.iterkeys():
|
||||||
paginator = paginators[key]
|
paginator = paginators[key]
|
||||||
page = paginator.page(page_num+1)
|
page = paginator.page(page_num + 1)
|
||||||
paginated_localcontext.update({'%s_paginator' % key: paginator,
|
paginated_localcontext.update(
|
||||||
'%s_page' % key: page})
|
{'%s_paginator' % key: paginator,
|
||||||
|
'%s_page' % key: page})
|
||||||
if page_num > 0:
|
if page_num > 0:
|
||||||
ext = '.' + paginated_name.rsplit('.')[-1]
|
ext = '.' + paginated_name.rsplit('.')[-1]
|
||||||
paginated_name = paginated_name.replace(ext,
|
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,
|
_write_file(template, paginated_localcontext, self.output_path,
|
||||||
paginated_name)
|
paginated_name)
|
||||||
else:
|
else:
|
||||||
# no pagination
|
# no pagination
|
||||||
_write_file(template, localcontext, self.output_path, name)
|
_write_file(template, localcontext, self.output_path, name)
|
||||||
|
|
@ -154,8 +160,8 @@ class Writer(object):
|
||||||
relative paths.
|
relative paths.
|
||||||
|
|
||||||
:param name: name of the file to output.
|
:param name: name of the file to output.
|
||||||
:param context: dict that will be passed to the templates, which need to
|
:param context: dict that will be passed to the templates, which need
|
||||||
be updated.
|
to be updated.
|
||||||
"""
|
"""
|
||||||
def _update_content(name, input):
|
def _update_content(name, input):
|
||||||
"""Change all the relatives paths of the input content to relatives
|
"""Change all the relatives paths of the input content to relatives
|
||||||
|
|
@ -166,24 +172,26 @@ class Writer(object):
|
||||||
"""
|
"""
|
||||||
content = input._content
|
content = input._content
|
||||||
|
|
||||||
hrefs = re.compile(r'<\s*[^\>]*href\s*=(^!#)\s*(["\'])(.*?)\1')
|
hrefs = re.compile(r"""
|
||||||
srcs = re.compile(r'<\s*[^\>]*src\s*=\s*(["\'])(.*?)\1')
|
(?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)
|
def replacer(m):
|
||||||
matches.extend(srcs.findall(content))
|
relative_path = m.group('path')
|
||||||
relative_paths = []
|
dest_path = os.path.normpath(
|
||||||
for found in matches:
|
os.sep.join((get_relative_path(name), "static",
|
||||||
found = found[1]
|
relative_path)))
|
||||||
if found not in relative_paths:
|
|
||||||
relative_paths.append(found)
|
|
||||||
|
|
||||||
for relative_path in relative_paths:
|
return m.group('markup') + m.group('quote') + dest_path \
|
||||||
if not ":" in relative_path: # we don't want to rewrite protocols
|
+ m.group('quote')
|
||||||
dest_path = os.sep.join((get_relative_path(name), "static",
|
|
||||||
relative_path))
|
|
||||||
content = content.replace(relative_path, dest_path)
|
|
||||||
|
|
||||||
return content
|
return hrefs.sub(replacer, content)
|
||||||
|
|
||||||
if context is None:
|
if context is None:
|
||||||
return
|
return
|
||||||
|
|
@ -203,4 +211,4 @@ class Writer(object):
|
||||||
if relative_path not in paths:
|
if relative_path not in paths:
|
||||||
paths.append(relative_path)
|
paths.append(relative_path)
|
||||||
setattr(item, "_get_content",
|
setattr(item, "_get_content",
|
||||||
partial(_update_content, name, item))
|
partial(_update_content, name, item))
|
||||||
|
|
|
||||||
9
samples/content/article2-fr.rst
Normal file
9
samples/content/article2-fr.rst
Normal 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.
|
||||||
9
samples/content/article2.rst
Normal file
9
samples/content/article2.rst
Normal 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
|
||||||
|
|
@ -26,7 +26,7 @@ And here comes the cool stuff_.
|
||||||
:width: 600 px
|
:width: 600 px
|
||||||
:alt: alternate text
|
:alt: alternate text
|
||||||
|
|
||||||
.. code-block:: python
|
::
|
||||||
|
|
||||||
>>> from ipdb import set_trace
|
>>> from ipdb import set_trace
|
||||||
>>> set_trace()
|
>>> set_trace()
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
AUTHOR = u'Alexis Métaireau'
|
AUTHOR = u'Alexis Métaireau'
|
||||||
SITENAME = u"Alexis' log"
|
SITENAME = u"Alexis' log"
|
||||||
SITEURL = 'http://blog.notmyidea.org'
|
SITEURL = 'http://blog.notmyidea.org'
|
||||||
|
TIMEZONE = "Europe/Paris"
|
||||||
|
|
||||||
GITHUB_URL = 'http://github.com/ametaireau/'
|
GITHUB_URL = 'http://github.com/ametaireau/'
|
||||||
DISQUS_SITENAME = "blog-notmyidea"
|
DISQUS_SITENAME = "blog-notmyidea"
|
||||||
PDF_GENERATOR = False
|
PDF_GENERATOR = False
|
||||||
REVERSE_CATEGORY_ORDER = True
|
REVERSE_CATEGORY_ORDER = True
|
||||||
LOCALE = ""
|
LOCALE = ""
|
||||||
DEFAULT_PAGINATION = 2
|
DEFAULT_PAGINATION = 4
|
||||||
|
|
||||||
FEED_RSS = 'feeds/all.rss.xml'
|
FEED_RSS = 'feeds/all.rss.xml'
|
||||||
CATEGORY_FEED_RSS = 'feeds/%s.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
|
# foobar will not be used, because it's not in caps. All configuration keys
|
||||||
# have to be in caps
|
# have to be in caps
|
||||||
foobar = "barbaz"
|
foobar = "barbaz"
|
||||||
|
|
||||||
|
|
|
||||||
27
setup.py
27
setup.py
|
|
@ -1,25 +1,34 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from setuptools import setup
|
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']
|
try:
|
||||||
if sys.version_info < (2,7):
|
import argparse
|
||||||
|
except ImportError:
|
||||||
requires.append('argparse')
|
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(
|
setup(
|
||||||
name = "pelican",
|
name = "pelican",
|
||||||
version = VERSION,
|
version = "3.0",
|
||||||
url = 'http://alexis.notmyidea.org/pelican/',
|
url = 'http://pelican.notmyidea.org/',
|
||||||
author = 'Alexis Metaireau',
|
author = 'Alexis Metaireau',
|
||||||
author_email = 'alexis@notmyidea.org',
|
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(),
|
long_description=open('README.rst').read(),
|
||||||
packages = ['pelican', 'pelican.plugins'],
|
packages = ['pelican', 'pelican.tools', 'pelican.plugins'],
|
||||||
include_package_data = True,
|
include_package_data = True,
|
||||||
install_requires = requires,
|
install_requires = requires,
|
||||||
scripts = ['bin/pelican', 'tools/pelican-themes'],
|
entry_points = entry_points,
|
||||||
classifiers = ['Development Status :: 5 - Production/Stable',
|
classifiers = ['Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'License :: OSI Approved :: GNU Affero General Public License v3',
|
'License :: OSI Approved :: GNU Affero General Public License v3',
|
||||||
|
|
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
4
tests/content/article.rst
Normal file
4
tests/content/article.rst
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Article title
|
||||||
|
#############
|
||||||
|
|
||||||
|
This is some content. With some stuff to "typogrify".
|
||||||
12
tests/content/article_with_metadata.rst
Normal file
12
tests/content/article_with_metadata.rst
Normal 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
|
||||||
6
tests/content/article_with_uppercase_metadata.rst
Normal file
6
tests/content/article_with_uppercase_metadata.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
This is a super article !
|
||||||
|
#########################
|
||||||
|
|
||||||
|
:Category: Yeah
|
||||||
|
|
||||||
39
tests/default_conf.py
Normal file
39
tests/default_conf.py
Normal 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
26
tests/support.py
Normal 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
129
tests/test_contents.py
Normal 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
28
tests/test_generators.py
Normal 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
31
tests/test_pelican.py
Normal 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
65
tests/test_readers.py
Normal 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 "\
|
||||||
|
""typogrify".</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 "\
|
||||||
|
"“typogrify”.</p>\n"
|
||||||
|
|
||||||
|
self.assertEqual(content, expected)
|
||||||
|
except ImportError:
|
||||||
|
return unittest.skip('need the typogrify distribution')
|
||||||
37
tests/test_settings.py
Normal file
37
tests/test_settings.py
Normal 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
93
tests/test_utils.py
Normal 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)
|
||||||
|
|
@ -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
13
tox.ini
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[tox]
|
||||||
|
envlist = py26,py27
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
commands = nosetests -s tests
|
||||||
|
deps =
|
||||||
|
nose
|
||||||
|
Jinja2
|
||||||
|
Pygments
|
||||||
|
docutils
|
||||||
|
feedgenerator
|
||||||
|
unittest2
|
||||||
|
mock
|
||||||
Loading…
Add table
Add a link
Reference in a new issue