diff --git a/.gitignore b/.gitignore
index 9f9404ef..1ae0e9f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ tags
.tox
.coverage
htmlcov
+six-*.egg/
+*.orig
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..8dd40fb6
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,24 @@
+Alexis Métaireau
+Alexis Métaireau
+Alexis Métaireau
+Axel Haustant
+Axel Haustant
+Dave Mankoff
+Feth Arezki
+Guillaume
+Guillaume
+Guillaume B
+Guillermo López
+Guillermo López
+Jomel Imperio
+Justin Mayer
+Justin Mayer
+Marco Milanesi
+Massimo Santini
+Rémy HUBSCHER
+Simon Conseil
+Simon Liedtke
+Skami18
+Stuart Colville
+Stéphane Bunel
+tBunnyMan
diff --git a/.travis.yml b/.travis.yml
index bb9a22e4..1ff512b6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,14 @@
language: python
python:
- - "2.6"
- "2.7"
+ - "3.3"
before_install:
- sudo apt-get update -qq
- - sudo apt-get install -qq ruby-sass
+ - sudo apt-get install -qq --no-install-recommends asciidoc
+ - sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
install:
- - pip install nose unittest2 mock --use-mirrors
+ - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then ln -s /usr/share/asciidoc/asciidocapi.py ~/virtualenv/python2.7/lib/python2.7/site-packages/; fi
+ - pip install mock --use-mirrors
- pip install . --use-mirrors
- - pip install Markdown
- - pip install webassets
- - pip install cssmin
-script: nosetests -s tests
-notifications:
- irc:
- channels:
- - "irc.freenode.org#pelican"
- on_success: change
+ - pip install --use-mirrors Markdown
+script: python -m unittest discover
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 00000000..b28b22a3
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,40 @@
+Contribution submission guidelines
+==================================
+
+* Consider whether your new feature might be better suited as a plugin_. Folks
+ are usually available in the `#pelican IRC channel`_ if help is needed to
+ make that determination.
+* `Create a new git branch`_ specific to your change (as opposed to making
+ your commits in the master branch).
+* **Don't put multiple fixes/features in the same branch / pull request.**
+ For example, if you're hacking on a new feature and find a bugfix that
+ doesn't *require* your new feature, **make a new distinct branch and pull
+ request** for the bugfix.
+* Adhere to PEP8 coding standards whenever possible.
+* Check for unnecessary whitespace via ``git diff --check`` before committing.
+* **Add docs and tests for your changes**.
+* `Run all the tests`_ **on both Python 2.7 and 3.3** to ensure nothing was
+ accidentally broken.
+* First line of your commit message should start with present-tense verb, be 50
+ characters or less, and include the relevant issue number(s) if applicable.
+ *Example:* ``Ensure proper PLUGIN_PATH behavior. Refs #428.`` If the commit
+ *completely fixes* an existing bug report, please use ``Fixes #585`` or ``Fix
+ #585`` syntax (so the relevant issue is automatically closed upon PR merge).
+* After the first line of the commit message, add a blank line and then a more
+ detailed explanation (when relevant).
+* If you have previously filed a GitHub issue and want to contribute code that
+ addresses that issue, **please use** ``hub pull-request`` instead of using
+ GitHub's web UI to submit the pull request. This isn't an absolute
+ requirement, but makes the maintainers' lives much easier! Specifically:
+ `install hub `_ and then run
+ `hub pull-request `_ to
+ turn your GitHub issue into a pull request containing your code.
+
+Check out our `Git Tips`_ page or ask on the `#pelican IRC channel`_ if you
+need assistance or have any questions about these guidelines.
+
+.. _`plugin`: http://docs.getpelican.com/en/latest/plugins.html
+.. _`#pelican IRC channel`: http://webchat.freenode.net/?channels=pelican&uio=d4
+.. _`Create a new git branch`: https://github.com/getpelican/pelican/wiki/Git-Tips#making-your-changes
+.. _`Run all the tests`: http://docs.getpelican.com/en/latest/contribute.html#running-the-test-suite
+.. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips
diff --git a/MANIFEST.in b/MANIFEST.in
index 2f2ea824..136243c0 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,4 @@
include *.rst
global-include *.py
-recursive-include pelican *.html *.css *png *.in
+recursive-include pelican *.html *.css *png *.in *.rst *.md *.mkd *.xml
include LICENSE THANKS docs/changelog.rst
-recursive-include tests *
-recursive-exclude tests *.pyc
diff --git a/README.rst b/README.rst
index b2648bf1..20c3f217 100644
--- a/README.rst
+++ b/README.rst
@@ -2,7 +2,7 @@ Pelican
=======
.. image:: https://secure.travis-ci.org/getpelican/pelican.png?branch=master
- :target: http://travis-ci.org/#!/getpelican/pelican
+ :target: http://travis-ci.org/getpelican/pelican
:alt: Travis-ci: continuous integration status.
Pelican is a static site generator, written in Python_.
@@ -27,7 +27,6 @@ Pelican currently supports:
* Publication of articles in multiple languages
* Atom/RSS feeds
* Code syntax highlighting
-* Asset management with `webassets`_ (optional)
* Import from WordPress, Dotclear, or RSS feeds
* Integration with external tools: Twitter, Google Analytics, etc. (optional)
@@ -67,4 +66,3 @@ client handy, use the webchat_ for quick feedback.
.. _`#pelican on Freenode`: irc://irc.freenode.net/pelican
.. _webchat: http://webchat.freenode.net/?channels=pelican&uio=d4
.. _contribute: http://docs.getpelican.com/en/latest/contribute.html
-.. _webassets: https://github.com/miracle2k/webassets
diff --git a/THANKS b/THANKS
index eeaf2309..e4eed231 100644
--- a/THANKS
+++ b/THANKS
@@ -1,10 +1,10 @@
-Pelican is a project by Alexis Métaireau but there is
-a quite big number of people that contributed or implemented key features over
-time. We try to keep this list up to date, but you can have a look at the nice
-graphs proposed by github about contributors here:
-https://github.com/getpelican/pelican/graphs/contributors
+Pelican is a project originally created by Alexis Métaireau
+, but there are a large number of people that have
+contributed or implemented key features over time. We do our best to keep this
+list up-to-date, but you can also have a look at the nice contributor graphs
+produced by GitHub: https://github.com/getpelican/pelican/graphs/contributors
-If you want to contibute, check the documentation section about how to do so
+If you want to contibute, check the documentation section about how to do so:
Aaron Kavlie
@@ -20,8 +20,11 @@ Alexis Métaireau
Allan Whatmough
Andrea Crotti
Andrew Laski
+Andrew Spiers
Arnaud BOS
asselinpaul
+Axel Haustant
+Benoît HERVIER
Borgar
Brandon W Maister
Brendan Wholihan
@@ -30,19 +33,26 @@ Brian Hsu
Brian St. Pierre
Bruno Binet
BunnyMan
+Chenguang Wang
+Chris Elston
+Chris McDonald (Wraithan)
Chris Streeter
Christophe Chauvet
Clint Howarth
+Colin Dunklau
Dafydd Crosby
Dana Woodman
-dave mankoff
+Dave King
+Dave Mankoff
David Beitey
David Marble
+Deniz Turgut (Avaris)
derdon
Dirkjan Ochtman
Dirk Makowski
draftcode
Edward Delaporte
+Emily Strickland
epatters
Eric Case
Erik Hetzner
@@ -50,31 +60,43 @@ FELD Boris
Feth Arezki
Florian Jacob
Florian Preinstorfer
+Félix Delval
Freeculture
+George V. Reilly
Guillaume
Guillaume B
-Guillermo López
+Guillermo López
guillermooo
Ian Cordasco
+Igor Kalnitsky
+Irfan Ahmad
Iuri de Silvio
+Ivan Dyedov
+James King
James Rowe
jawher
+Jered Boxman
Jerome
Jiachen Yang
Jochen Breuer
joe di castro
+John Kristensen
+John Mastro
Jökull Sólberg Auðunsson
+Jomel Imperio
+Joseph Reagle
Joshua Adelman
Julian Berman
-justinmayer
Justin Mayer
Kyle Fuller
Laureline Guerin
Leonard Huang
+Leroy Jiang
Marcel Hellkamp
Marco Milanesi
Marcus Fredriksson
Mario Rodas
+Mark Caudill
Martin Brochhaus
Massimo Santini
Matt Bowcock
@@ -91,32 +113,45 @@ Nico Di Rocco
Nicolas Duhamel
Nicolas Perriault
Nicolas Steinmetz
+Paul Asselin
Pavel Puchkin
Perry Roper
+Peter Desmet
Philippe Pepiot
Rachid Belaid
+Randall Degges
Ranjhith Kalisamy
Remi Rampin
Rémy HUBSCHER
renhbo
+Richard Duivenvoorde
+Rogdham
Roman Skvazh
Ronny Pfannschmidt
Rory McCann
+Rıdvan Örsvuran
saghul
sam
Samrat Man Singh
+Simon Conseil
Simon Liedtke
Skami18
solsTiCe d'Hiver
+Steve Schwarz
Stéphane Bunel
Stéphane Raimbault
Stuart Colville
+Talha Mansoor
Tarek Ziade
Thanos Lefteris
-the Bunny Man
+Thomas Thurman
Tobias
Tomi Pieviläinen
Trae Blain
Tshepang Lekhonkhobe
+Valentin-Costel Hăloiu
+Vlad Niculae
+William Light
Wladislaw Merezhko
+W. Trevor King
Zoresvit
diff --git a/dev_requirements.txt b/dev_requirements.txt
index acf01773..fa2634a0 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,8 +1,8 @@
# Tests
-unittest2
mock
+
# Optional Packages
Markdown
-BeautifulSoup
+BeautifulSoup4
+lxml
typogrify
-webassets
\ No newline at end of file
diff --git a/docs/changelog.rst b/docs/changelog.rst
index e29c94c0..fcec0b53 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,10 +1,28 @@
Release history
###############
-3.2 (XXXX-XX-XX)
+3.3 (XXXX-XX-XX)
================
-* [...]
+3.2 (2013-04-24)
+================
+
+* Support for Python 3!
+* Override page save-to location from meta-data (enables using a static page as
+ the site's home page, for example)
+* Time period archives (per-year, per-month, and per-day archives of posts)
+* Posterous blog import
+* Improve WordPress blog import
+* Migrate plugins to separate repository
+* Improve HTML parser
+* Provide ability to show or hide categories from menu using
+ ``DISPLAY_CATEGORIES_ON_MENU`` option
+* Auto-regeneration can be told to ignore files via ``IGNORE_FILES`` setting
+* Improve post-generation feedback to user
+* For multilingual posts, use meta-data to designate which is the original
+ and which is the translation
+* Add ``.mdown`` to list of supported Markdown file extensions
+* Document-relative URL generation (``RELATIVE_URLS``) is now off by default
3.1 (2012-12-04)
================
diff --git a/docs/conf.py b/docs/conf.py
index 2a11fe3e..40de84c7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
import sys, os
-sys.path.append(os.path.abspath('..'))
+sys.path.append(os.path.abspath(os.pardir))
from pelican import __version__, __major__
@@ -10,8 +11,8 @@ templates_path = ['_templates']
extensions = ['sphinx.ext.autodoc',]
source_suffix = '.rst'
master_doc = 'index'
-project = u'Pelican'
-copyright = u'2010, Alexis Metaireau and contributors'
+project = 'Pelican'
+copyright = '2010, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
version = __version__
release = __major__
@@ -34,16 +35,16 @@ htmlhelp_basename = 'Pelicandoc'
# -- Options for LaTeX output --------------------------------------------------
latex_documents = [
- ('index', 'Pelican.tex', u'Pelican Documentation',
- u'Alexis Métaireau', 'manual'),
+ ('index', 'Pelican.tex', 'Pelican Documentation',
+ 'Alexis Métaireau', 'manual'),
]
# -- Options for manual page output --------------------------------------------
man_pages = [
- ('index', 'pelican', u'pelican documentation',
- [u'Alexis Métaireau'], 1),
- ('pelican-themes', 'pelican-themes', u'A theme manager for Pelican',
- [u'Mickaël Raybaud'], 1),
- ('themes', 'pelican-theming', u'How to create themes for Pelican',
- [u'The Pelican contributors'], 1)
+ ('index', 'pelican', 'pelican documentation',
+ ['Alexis Métaireau'], 1),
+ ('pelican-themes', 'pelican-themes', 'A theme manager for Pelican',
+ ['Mickaël Raybaud'], 1),
+ ('themes', 'pelican-theming', 'How to create themes for Pelican',
+ ['The Pelican contributors'], 1)
]
diff --git a/docs/contribute.rst b/docs/contribute.rst
index c302dcc6..80d07644 100644
--- a/docs/contribute.rst
+++ b/docs/contribute.rst
@@ -1,29 +1,34 @@
-How to contribute?
-###################
-There are many ways to contribute to Pelican. You can enhance the
-documentation, add missing features, and fix bugs (or just report them).
+How to contribute
+#################
-Don't hesitate to fork and make a pull request on GitHub. When doing so, please
-create a new feature branch as opposed to making your commits in the master
-branch.
+There are many ways to contribute to Pelican. You can improve the
+documentation, add missing features, and fix bugs (or just report them). You
+can also help out by reviewing and commenting on
+`existing issues `_.
+
+Don't hesitate to fork Pelican and submit a pull request on GitHub. When doing
+so, please adhere to the following guidelines.
+
+.. include:: ../CONTRIBUTING.rst
Setting up the development environment
======================================
-You're free to set up your development environment any way you like. Here is a
-way using the `virtualenv `_ and `virtualenvwrapper
-`_ tools. If you don't
-have them, you can install these both of these packages via::
+While there are many ways to set up one's development environment, following
+is a method that uses `virtualenv `_. If you don't
+have ``virtualenv`` installed, you can install it via::
- $ pip install virtualenvwrapper
+ $ pip install virtualenv
Virtual environments allow you to work on Python projects which are isolated
from one another so you can use different packages (and package versions) with
different projects.
-To create a virtual environment, use the following syntax::
+To create and activate a virtual environment, use the following syntax::
- $ mkvirtualenv pelican
+ $ virtualenv ~/virtualenvs/pelican
+ $ cd ~/virtualenvs/pelican
+ $ . bin/activate
To clone the Pelican source::
@@ -38,32 +43,102 @@ To install Pelican and its dependencies::
$ python setup.py develop
-Running the test suite
-======================
+Or using ``pip``::
-Each time you add a feature, there are two things to do regarding tests:
-checking that the existing tests pass, and adding tests for the new feature
-or bugfix.
-
-The tests live in "pelican/tests" and you can run them using the
-"discover" feature of unittest2::
-
- $ unit2 discover
-
-If you have made changes that affect the output of a Pelican-generated weblog,
-then you should update the output used by functional tests.
-To do so, you can use the following two commands::
-
- $ LC_ALL="C" pelican -o tests/output/custom/ -s samples/pelican.conf.py \
- samples/content/
- $ LC_ALL="C" pelican -o tests/output/basic/ samples/content/
+ $ pip install -e .
Coding standards
================
Try to respect what is described in the `PEP8 specification
-`_ when providing patches. This can be
-eased via the `pep8 `_ or `flake8
+`_ when making contributions. This
+can be eased via the `pep8 `_ or `flake8
`_ tools, the latter of which in
particular will give you some useful hints about ways in which the
code/formatting can be improved.
+
+Building the docs
+=================
+
+If you make changes to the documentation, you should preview your changes
+before committing them::
+
+ $ pip install sphinx
+ $ cd src/pelican/docs
+ $ make html
+
+Open ``_build/html/index.html`` in your browser to preview the documentation.
+
+Running the test suite
+======================
+
+Each time you add a feature, there are two things to do regarding tests:
+check that the existing tests pass, and add tests for the new feature
+or bugfix.
+
+The tests live in ``pelican/tests`` and you can run them using the
+"discover" feature of ``unittest``::
+
+ $ python -m unittest discover
+
+After making your changes and running the tests, you may see a test failure
+mentioning that "some generated files differ from the expected functional tests
+output." If you have made changes that affect the HTML output generated by
+Pelican, and the changes to that output are expected and deemed correct given
+the nature of your changes, then you should update the output used by the
+functional tests. To do so, you can use the following two commands::
+
+ $ pelican -o pelican/tests/output/custom/ -s samples/pelican.conf.py \
+ samples/content/
+ $ pelican -o pelican/tests/output/basic/ samples/content/
+
+Testing on Python 2 and 3
+-------------------------
+
+Testing on Python 3 currently requires some extra steps: installing
+Python 3-compatible versions of dependent packages and plugins.
+
+Tox_ is a useful tool to run tests on both versions. It will install the
+Python 3-compatible version of dependent packages.
+
+.. _Tox: http://testrun.org/tox/latest/
+
+Python 3 development tips
+=========================
+
+Here are some tips that may be useful when doing some code for both Python 2.7
+and Python 3 at the same time:
+
+- Assume every string and literal is unicode (import unicode_literals):
+
+ - Do not use prefix ``u'``.
+ - Do not encode/decode strings in the middle of sth. Follow the code to the
+ source (or target) of a string and encode/decode at the first/last possible
+ point.
+ - In other words, write your functions to expect and to return unicode.
+ - Encode/decode strings if e.g. the source is a Python function that is known
+ to handle this badly, e.g. strftime() in Python 2.
+
+- Use new syntax: print function, "except ... *as* e" (not comma) etc.
+- Refactor method calls like ``dict.iteritems()``, ``xrange()`` etc. in a way
+ that runs without code change in both Python versions.
+- Do not use magic method ``__unicode()__`` in new classes. Use only ``__str()__``
+ and decorate the class with ``@python_2_unicode_compatible``.
+- Do not start int literals with a zero. This is a syntax error in Py3k.
+- Unfortunately I did not find an octal notation that is valid in both
+ Pythons. Use decimal instead.
+- use six, e.g.:
+
+ - ``isinstance(.., basestring) -> isinstance(.., six.string_types)``
+ - ``isinstance(.., unicode) -> isinstance(.., six.text_type)``
+
+- ``setlocale()`` in Python 2 bails when we give the locale name as unicode,
+ and since we are using ``from __future__ import unicode_literals``, we do
+ that everywhere! As a workaround, I enclosed the localename with ``str()``;
+ in Python 2 this casts the name to a byte string, in Python 3 this should do
+ nothing, because the locale name already had been unicode.
+
+- Kept range() almost everywhere as-is (2to3 suggests list(range())), just
+ changed it where I felt necessary.
+
+- Changed xrange() back to range(), so it is valid in both Python versions.
diff --git a/docs/faq.rst b/docs/faq.rst
index a8617b30..a8043e07 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -10,32 +10,34 @@ If you have a problem, question, or suggestion, please start by striking up a
conversation on `#pelican on Freenode `_.
Those who don't have an IRC client handy can jump in immediately via
`IRC webchat `_. Because
-of differing time zones, you may not get an immediate response to your question,
-but please be patient and stay logged into IRC — someone will almost always
-respond.
+of differing time zones, you may not get an immediate response to your
+question, but please be patient and stay logged into IRC — someone will almost
+always respond if you wait long enough (it may take a few hours).
-If you are unable to resolve your issue or if you have a feature request, please
+If you're unable to resolve your issue or if you have a feature request, please
refer to the `issue tracker `_.
How can I help?
================
-There are several ways to help out. First, you can use Pelican and report any
+There are several ways to help out. First, you can report any Pelican
suggestions or problems you might have via IRC or the `issue tracker
-`_.
+`_. If submitting an issue
+report, please check the existing issue list first in order to avoid submitting
+a duplicate issue.
If you want to contribute, please fork `the git repository
`_, create a new feature branch, make
-your changes, and issue a pull request. Someone will review your changes as soon
-as possible. Please refer to the :doc:`How to Contribute ` section
-for more details.
+your changes, and issue a pull request. Someone will review your changes as
+soon as possible. Please refer to the :doc:`How to Contribute `
+section for more details.
You can also contribute by creating themes and improving the documentation.
Is it mandatory to have a configuration file?
=============================================
-No, it's not. Configuration files are just an easy way to configure Pelican.
+Configuration files are optional and are just an easy way to configure Pelican.
For basic operations, it's possible to specify options while invoking Pelican
via the command line. See ``pelican --help`` for more information.
@@ -44,10 +46,19 @@ I'm creating my own theme. How do I use Pygments for syntax highlighting?
Pygments adds some classes to the generated content. These classes are used by
themes to style code syntax highlighting via CSS. Specifically, you can
-customize the appearance of your syntax highlighting via the ``.codehilite pre``
+customize the appearance of your syntax highlighting via the ``.highlight 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
-`_.
+Django code, for example, use the style selector drop-down at top-right on the
+`Pygments project demo site `_.
+
+You can use the following example commands to generate a starting CSS file from
+a Pygments built-in style (in this case, "monokai") and then copy the generated
+CSS file to your new theme::
+
+ pygmentize -S monokai -f html -a .highlight > pygment.css
+ cp pygment.css path/to/theme/static/css/
+
+Don't forget to import your ``pygment.css`` file from your main CSS file.
How do I create my own theme?
==============================
@@ -58,16 +69,16 @@ 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 the following, including ``sudo`` if
-required::
+install it. You can do so by typing the following command, prepending ``sudo``
+if permissions require it::
- (sudo) pip install markdown
+ pip install markdown
-If you don't have pip installed, consider installing the pip installer via::
+If you don't have ``pip`` installed, consider installing it via::
- (sudo) easy_install pip
+ easy_install pip
-Can I use arbitrary meta-data in my templates?
+Can I use arbitrary metadata in my templates?
==============================================
Yes. For example, to include a modified date in a Markdown post, one could
@@ -75,7 +86,11 @@ include the following at the top of the article::
Modified: 2012-08-08
-That meta-data can then be accessed in the template::
+For reStructuredText, this metadata should of course be prefixed with a colon::
+
+ :Modified: 2012-08-08
+
+This metadata can then be accessed in the template::
{% if article.modified %}
Last modified: {{ article.modified }}
@@ -84,23 +99,57 @@ That meta-data can then be accessed in the template::
How do I assign custom templates on a per-page basis?
=====================================================
-It's as simple as adding an extra line of metadata to any pages or articles you
-want to have its own template.
+It's as simple as adding an extra line of metadata to any page or article that
+you want to have its own template. For example, this is how it would be handled
+for content in reST format::
:template: template_name
+For content in Markdown format::
+
+ Template: template_name
+
Then just make sure your theme contains the relevant template file (e.g.
``template_name.html``).
+How can I override the generated URL of a specific page or article?
+===================================================================
+
+Include ``url`` and ``save_as`` metadata in any pages or articles that you want
+to override the generated URL. Here is an example page in reST format::
+
+ Override url/save_as page
+ #########################
+
+ :url: override/url/
+ :save_as: override/url/index.html
+
+With this metadata, the page will be written to ``override/url/index.html``
+and Pelican will use url ``override/url/`` to link to this page.
+
+How can I use a static page as my home page?
+============================================
+
+The override feature mentioned above can be used to specify a static page as
+your home page. The following Markdown example could be stored in
+``content/pages/home.md``::
+
+ Title: Welcome to My Site
+ URL:
+ save_as: index.html
+
+ Thank you for visiting. Welcome!
+
What if I want to disable feed generation?
==========================================
-To disable all feed generation, all feed settings should be set to ``None``.
-All but two feed settings already default to ``None``, so if you want to disable
-all feed generation, you only need to specify the following settings::
+To disable feed generation, all feed settings should be set to ``None``.
+All but three feed settings already default to ``None``, so if you want to
+disable all feed generation, you only need to specify the following settings::
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
+ TRANSLATION_FEED_ATOM = None
Please note that ``None`` and ``''`` are not the same thing. The word ``None``
should not be surrounded by quotes.
@@ -108,23 +157,20 @@ should not be surrounded by quotes.
I'm getting a warning about feeds generated without SITEURL being set properly
==============================================================================
-`RSS and Atom feeds require all URLs and links in them to be absolute
+`RSS and Atom feeds require all URL links to be absolute
`_.
-In order to properly generate all URLs properly in Pelican you will need to set
-``SITEURL`` to the full path of your blog. When using ``make html`` and the
-default Makefile provided by the `pelican-quickstart` bootstrap script to test
-build your site, it's normal to see this warning since ``SITEURL`` is
-deliberately left undefined. If configured properly no other ``make`` commands
-should result in this warning.
+In order to properly generate links in Pelican you will need to set ``SITEURL``
+to the full path of your site.
-Feeds are still generated when this warning is displayed but may not validate.
+Feeds are still generated when this warning is displayed, but links within may
+be malformed and thus the feed may not validate.
My feeds are broken since I upgraded to Pelican 3.x
===================================================
Starting in 3.0, some of the FEED setting names were changed to more explicitly
refer to the Atom feeds they inherently represent (much like the FEED_RSS
-setting names). Here is an exact list of the renamed setting names::
+setting names). Here is an exact list of the renamed settings::
FEED -> FEED_ATOM
TAG_FEED -> TAG_FEED_ATOM
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 3e527611..ddffb5ff 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -4,28 +4,34 @@ Getting started
Installing Pelican
==================
-You're ready? Let's go! You can install Pelican via several different methods.
-The simplest is via `pip `_::
+Pelican currently runs best on Python 2.7.x; earlier versions of Python are
+not supported. There is provisional support for Python 3.3, although there may
+be rough edges, particularly with regards to optional 3rd-party components.
+
+You can install Pelican via several different methods. The simplest is via
+`pip `_::
$ pip install pelican
-If you don't have ``pip`` installed, an alternative method is ``easy_install``::
+If you don't have ``pip`` installed, an alternative method is
+``easy_install``::
$ easy_install pelican
-While the above is the simplest method, the recommended approach is to create
-a virtual environment for Pelican via virtualenv_ and virtualenvwrapper_ before
-installing Pelican. Assuming you've followed the virtualenvwrapper
-`installation `_
-and `shell configuration
-`_
-steps, you can then open a new terminal session and create a new virtual
-environment for Pelican::
+(Keep in mind that operating systems will often require you to prefix the above
+commands with ``sudo`` in order to install Pelican system-wide.)
- $ mkvirtualenv pelican
+While the above is the simplest method, the recommended approach is to create
+a virtual environment for Pelican via virtualenv_ before installing Pelican.
+Assuming you have virtualenv_ installed, you can then open a new terminal
+session and create a new virtual environment for Pelican::
+
+ $ virtualenv ~/virtualenvs/pelican
+ $ cd ~/virtualenvs/pelican
+ $ . bin/activate
Once the virtual environment has been created and activated, Pelican can be
-be installed via ``pip`` or ``easy_install`` as noted above. Alternatively, if
+be installed via ``pip install pelican`` as noted above. Alternatively, if
you have the project source, you can install Pelican using the distutils
method::
@@ -46,6 +52,46 @@ If you want to use AsciiDoc you need to install it from `source
`_ or use your operating
system's package manager.
+Basic usage
+-----------
+
+Once Pelican is installed, you can use it to convert your Markdown or reST
+content into HTML via the ``pelican`` command, specifying the path to your
+content and (optionally) the path to your settings file::
+
+$ pelican /path/to/your/content/ [-s path/to/your/settings.py]
+
+The above command will generate your site and save it in the ``output/``
+folder, using the default theme to produce a simple site. The default theme
+consists of very simple HTML without styling and is provided so folks may use
+it as a basis for creating their own themes.
+
+You can also tell Pelican to watch for your modifications, instead of
+manually re-running it every time you want to see your changes. To enable this,
+run the ``pelican`` command with the ``-r`` or ``--autoreload`` option.
+
+Pelican has other command-line switches available. Have a look at the help to
+see all the options you can use::
+
+ $ pelican --help
+
+Continue reading below for more detail, and check out the Pelican wiki's
+`Tutorials `_ page for
+links to community-published tutorials.
+
+Viewing the generated files
+---------------------------
+
+The files generated by Pelican are static files, so you don't actually need
+anything special to view them. 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
+
Upgrading
---------
@@ -61,60 +107,72 @@ perform the same step to install the most recent version.
Dependencies
------------
-At this time, Pelican is dependent on the following Python packages:
+When Pelican is installed, the following dependent Python packages should be
+automatically installed without any action on your part:
-* feedgenerator, to generate the Atom feeds
-* jinja2, for templating support
-* docutils, for supporting reStructuredText as an input format
+* `feedgenerator `_, to generate the
+ Atom feeds
+* `jinja2 `_, for templating support
+* `pygments `_, for syntax highlighting
+* `docutils `_, for supporting
+ reStructuredText as an input format
+* `pytz `_, for timezone definitions
+* `blinker `_, an object-to-object and
+ broadcast signaling system
+* `unidecode `_, for ASCII
+ transliterations of Unicode text
-If you're not using Python 2.7, you will also need the ``argparse`` package.
+If you want the following optional packages, you will need to install them
+manually via ``pip``:
-Optionally:
+* `markdown `_, for supporting Markdown as
+ an input format
+* `typogrify `_, for typographical
+ enhancements
-* pygments, for syntax highlighting
-* Markdown, for supporting Markdown as an input format
-* Typogrify, for typographical enhancements
+Kickstart your site
+===================
-Kickstart a blog
-================
-
-Following is a brief tutorial for those who want to get started right away.
-We're going to assume that virtualenv_ and virtualenvwrapper_ are installed and
-configured; if you've installed Pelican outside of a virtual environment,
-you can skip to the ``pelican-quickstart`` command. Let's first create a new
-virtual environment and install Pelican into it::
-
- $ mkvirtualenv pelican
- $ pip install pelican Markdown
-
-Next we'll create a directory to house our site content and configuration files,
-which can be located any place you prefer, and associate this new project with
-the currently-active virtual environment::
-
- $ mkdir ~/code/yoursitename
- $ cd ~/code/yoursitename
- $ setvirtualenvproject
-
-Now we can run the ``pelican-quickstart`` command, which will ask some questions
-about your site::
+Once Pelican has been installed, you can create a skeleton project via the
+``pelican-quickstart`` command, which begins by asking some questions about
+your site::
$ pelican-quickstart
-Once you finish answering all the questions, you can begin adding content to the
-*content* folder that has been created for you. (See *Writing articles using
-Pelican* section below for more information about how to format your content.)
-Once you have some content to generate, you can convert it to HTML via the
-following command::
+Once you finish answering all the questions, your project will consist of the
+following hierarchy (except for "pages", which you can optionally add yourself
+if you plan to create non-chronological content)::
+
+ yourproject/
+ ├── content
+ │ └── (pages)
+ ├── output
+ ├── develop_server.sh
+ ├── Makefile
+ ├── pelicanconf.py # Main settings file
+ └── publishconf.py # Settings to use when ready to publish
+
+The next step is to begin to adding content to the *content* folder that has
+been created for you. (See *Writing articles using Pelican* section below for
+more information about how to format your content.)
+
+Once you have written some content to generate, you can use the ``pelican``
+command to generate your site, which will be placed in the output folder.
+Alternatively, you can use automation tools that "wrap" the ``pelican`` command
+to simplify the process of generating, previewing, and uploading your site. One
+such tool is the ``Makefile`` that's automatically created for you when you use
+``pelican-quickstart`` to create a skeleton project. To use ``make`` to
+generate your site, run::
$ make html
If you'd prefer to have Pelican automatically regenerate your site every time a
-change is detected (handy when testing locally), use the following command
-instead::
+change is detected (which is handy when testing locally), use the following
+command instead::
$ make regenerate
-To serve the site so it can be previewed in your browser at
+To serve the generated site so it can be previewed in your browser at
http://localhost:8000::
$ make serve
@@ -138,18 +196,29 @@ use rsync over ssh::
That's it! Your site should now be live.
-Writing articles using Pelican
-==============================
+Writing content using Pelican
+=============================
+
+Articles and pages
+------------------
+
+Pelican considers "articles" to be chronological content, such as posts on a
+blog, and thus associated with a date.
+
+The idea behind "pages" is that they are usually not temporal in nature and are
+used for content that does not change very often (e.g., "About" or "Contact"
+pages).
File metadata
---------------
+-------------
Pelican tries to be smart enough to get the information it needs from the
file system (for instance, about the category of your articles), but some
information you need to provide in the form of metadata inside your files.
-You can provide this metadata in reStructuredText text files via the
-following syntax (give your file the ``.rst`` extension)::
+If you are writing your content in reStructuredText format, you can provide
+this metadata in text files via the following syntax (give your file the
+``.rst`` extension)::
My super title
##############
@@ -166,77 +235,78 @@ Pelican implements an extension to reStructuredText to enable support for the
This will be turned into :abbr:`HTML (HyperText Markup Language)`.
-You can also use Markdown syntax (with a file ending in ``.md``, ``.markdown``,
-or ``.mkd``). Markdown generation will not work until you explicitly install the
-``Markdown`` package, which can be done via ``pip install Markdown``. Metadata
-syntax for Markdown posts should follow this pattern::
+You can also use Markdown syntax (with a file ending in ``.md``,
+``.markdown``, ``.mkd``, or ``.mdown``). Markdown generation requires that you
+first explicitly install the ``Markdown`` package, which can be done via ``pip
+install Markdown``. Metadata syntax for Markdown posts should follow this
+pattern::
Title: My super title
Date: 2010-12-03 10:20
- Tags: thats, awesome
- Category: yeah
+ Category: Python
+ Tags: pelican, publishing
Slug: my-super-post
Author: Alexis Metaireau
Summary: Short version for index and feeds
This is the content of my super blog post.
-Note that, aside from the title, none of this metadata is mandatory: if the
-date is not specified, Pelican can rely on the file's "mtime" timestamp through
-the ``DEFAULT_DATE`` setting, and the category can be determined by the
-directory in which the file resides. For example, a file located at
+Pelican can also process HTML files ending in ``.html`` and ``.htm``. Pelican
+interprets the HTML in a very straightforward manner, reading metadata from
+``meta`` tags, the title from the ``title`` tag, and the body out from the
+``body`` tag::
+
+
+
+ My super title
+
+
+
+
+
+
+
+ This is the content of my super blog post.
+
+
+
+With HTML, there is one simple exception to the standard metadata: ``tags`` can
+be specified either via the ``tags`` metadata, as is standard in Pelican, or
+via the ``keywords`` metadata, as is standard in HTML. The two can be used
+interchangeably.
+
+Note that, aside from the title, none of this article metadata is mandatory:
+if the date is not specified and ``DEFAULT_DATE`` is set to ``fs``, Pelican
+will rely on the file's "mtime" timestamp, and the category can be determined
+by the directory in which the file resides. For example, a file located at
``python/foobar/myfoobar.rst`` will have a category of ``foobar``. If you would
like to organize your files in other ways where the name of the subfolder would
not be a good category name, you can set the setting ``USE_FOLDER_AS_CATEGORY``
-to ``False``. If there is no summary metadata for a given post, the
+to ``False``.
+
+If you do not explicitly specify summary metadata for a given post, the
``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the
beginning of an article are used as the summary.
You can also extract any metadata from the filename through a regular
-expression to be set in the ``FILENAME_METADATA`` setting.
-All named groups that are matched will be set in the metadata object. The
-default value for the ``FILENAME_METADATA`` setting will only extract the date
-from the filename. For example, if you would like to extract both the date and
-the slug, you could set something like:
-``'(?P\d{4}-\d{2}-\d{2})_(?P.*)'``
+expression to be set in the ``FILENAME_METADATA`` setting. All named groups
+that are matched will be set in the metadata object. The default value for the
+``FILENAME_METADATA`` setting will only extract the date from the filename. For
+example, if you would like to extract both the date and the slug, you could set
+something like: ``'(?P\d{4}-\d{2}-\d{2})_(?P.*)'``
Please note that the metadata available inside your files takes precedence over
the metadata extracted from the filename.
-Generate your blog
-------------------
-
-The ``make`` shortcut commands mentioned in the *Kickstart a blog* section
-are mostly wrappers around the ``pelican`` command that generates the HTML from
-the content. The ``pelican`` command can also be run directly::
-
- $ pelican /path/to/your/content/ [-s path/to/your/settings.py]
-
-The above command will generate your weblog and save it in the ``output/``
-folder, using the default theme to produce a simple site. The default theme is
-simple HTML without styling and is provided so folks may use it as a basis for
-creating their own themes.
-
-Pelican has other command-line switches available. Have a look at the help to
-see all the options you can use::
-
- $ pelican --help
-
-Auto-reload
------------
-
-It's possible to tell Pelican to watch for your modifications, instead of
-manually re-running it every time you want to see your changes. To enable this,
-run the ``pelican`` command with the ``-r`` or ``--autoreload`` option.
-
Pages
-----
If you create a folder named ``pages`` inside the content folder, all the
-files in it will be used to generate static pages.
+files in it will be used to generate static pages, such as **About** or
+**Contact** pages. (See example filesystem layout below.)
-Then, use the ``DISPLAY_PAGES_ON_MENU`` setting to add all those pages to
-the primary navigation menu.
+You can use the ``DISPLAY_PAGES_ON_MENU`` setting to control whether all those
+pages are displayed in the primary navigation menu. (Default is ``True``.)
If you want to exclude any pages from being linked to or listed in the menu
then add a ``status: hidden`` attribute to its metadata. This is useful for
@@ -251,23 +321,25 @@ hierarchy. This makes it easier to link from the current post to other posts
and images that may be sitting alongside the current post (instead of having
to determine where those resources will be placed after site generation).
-To link to internal content, use the following syntax:
-``|filename|path/to/file``.
+To link to internal content (files in the ``content`` directory), use the
+following syntax: ``|filename|path/to/file``::
-For example, you may want to add links between "article1" and "article2" given
-the structure::
website/
├── content
│ ├── article1.rst
- │ └── cat/
- │ └── article2.md
+ │ ├── cat/
+ │ │ └── article2.md
+ │ └── pages
+ │ └── about.md
└── pelican.conf.py
In this example, ``article1.rst`` could look like::
- Title: The first article
- Date: 2012-12-01
+ The first article
+ #################
+
+ :date: 2012-12-01 10:02
See below intra-site link examples in reStructuredText format.
@@ -277,18 +349,37 @@ In this example, ``article1.rst`` could look like::
and ``article2.md``::
Title: The second article
- Date: 2012-12-01
+ Date: 2012-12-01 10:02
See below intra-site link examples in Markdown format.
[a link relative to content root](|filename|/article1.rst)
[a link relative to current file](|filename|../article1.rst)
-.. note::
+Embedding non-article or non-page content is slightly different in that the
+directories need to be specified in ``pelicanconf.py`` file. The ``images``
+directory is configured for this by default but others will need to be added
+manually::
- You can use the same syntax to link to internal pages or even static
- content (like images) which would be available in a directory listed in
- ``settings["STATIC_PATHS"]``.
+ content
+ ├── images
+ │ └── han.jpg
+ └── misc
+ └── image-test.md
+
+And ``image-test.md`` would include::
+
+ 
+
+Any content can be linked in this way. What happens is that the ``images``
+directory gets copied to ``output/static/`` upon publishing. This is
+because ``images`` is in the ``settings["STATIC_PATHS"]`` list by default. If
+you want to have another directory, say ``pdfs`` you would need to add the
+following to ``pelicanconf.py``::
+
+ STATIC_PATHS = ['images', 'pdfs']
+
+And then the ``pdfs`` directory would also be copied to ``output/static/``.
Importing an existing blog
--------------------------
@@ -338,6 +429,19 @@ 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.
+If you do not want the original version of one specific article to be detected
+by the ``DEFAULT_LANG`` setting, use the ``translation`` metadata to specify
+which posts are translations::
+
+ Foobar is not dead
+ ##################
+
+ :slug: foobar-is-not-dead
+ :lang: en
+ :translation: true
+
+ That's true, foobar is still alive!
+
Syntax highlighting
-------------------
@@ -369,19 +473,4 @@ publishing, for example), you can add a ``status: draft`` attribute to its
metadata. That article will then be output to the ``drafts`` folder and not
listed on the index page nor on any category page.
-Viewing the generated files
----------------------------
-
-The files generated by Pelican are static files, so you don't actually need
-anything special to see what's happening with the generated files.
-
-You can either use your browser to open the files on your disk::
-
- firefox output/index.html
-
-Or run a simple web server using Python::
-
- cd output && python -m SimpleHTTPServer
-
.. _virtualenv: http://www.virtualenv.org/
-.. _virtualenvwrapper: http://www.doughellmann.com/projects/virtualenvwrapper/
diff --git a/docs/importer.rst b/docs/importer.rst
index ad4e3984..9a0c513e 100644
--- a/docs/importer.rst
+++ b/docs/importer.rst
@@ -4,55 +4,61 @@
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:
+``pelican-import`` is a command-line tool for converting articles from other
+software to reStructuredText or Markdown. The supported import formats are:
- WordPress XML export
- Dotclear export
+- Posterous API
- RSS/Atom feed
-The conversion from HTML to reStructuredText relies on `pandoc
-`_. For Dotclear, if the source posts are
-written with Markdown syntax, they will not be converted (as Pelican also
-supports Markdown).
+The conversion from HTML to reStructuredText or Markdown relies on `Pandoc`_.
+For Dotclear, if the source posts are written with Markdown syntax, they will
+not be converted (as Pelican also supports Markdown).
+
Dependencies
-""""""""""""
+============
-``pelican-import`` has two dependencies not required by the rest of pelican:
+``pelican-import`` has some dependencies not required by the rest of Pelican:
-- BeautifulSoup
-- pandoc
+- *BeautifulSoup4* and *lxml*, for WordPress and Dotclear import. Can be installed like
+ any other Python package (``pip install BeautifulSoup4 lxml``).
+- *Feedparser*, for feed import (``pip install feedparser``).
+- *Pandoc*, see the `Pandoc site`_ for installation instructions on your
+ operating system.
-BeatifulSoup can be installed like any other Python package::
-
- $ pip install BeautifulSoup
-
-For pandoc, install a package for your operating system from the
-`pandoc site `_.
+.. _Pandoc: http://johnmacfarlane.net/pandoc/
+.. _Pandoc site: http://johnmacfarlane.net/pandoc/installing.html
Usage
-"""""
+=====
-| pelican-import [-h] [--wpfile] [--dotclear] [--feed] [-o OUTPUT]
-| [-m MARKUP] [--dir-cat] [--strip-raw] [--disable-slugs]
-| input
+::
+
+ pelican-import [-h] [--wpfile] [--dotclear] [--posterous] [--feed] [-o OUTPUT]
+ [-m MARKUP] [--dir-cat] [--dir-page] [--strip-raw] [--disable-slugs]
+ [-e EMAIL] [-p PASSWORD]
+ input|api_token
Positional arguments
-====================
+--------------------
input The input file to read
+ api_token [Posterous only] api_token can be obtained from http://posterous.com/api/
Optional arguments
-""""""""""""""""""
+------------------
- -h, --help show this help message and exit
- --wpfile Wordpress XML export (default: False)
+ -h, --help Show this help message and exit
+ --wpfile WordPress XML export (default: False)
--dotclear Dotclear export (default: False)
+ --posterous Posterous API (default: False)
--feed Feed to parse (default: False)
-o OUTPUT, --output OUTPUT
Output path (default: output)
@@ -61,6 +67,8 @@ Optional arguments
(default: rst)
--dir-cat Put files in directories with categories name
(default: False)
+ --dir-page Put files recognised as pages in "pages/" sub-
+ directory (wordpress import only) (default: False)
--strip-raw Strip raw HTML code that can't be converted to markup
such as flash embeds or iframes (wordpress import
only) (default: False)
@@ -68,6 +76,11 @@ Optional arguments
output. With this disabled, your Pelican URLs may not
be consistent with your original posts. (default:
False)
+ -e EMAIL, --email=EMAIL
+ Email used to authenticate Posterous API
+ -p PASSWORD, --password=PASSWORD
+ Password used to authenticate Posterous API
+
Examples
========
@@ -80,10 +93,15 @@ For Dotclear::
$ pelican-import --dotclear -o ~/output ~/backup.txt
+for Posterous::
+
+ $ pelican-import --posterous -o ~/output --email= --password=
+
+
Tests
=====
To test the module, one can use sample files:
-- for Wordpress: http://wpcandy.com/made/the-sample-post-collection
+- for WordPress: http://wpcandy.com/made/the-sample-post-collection
- for Dotclear: http://themes.dotaddict.org/files/public/downloads/lorem-backup.txt
diff --git a/docs/index.rst b/docs/index.rst
index ebe1ace6..eceb407f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,10 +3,10 @@ Pelican
Pelican is a static site generator, written in Python_.
-* Write your weblog entries directly with your editor of choice (vim!)
- in reStructuredText_, Markdown_, or AsciiDoc_
-* Includes a simple CLI tool to (re)generate the weblog
-* Easy to interface with DVCSes and web hooks
+* Write your content directly with your editor of choice (vim!)
+ in reStructuredText_, Markdown_, or AsciiDoc_ formats
+* Includes a simple CLI tool to (re)generate your site
+* Easy to interface with distributed version control systems and web hooks
* Completely static output is easy to host anywhere
Features
@@ -14,16 +14,15 @@ Features
Pelican currently supports:
-* Blog articles and pages
+* Articles (e.g., blog posts) and pages (e.g., "About", "Projects", "Contact")
* Comments, via an external service (Disqus). (Please note that while
useful, Disqus is an external service, and thus the comment data will be
somewhat outside of your control and potentially subject to data loss.)
* Theming support (themes are created using Jinja2_ templates)
-* PDF generation of the articles/pages (optional)
* Publication of articles in multiple languages
* Atom/RSS feeds
* Code syntax highlighting
-* Asset management with `webassets`_ (optional)
+* PDF generation of the articles/pages (optional)
* Import from WordPress, Dotclear, or RSS feeds
* Integration with external tools: Twitter, Google Analytics, etc. (optional)
@@ -80,4 +79,3 @@ A French version of the documentation is available at :doc:`fr/index`.
.. _`Pelican's internals`: http://docs.getpelican.com/en/latest/internals.html
.. _`#pelican on Freenode`: irc://irc.freenode.net/pelican
.. _webchat: http://webchat.freenode.net/?channels=pelican&uio=d4
-.. _webassets: https://github.com/miracle2k/webassets
diff --git a/docs/internals.rst b/docs/internals.rst
index 280e14d7..704122ba 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -23,8 +23,8 @@ The logic is separated into different classes and concepts:
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 (AsciiDoc, Markdown and
- reStructuredText for now, but the system is extensible). Given a file, they
+* **Readers** are used to read from various formats (AsciiDoc, HTML, Markdown and
+ reStructuredText for now, but the system is extensible). Given a file, they
return metadata (author, tags, category, etc.) and content (HTML-formatted).
* **Generators** generate the different outputs. For instance, Pelican comes with
@@ -47,19 +47,17 @@ Take a look at the Markdown reader::
class MarkdownReader(Reader):
enabled = bool(Markdown)
- def read(self, filename):
+ def read(self, source_path):
"""Parse content and metadata of markdown files"""
- text = open(filename)
+ text = pelican_open(source_path)
md = Markdown(extensions = ['meta', 'codehilite'])
content = md.convert(text)
metadata = {}
for name, value in md.Meta.items():
- if name in _METADATA_FIELDS:
- meta = _METADATA_FIELDS[name](value[0])
- else:
- meta = value[0]
- metadata[name.lower()] = meta
+ name = name.lower()
+ meta = self.process_metadata(name, value[0])
+ metadata[name] = meta
return content, metadata
Simple, isn't it?
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 7e09810b..064ba73d 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -6,9 +6,6 @@ Plugins
Beginning with version 3.0, Pelican supports plugins. Plugins are a way to add
features to Pelican without having to directly modify the Pelican core.
-Pelican is shipped with a set of bundled plugins, but you can easily implement
-your own. This page describes how to use and create plugins.
-
How to use plugins
==================
@@ -16,19 +13,32 @@ To load plugins, you have to specify them in your settings file. There are two
ways to do so. The first method is to specify strings with the path to the
callables::
- PLUGINS = ['pelican.plugins.gravatar',]
+ PLUGINS = ['package.myplugin',]
Alternatively, another method is to import them and add them to the list::
- from pelican.plugins import gravatar
- PLUGINS = [gravatar,]
+ from package import myplugin
+ PLUGINS = [myplugin,]
If your plugins are not in an importable path, you can specify a ``PLUGIN_PATH``
-in the settings::
+in the settings. ``PLUGIN_PATH`` can be an absolute path or a path relative to
+the settings file::
PLUGIN_PATH = "plugins"
PLUGINS = ["list", "of", "plugins"]
+Where to find plugins
+=====================
+
+We maintain a separate repository of plugins for people to share and use.
+Please visit the `pelican-plugins`_ repository for a list of available plugins.
+
+.. _pelican-plugins: https://github.com/getpelican/pelican-plugins
+
+Please note that while we do our best to review and maintain these plugins,
+they are submitted by the Pelican community and thus may have varying levels of
+support and interoperability.
+
How to create plugins
=====================
@@ -47,8 +57,6 @@ which you map the signals to your plugin logic. Let's take a simple example::
def register():
signals.initialized.connect(test)
-
-
List of signals
===============
@@ -73,6 +81,8 @@ get_generators generators invoked in Pelic
generator in a tuple or in a list.
pages_generate_context pages_generator, metadata
pages_generator_init pages_generator invoked in the PagesGenerator.__init__
+pages_generator_finalized pages_generator invoked at the end of PagesGenerator.generate_context
+content_object_init content_object invoked at the end of Content.__init__ (see note below)
============================= ============================ ===========================================================================
The list is currently small, so don't hesitate to add signals and make a pull
@@ -94,270 +104,3 @@ request if you need them!
def register():
signals.content_object_init.connect(test, sender=contents.Article)
-
-
-
-List of plugins
-===============
-
-The following plugins are currently included with Pelican:
-
-* `Asset management`_ ``pelican.plugins.assets``
-* `GitHub activity`_ ``pelican.plugins.github_activity``
-* `Global license`_ ``pelican.plugins.global_license``
-* `Gravatar`_ ``pelican.plugins.gravatar``
-* `Gzip cache`_ ``pelican.plugins.gzip_cache``
-* `HTML tags for reStructuredText`_ ``pelican.plugins.html_rst_directive``
-* `Related posts`_ ``pelican.plugins.related_posts``
-* `Sitemap`_ ``pelican.plugins.sitemap``
-
-Ideas for plugins that haven't been written yet:
-
-* Tag cloud
-* Translation
-
-Plugin descriptions
-===================
-
-Asset management
-----------------
-
-This plugin allows you to use the `Webassets`_ module to manage assets such as
-CSS and JS files. The module must first be installed::
-
- pip install webassets
-
-The Webassets module allows you to perform a number of useful asset management
-functions, including:
-
-* CSS minifier (``cssmin``, ``yui_css``, ...)
-* CSS compiler (``less``, ``sass``, ...)
-* JS minifier (``uglifyjs``, ``yui_js``, ``closure``, ...)
-
-Others filters include gzip compression, integration of images in CSS via data
-URIs, and more. Webassets can also append a version identifier to your asset
-URL to convince browsers to download new versions of your assets when you use
-far-future expires headers. Please refer to the `Webassets documentation`_ for
-more information.
-
-When used with Pelican, Webassets is configured to process assets in the
-``OUTPUT_PATH/theme`` directory. You can use Webassets in your templates by
-including one or more template tags. The Jinja variable ``{{ ASSET_URL }}`` can
-be used in templates and is relative to the ``theme/`` url. The
-``{{ ASSET_URL }}`` variable should be used in conjunction with the
-``{{ SITEURL }}`` variable in order to generate URLs properly. For example:
-
-.. code-block:: jinja
-
- {% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %}
-
- {% endassets %}
-
-... will produce a minified css file with a version identifier that looks like:
-
-.. code-block:: html
-
-
-
-These filters can be combined. Here is an example that uses the SASS compiler
-and minifies the output:
-
-.. code-block:: jinja
-
- {% assets filters="sass,cssmin", output="css/style.min.css", "css/style.scss" %}
-
- {% endassets %}
-
-Another example for Javascript:
-
-.. code-block:: jinja
-
- {% assets filters="uglifyjs,gzip", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %}
-
- {% endassets %}
-
-The above will produce a minified and gzipped JS file:
-
-.. code-block:: html
-
-
-
-Pelican's debug mode is propagated to Webassets to disable asset packaging
-and instead work with the uncompressed assets. However, this also means that
-the LESS and SASS files are not compiled. This should be fixed in a future
-version of Webassets (cf. the related `bug report
-`_).
-
-.. _Webassets: https://github.com/miracle2k/webassets
-.. _Webassets documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html
-
-
-GitHub activity
----------------
-
-This plugin makes use of the `feedparser`_ library that you'll need to
-install.
-
-Set the ``GITHUB_ACTIVITY_FEED`` parameter to your GitHub activity feed.
-For example, to track Pelican project activity, the setting would be::
-
- GITHUB_ACTIVITY_FEED = 'https://github.com/getpelican.atom'
-
-On the template side, you just have to iterate over the ``github_activity``
-variable, as in this example::
-
- {% if GITHUB_ACTIVITY_FEED %}
-
-
Github Activity
-
-
- {% for entry in github_activity %}
-
{{ entry[0] }} {{ entry[1] }}
- {% endfor %}
-
-
- {% endif %}
-
-``github_activity`` is a list of lists. The first element is the title,
-and the second element is the raw HTML from GitHub.
-
-.. _feedparser: https://crate.io/packages/feedparser/
-
-Global license
---------------
-
-This plugin allows you to define a ``LICENSE`` setting and adds the contents of that
-license variable to the article's context, making that variable available to use
-from within your theme's templates.
-
-Gravatar
---------
-
-This plugin assigns the ``author_gravatar`` variable to the Gravatar URL and
-makes the variable available within the article's context. You can add
-``AUTHOR_EMAIL`` to your settings file to define the default author's email
-address. Obviously, that email address must be associated with a Gravatar
-account.
-
-Alternatively, you can provide an email address from within article metadata::
-
- :email: john.doe@example.com
-
-If the email address is defined via at least one of the two methods above,
-the ``author_gravatar`` variable is added to the article's context.
-
-Gzip cache
-----------
-
-Certain web servers (e.g., Nginx) can use a static cache of gzip-compressed
-files to prevent the server from compressing files during an HTTP call. Since
-compression occurs at another time, these compressed files can be compressed
-at a higher compression level for increased optimization.
-
-The ``gzip_cache`` plugin compresses all common text type files into a ``.gz``
-file within the same directory as the original file.
-
-HTML tags for reStructuredText
-------------------------------
-
-This plugin allows you to use HTML tags from within reST documents. Following
-is a usage example, which is in this case a contact form::
-
- .. html::
-
-
-
-Related posts
--------------
-
-This plugin adds the ``related_posts`` variable to the article's context.
-To enable, add the following to your settings file::
-
- from pelican.plugins import related_posts
- PLUGINS = [related_posts]
-
-You can then use the ``article.related_posts`` variable in your templates.
-For example::
-
- {% if article.related_posts %}
-
- {% for related_post in article.related_posts %}
-
{{ related_post }}
- {% endfor %}
-
- {% endif %}
-
-Sitemap
--------
-
-The sitemap plugin generates plain-text or XML sitemaps. You can use the
-``SITEMAP`` variable in your settings file to configure the behavior of the
-plugin.
-
-The ``SITEMAP`` variable must be a Python dictionary and can contain three keys:
-
-- ``format``, which sets the output format of the plugin (``xml`` or ``txt``)
-
-- ``priorities``, which is a dictionary with three keys:
-
- - ``articles``, the priority for the URLs of the articles and their
- translations
-
- - ``pages``, the priority for the URLs of the static pages
-
- - ``indexes``, the priority for the URLs of the index pages, such as tags,
- author pages, categories indexes, archives, etc...
-
- All the values of this dictionary must be decimal numbers between ``0`` and ``1``.
-
-- ``changefreqs``, which is a dictionary with three items:
-
- - ``articles``, the update frequency of the articles
-
- - ``pages``, the update frequency of the pages
-
- - ``indexes``, the update frequency of the index pages
-
- Valid frequency values are ``always``, ``hourly``, ``daily``, ``weekly``, ``monthly``,
- ``yearly`` and ``never``.
-
-If a key is missing or a value is incorrect, it will be replaced with the
-default value.
-
-The sitemap is saved in ``/sitemap.``.
-
-.. note::
- ``priorities`` and ``changefreqs`` are information for search engines.
- They are only used in the XML sitemaps.
- For more information:
-
-**Example**
-
-Here is an example configuration (it's also the default settings):
-
-.. code-block:: python
-
- PLUGINS=['pelican.plugins.sitemap',]
-
- SITEMAP = {
- 'format': 'xml',
- 'priorities': {
- 'articles': 0.5,
- 'indexes': 0.5,
- 'pages': 0.5
- },
- 'changefreqs': {
- 'articles': 'monthly',
- 'indexes': 'daily',
- 'pages': 'monthly'
- }
- }
diff --git a/docs/settings.rst b/docs/settings.rst
index baf9f60c..3a32ad22 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -40,6 +40,9 @@ Setting name (default value) What doe
`DEFAULT_CATEGORY` (``'misc'``) The default category to fall back on.
`DEFAULT_DATE_FORMAT` (``'%a %d %B %Y'``) The default date format you want to use.
`DISPLAY_PAGES_ON_MENU` (``True``) Whether to display pages on the menu of the
+ template. Templates may or may not honor this
+ setting.
+`DISPLAY_CATEGORIES_ON_MENU` (``True``) Whether to display categories on the menu of the
template. Templates may or not honor this
setting.
`DEFAULT_DATE` (``None``) The default date you want to use.
@@ -59,23 +62,39 @@ Setting name (default value) What doe
For example, if you would like to extract both the
date and the slug, you could set something like:
``'(?P\d{4}-\d{2}-\d{2})_(?P.*)'``.
-`DELETE_OUTPUT_DIRECTORY` (``False``) Delete the content of the output directory before
- generating new files.
+`PATH_METADATA` (``''``) Like ``FILENAME_METADATA``, but parsed from a page's
+ full path relative to the content source directory.
+`DELETE_OUTPUT_DIRECTORY` (``False``) Delete the output directory, and **all** of its contents, before
+ generating new files. This can be useful in preventing older,
+ unnecessary files from persisting in your output. However, **this is
+ a destructive setting and should be handled with extreme care.**
`FILES_TO_COPY` (``()``) A list of files (or directories) to copy from the source (inside the
content directory) to the destination (inside the output directory).
For example: ``(('extra/robots.txt', 'robots.txt'),)``.
`JINJA_EXTENSIONS` (``[]``) A list of any Jinja2 extensions you want to use.
+`JINJA_FILTERS` (``{}``) A list of custom Jinja2 filters you want to use.
+ The dictionary should map the filtername to the filter function.
+ For example: ``{'urlencode': urlencode_filter}``
+ See `Jinja custom filters documentation`_.
`LOCALE` (''[#]_) Change the locale. A list of locales can be provided
here or a single string representing one locale.
When providing a list, all the locales will be tried
until one works.
`MARKUP` (``('rst', 'md')``) A list of available markup languages you want
to use. For the moment, the only available values
- are `rst`, `md`, `markdown`, `mkd`, `html`, and `htm`.
-`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.
+ are `rst`, `md`, `markdown`, `mkd`, `mdown`, `html`, and `htm`.
+`IGNORE_FILES` (``['.#*']``) A list of file globbing patterns to match against the
+ source files to be ignored by the processor. For example,
+ the default ``['.#*']`` will ignore emacs lock files.
+`MD_EXTENSIONS` (``['codehilite(css_class=highlight)','extra']``) A list of the extensions that the Markdown processor
+ will use. Refer to the Python Markdown documentation's
+ `Extensions section `_
+ for a complete list of supported extensions. (Note that
+ defining this in your settings file will override and
+ replace the default values. If your goal is to *add*
+ to the default values for this setting, you'll need to
+ include them explicitly and enumerate the full list of
+ desired Markdown extensions.)
`OUTPUT_PATH` (``'output/'``) Where to output the generated files.
`PATH` (``None``) Path to content directory to be processed by Pelican.
`PAGE_DIR` (``'pages'``) Directory to look at for pages, relative to `PATH`.
@@ -90,9 +109,9 @@ Setting name (default value) What doe
`OUTPUT_SOURCES_EXTENSION` (``.text``) Controls the extension that will be used by the SourcesGenerator.
Defaults to ``.text``. If not a valid string the default value
will be used.
-`RELATIVE_URLS` (``True``) Defines whether Pelican should use document-relative URLs or
- not. If set to ``False``, Pelican will use the SITEURL
- setting to construct absolute URLs.
+`RELATIVE_URLS` (``False``) Defines whether Pelican should use document-relative URLs or
+ not. Only set this to ``True`` when developing/testing and only
+ if you fully understand the effect it can have on links/feeds.
`PLUGINS` (``[]``) The list of plugins to load. See :ref:`plugins`.
`SITENAME` (``'A Pelican Blog'``) Your site name
`SITEURL` Base URL of your website. Not defined by default,
@@ -176,6 +195,27 @@ Example usage:
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/``.
+Pelican can optionally create per-year, per-month, and per-day archives of your
+posts. These secondary archives are disabled by default but are automatically
+enabled if you supply format strings for their respective `_SAVE_AS` settings.
+Period archives fit intuitively with the hierarchical model of web URLs and can
+make it easier for readers to navigate through the posts you've written over time.
+
+Example usage:
+
+* YEAR_ARCHIVE_SAVE_AS = ``'posts/{date:%Y}/index.html'``
+* MONTH_ARCHIVE_SAVE_AS = ``'posts/{date:%Y}/{date:%b}/index.html'``
+
+With these settings, Pelican will create an archive of all your posts for the year
+at (for instance) 'posts/2011/index.html', and an archive of all your posts for
+the month at 'posts/2011/Aug/index.html'.
+
+.. note::
+ Period archives work best when the final path segment is 'index.html'.
+ This way a reader can remove a portion of your URL and automatically
+ arrive at an appropriate archive of posts, without having to specify
+ a page name.
+
==================================================== =====================================================
Setting name (default value) What does it do?
==================================================== =====================================================
@@ -186,7 +226,9 @@ Setting name (default value) What does it do?
`ARTICLE_LANG_SAVE_AS` (``'{slug}-{lang}.html'``) The place where we will save an article which
doesn't use the default language.
`PAGE_URL` (``'pages/{slug}.html'``) The URL we will use to link to a page.
-`PAGE_SAVE_AS` (``'pages/{slug}.html'``) The location we will save the page.
+`PAGE_SAVE_AS` (``'pages/{slug}.html'``) The location we will save the page. This value has to be
+ the same as PAGE_URL or you need to use a rewrite in
+ your server config.
`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
@@ -200,6 +242,12 @@ Setting name (default value) What does it do?
`_SAVE_AS` The location to save content generated from direct
templates. Where is the
upper case template name.
+`YEAR_ARCHIVE_SAVE_AS` (False) The location to save per-year archives of your
+ posts.
+`MONTH_ARCHIVE_SAVE_AS` (False) The location to save per-month archives of your
+ posts.
+`DAY_ARCHIVE_SAVE_AS` (False) The location to save per-day archives of your
+ posts.
==================================================== =====================================================
.. note::
@@ -223,10 +271,10 @@ Have a look at `the wikipedia page`_ to get a list of valid timezone values.
Date format and locale
----------------------
-If no DATE_FORMATS 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`_ :
+If no DATE_FORMATS are set, Pelican will fall back to DEFAULT_DATE_FORMAT. If
+you need to maintain multiple languages with different date formats, you can
+set this dict using the language name (``lang`` metadata in your post content)
+as the key. Regarding available format codes, see `strftime document of python`_ :
.. parsed-literal::
@@ -386,7 +434,7 @@ The default theme does not support tag clouds, but it is pretty easy to add::
@@ -470,6 +518,7 @@ free to use them in your themes as well.
======================= =======================================================
Setting name What does it do ?
======================= =======================================================
+`SITESUBTITLE` A subtitle to appear in the header.
`DISQUS_SITENAME` Pelican can handle Disqus comments. Specify the
Disqus sitename identifier here.
`GITHUB_URL` Your GitHub URL (if you have one). It will then
@@ -503,3 +552,6 @@ Example settings
.. literalinclude:: ../samples/pelican.conf.py
:language: python
+
+
+.. _Jinja custom filters documentation: http://jinja.pocoo.org/docs/api/#custom-filters
diff --git a/docs/themes.rst b/docs/themes.rst
index 664b4466..ddf509f8 100644
--- a/docs/themes.rst
+++ b/docs/themes.rst
@@ -17,16 +17,17 @@ To make your own theme, you must follow the following structure::
│ ├── css
│ └── images
└── templates
- ├── archives.html // to display archives
- ├── article.html // processed for each article
- ├── author.html // processed for each author
- ├── authors.html // must list all the authors
- ├── categories.html // must list all the categories
- ├── category.html // processed for each category
- ├── index.html // the index. List all the articles
- ├── page.html // processed for each page
- ├── tag.html // processed for each tag
- └── tags.html // must list all the tags. Can be a tag cloud.
+ ├── archives.html // to display archives
+ ├── period_archives.html // to display time-period archives
+ ├── article.html // processed for each article
+ ├── author.html // processed for each author
+ ├── authors.html // must list all the authors
+ ├── categories.html // must list all the categories
+ ├── category.html // processed for each category
+ ├── index.html // the index. List all the articles
+ ├── page.html // processed for each page
+ ├── tag.html // processed for each tag
+ └── tags.html // must list all the tags. Can be a tag cloud.
* `static` contains all the static assets, which will be copied to the output
`theme` folder. I've put the CSS and image folders here, but they are
@@ -54,10 +55,15 @@ All of these settings will be available to all templates.
============= ===================================================
Variable Description
============= ===================================================
+output_file The name of the file currently being generated. For
+ instance, when Pelican is rendering the homepage,
+ output_file will be "index.html".
articles The list of articles, ordered descending by date
All the elements are `Article` objects, so you can
access their attributes (e.g. title, summary, author
- etc.)
+ etc.). Sometimes this is shadowed (for instance in
+ the tags page). You will then find info about it
+ in the `all_articles` variable.
dates The same list of articles, but ordered by date,
ascending
tags A list of (tag, articles) tuples, containing all
@@ -68,6 +74,37 @@ categories A list of (category, articles) tuples, containing
pages The list of pages
============= ===================================================
+Sorting
+-------
+
+URL wrappers (currently categories, tags, and authors), have
+comparison methods that allow them to be easily sorted by name::
+
+ {% for tag, articles in tags|sort %}
+
+If you want to sort based on different criteria, `Jinja's sort
+command`__ has a number of options.
+
+__ http://jinja.pocoo.org/docs/templates/#sort
+
+
+Date Formatting
+---------------
+
+Pelican formats the date with according to your settings and locale
+(``DATE_FORMATS``/``DEFAULT_DATE_FORMAT``) and provides a
+``locale_date`` attribute. On the other hand, ``date`` attribute will
+be a `datetime`_ object. If you need custom formatting for a date
+different than your settings, use the Jinja filter ``strftime``
+that comes with Pelican. Usage is same as Python `strftime`_ format,
+but the filter will do the right thing and format your date according
+to the locale given in your settings::
+
+ {{ article.date|strftime('%d %B %Y') }}
+
+.. _datetime: http://docs.python.org/2/library/datetime.html#datetime-objects
+.. _strftime: http://docs.python.org/2/library/datetime.html#strftime-strptime-behavior
+
index.html
----------
diff --git a/docs/tips.rst b/docs/tips.rst
index abb739b1..64695db0 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -6,47 +6,80 @@ 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.
-
-User Pages
-----------
-GitHub allows you to create user pages in the form of ``username.github.com``.
-Whatever is created in the master branch will be published. For this purpose,
-just the output generated by Pelican needs to pushed to GitHub.
-
-So given a repository containing your articles, just run Pelican over the posts
-and deploy the master branch to GitHub::
-
- $ pelican -s pelican.conf.py ./path/to/posts -o /path/to/output
-
-Now add all the files in the output directory generated by Pelican::
-
- $ git add /path/to/output/*
- $ git commit -am "Your Message"
- $ git push origin master
+`GitHub Pages `_ offer an easy
+and convenient way to publish Pelican sites. There are `two types of GitHub
+Pages `_:
+*Project Pages* and *User Pages*. Pelican sites can be published as both
+Project Pages and User Pages.
Project Pages
-------------
-For creating Project pages, a branch called ``gh-pages`` is used for publishing.
-The excellent `ghp-import `_ makes this
-really easy, which can be installed via::
- $ pip install ghp-import
+To publish a Pelican site as Project Pages you need to *push* the content of
+the ``output`` dir generated by Pelican to a repository's ``gh-pages`` branch
+on GitHub.
-Then, given a repository containing your articles, you would simply run
-Pelican and upload the output to GitHub::
+The excellent `ghp-import `_, which can
+be installed with ``easy_install`` or ``pip``, makes this process really easy.
- $ pelican -s pelican.conf.py .
+For example, if the sources of your Pelican site are contained in a GitHub
+repository, and if you want to publish your Pelican site as Project Pages of
+this repository, you can then use the following::
+
+ $ pelican content -o output pelicanconf.py
$ ghp-import output
$ git push origin gh-pages
-And that's it.
+The ``ghp-import output`` command updates the local ``gh-pages`` branch with
+the content of the ``output`` directory (creating the branch if it doesn't
+already exist). The ``git push origin gh-pages`` command updates the remote
+``gh-pages`` branch, effectively publishing the Pelican site.
-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!
+.. note::
-Put the following into ``.git/hooks/post-commit``::
+ The ``github`` target of the Makefile created by the ``pelican-quickstart``
+ command publishes the Pelican site as Project Pages as described above.
- pelican -s pelican.conf.py . && ghp-import output && git push origin gh-pages
+User Pages
+----------
+
+To publish a Pelican site as User Pages you need to *push* the content of the
+``output`` dir generated by Pelican to the ``master`` branch of your
+``.github.com`` repository on GitHub.
+
+Again, you can take advantage of ``ghp-import``::
+
+ $ pelican content -o output pelicanconf.py
+ $ ghp-import output
+ $ git push git@github.com:elemoine/elemoine.github.com.git gh-pages:master
+
+The ``git push`` command pushes the local ``gh-pages`` branch (freshly updated
+by the ``ghp-import`` command) to the ``elemoine.github.com`` repository's
+``master`` branch on GitHub.
+
+.. note::
+
+ To publish your Pelican site as User Pages feel free to adjust the the
+ ``github`` target of the Makefile.
+
+Extra Tips
+----------
+
+Tip #1:
+
+To automatically update your Pelican site on each commit you can create
+a post-commit hook. For example, you can add the following to
+``.git/hooks/post-commit``::
+
+ pelican pelican content -o output pelicanconf.py && ghp-import output && git push origin gh-pages
+
+Tip #2:
+
+To use a `custom domain
+`_ with
+GitHub Pages you need to have a ``CNAME`` file at the root of your pages. For
+that you will add ``CNAME`` file to your ``content``, dir and use the
+``FILES_TO_COPY`` setting variable to tell Pelican to copy that file
+to the ``output`` dir. For example::
+
+ FILES_TO_COPY = (('extra/CNAME', 'CNAME'),)
diff --git a/pelican/__init__.py b/pelican/__init__.py
index e2ea7f76..7f406c4f 100644
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -1,9 +1,14 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+import six
+
import os
import re
import sys
import time
import logging
import argparse
+import locale
from pelican import signals
@@ -12,8 +17,7 @@ from pelican.generators import (ArticlesGenerator, PagesGenerator,
SourceFileGenerator, TemplatePagesGenerator)
from pelican.log import init
from pelican.settings import read_settings
-from pelican.utils import (clean_output_dir, files_changed, file_changed,
- NoFilesError)
+from pelican.utils import clean_output_dir, folder_watcher, file_watcher
from pelican.writers import Writer
__major__ = 3
@@ -21,6 +25,8 @@ __minor__ = 2
__micro__ = 0
__version__ = "{0}.{1}.{2}".format(__major__, __minor__, __micro__)
+DEFAULT_CONFIG_NAME = 'pelicanconf.py'
+
logger = logging.getLogger(__name__)
@@ -40,6 +46,7 @@ class Pelican(object):
self.theme = settings['THEME']
self.output_path = settings['OUTPUT_PATH']
self.markup = settings['MARKUP']
+ self.ignore_files = settings['IGNORE_FILES']
self.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY']
self.init_path()
@@ -47,20 +54,30 @@ class Pelican(object):
signals.initialized.send(self)
def init_path(self):
- if not any(p in sys.path for p in ['', '.']):
+ if not any(p in sys.path for p in ['', os.curdir]):
logger.debug("Adding current directory to system path")
sys.path.insert(0, '')
def init_plugins(self):
- self.plugins = self.settings['PLUGINS']
- for plugin in self.plugins:
+ self.plugins = []
+ logger.debug('Temporarily adding PLUGIN_PATH to system path')
+ _sys_path = sys.path[:]
+ sys.path.insert(0, self.settings['PLUGIN_PATH'])
+ for plugin in self.settings['PLUGINS']:
# if it's a string, then import it
- if isinstance(plugin, basestring):
- logger.debug("Loading plugin `{0}' ...".format(plugin))
- plugin = __import__(plugin, globals(), locals(), 'module')
+ if isinstance(plugin, six.string_types):
+ logger.debug("Loading plugin `{0}`".format(plugin))
+ try:
+ plugin = __import__(plugin, globals(), locals(), str('module'))
+ except ImportError as e:
+ logger.error("Can't find plugin `{0}`: {1}".format(plugin, e))
+ continue
- logger.debug("Registering plugin `{0}'".format(plugin.__name__))
+ logger.debug("Registering plugin `{0}`".format(plugin.__name__))
plugin.register()
+ self.plugins.append(plugin)
+ logger.debug('Restoring system path')
+ sys.path = _sys_path
def _handle_deprecation(self):
@@ -133,10 +150,11 @@ class Pelican(object):
def run(self):
"""Run the generators and return"""
+ start_time = time.time()
context = self.settings.copy()
context['filenames'] = {} # share the dict between all the generators
- context['localsiteurl'] = self.settings.get('SITEURL') # share
+ context['localsiteurl'] = self.settings['SITEURL'] # share
generators = [
cls(
context,
@@ -166,6 +184,14 @@ class Pelican(object):
signals.finalized.send(self)
+ articles_generator = next(g for g in generators if isinstance(g, ArticlesGenerator))
+ pages_generator = next(g for g in generators if isinstance(g, PagesGenerator))
+
+ print('Done: Processed {} articles and {} pages in {:.2f} seconds.'.format(
+ len(articles_generator.articles) + len(articles_generator.translations),
+ len(pages_generator.pages) + len(pages_generator.translations),
+ time.time() - start_time))
+
def get_generator_classes(self):
generators = [StaticGenerator, ArticlesGenerator, PagesGenerator]
@@ -215,11 +241,14 @@ def parse_arguments():
'them separated by commas.')
parser.add_argument('-s', '--settings', dest='settings',
- help='The settings of the application.')
+ help='The settings of the application, this is automatically set to '
+ '{0} if a file exists with this name.'.format(DEFAULT_CONFIG_NAME))
parser.add_argument('-d', '--delete-output-directory',
dest='delete_outputdir',
- action='store_true', help='Delete the output directory.')
+ action='store_true',
+ default=None,
+ help='Delete the output directory.')
parser.add_argument('-v', '--verbose', action='store_const',
const=logging.INFO, dest='verbosity',
@@ -257,15 +286,31 @@ def get_config(args):
config['THEME'] = abstheme if os.path.exists(abstheme) else args.theme
if args.delete_outputdir is not None:
config['DELETE_OUTPUT_DIRECTORY'] = args.delete_outputdir
+
+ # argparse returns bytes in Py2. There is no definite answer as to which
+ # encoding argparse (or sys.argv) uses.
+ # "Best" option seems to be locale.getpreferredencoding()
+ # ref: http://mail.python.org/pipermail/python-list/2006-October/405766.html
+ if not six.PY3:
+ enc = locale.getpreferredencoding()
+ for key in config:
+ if key in ('PATH', 'OUTPUT_PATH', 'THEME'):
+ config[key] = config[key].decode(enc)
+ if key == "MARKUP":
+ config[key] = [a.decode(enc) for a in config[key]]
return config
def get_instance(args):
- settings = read_settings(args.settings, override=get_config(args))
+ config_file = args.settings
+ if config_file is None and os.path.isfile(DEFAULT_CONFIG_NAME):
+ config_file = DEFAULT_CONFIG_NAME
- cls = settings.get('PELICAN_CLASS')
- if isinstance(cls, basestring):
+ settings = read_settings(config_file, override=get_config(args))
+
+ cls = settings['PELICAN_CLASS']
+ if isinstance(cls, six.string_types):
module, cls_name = cls.rsplit('.', 1)
module = __import__(module)
cls = getattr(module, cls_name)
@@ -278,9 +323,19 @@ def main():
init(args.verbosity)
pelican = get_instance(args)
+ watchers = {'content': folder_watcher(pelican.path,
+ pelican.markup,
+ pelican.ignore_files),
+ 'theme': folder_watcher(pelican.theme,
+ [''],
+ pelican.ignore_files),
+ 'settings': file_watcher(args.settings)}
+
try:
if args.autoreload:
- files_found_error = True
+ print(' --- AutoReload Mode: Monitoring `content`, `theme` and `settings`'
+ ' for changes. ---')
+
while True:
try:
# Check source dir for changed files ending with the given
@@ -288,38 +343,54 @@ def main():
# restriction; all files are recursively checked if they
# have changed, no matter what extension the filenames
# have.
- if files_changed(pelican.path, pelican.markup) or \
- files_changed(pelican.theme, ['']):
- if not files_found_error:
- files_found_error = True
- pelican.run()
+ modified = {k: next(v) for k, v in watchers.items()}
- # reload also if settings.py changed
- if file_changed(args.settings):
- logger.info('%s changed, re-generating' %
- args.settings)
+ if modified['settings']:
pelican = get_instance(args)
+
+ if any(modified.values()):
+ print('\n-> Modified: {}. re-generating...'.format(
+ ', '.join(k for k, v in modified.items() if v)))
+
+ if modified['content'] is None:
+ logger.warning('No valid files found in content.')
+
+ if modified['theme'] is None:
+ logger.warning('Empty theme folder. Using `basic` theme.')
+
pelican.run()
- time.sleep(.5) # sleep to avoid cpu load
except KeyboardInterrupt:
logger.warning("Keyboard interrupt, quitting.")
break
- except NoFilesError:
- if files_found_error:
- logger.warning("No valid files found in content. "
- "Nothing to generate.")
- files_found_error = False
- time.sleep(1) # sleep to avoid cpu load
- except Exception, e:
+
+ except Exception as e:
+ if (args.verbosity == logging.DEBUG):
+ logger.critical(e.args)
+ raise
logger.warning(
- "Caught exception \"{}\". Reloading.".format(e)
- )
- continue
+ 'Caught exception "{0}". Reloading.'.format(e))
+
+ finally:
+ time.sleep(.5) # sleep to avoid cpu load
+
else:
+ if next(watchers['content']) is None:
+ logger.warning('No valid files found in content.')
+
+ if next(watchers['theme']) is None:
+ logger.warning('Empty theme folder. Using `basic` theme.')
+
pelican.run()
- except Exception, e:
- logger.critical(unicode(e))
+
+ except Exception as e:
+ # localized systems have errors in native language if locale is set
+ # so convert the message to unicode with the correct encoding
+ msg = str(e)
+ if not six.PY3:
+ msg = msg.decode(locale.getpreferredencoding(False))
+
+ logger.critical(msg)
if (args.verbosity == logging.DEBUG):
raise
diff --git a/pelican/contents.py b/pelican/contents.py
index d675a2ad..5f2e66b0 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -1,49 +1,63 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+import six
+
import copy
import locale
import logging
import functools
import os
import re
+import sys
from datetime import datetime
-from sys import platform, stdin
-from pelican.settings import _DEFAULT_CONFIG
-from pelican.utils import slugify, truncate_html_words, memoized
from pelican import signals
+from pelican.settings import DEFAULT_CONFIG
+from pelican.utils import (slugify, truncate_html_words, memoized, strftime,
+ python_2_unicode_compatible, deprecated_attribute,
+ path_to_url)
+
+# Import these so that they're avalaible when you import from pelican.contents.
+from pelican.urlwrappers import (URLWrapper, Author, Category, Tag) # NOQA
logger = logging.getLogger(__name__)
-class Page(object):
- """Represents a page
- Given a content, and metadata, create an adequate object.
+class Content(object):
+ """Represents a content.
:param content: the string to parse, containing the original content.
+ :param metadata: the metadata associated to this page (optional).
+ :param settings: the settings dictionary (optional).
+ :param source_path: The location of the source of this content (if any).
+ :param context: The shared context between generators.
+
"""
- mandatory_properties = ('title',)
- default_template = 'page'
+ @deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
+ def filename():
+ return None
def __init__(self, content, metadata=None, settings=None,
- filename=None, context=None):
- # init parameters
- if not metadata:
+ source_path=None, context=None):
+ if metadata is None:
metadata = {}
- if not settings:
- settings = copy.deepcopy(_DEFAULT_CONFIG)
+ if settings is None:
+ settings = copy.deepcopy(DEFAULT_CONFIG)
self.settings = settings
self._content = content
self._context = context
self.translations = []
- local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
+ local_metadata = dict(settings['DEFAULT_METADATA'])
local_metadata.update(metadata)
# set metadata as attributes
for key, value in local_metadata.items():
+ if key in ('save_as', 'url'):
+ key = 'override_' + key
setattr(self, key.lower(), value)
# also keep track of the metadata attributes available
@@ -57,6 +71,8 @@ class Page(object):
if 'AUTHOR' in settings:
self.author = Author(settings['AUTHOR'], settings)
+ # XXX Split all the following code into pieces, there is too much here.
+
# manage languages
self.in_default_lang = True
if 'DEFAULT_LANG' in settings:
@@ -70,8 +86,7 @@ class Page(object):
if not hasattr(self, 'slug') and hasattr(self, 'title'):
self.slug = slugify(self.title)
- if filename:
- self.filename = filename
+ self.source_path = source_path
# manage the date format
if not hasattr(self, 'date_format'):
@@ -81,17 +96,15 @@ class Page(object):
self.date_format = settings['DEFAULT_DATE_FORMAT']
if isinstance(self.date_format, tuple):
- locale.setlocale(locale.LC_ALL, self.date_format[0])
+ locale_string = self.date_format[0]
+ if sys.version_info < (3, ) and isinstance(locale_string,
+ six.text_type):
+ locale_string = locale_string.encode('ascii')
+ locale.setlocale(locale.LC_ALL, locale_string)
self.date_format = self.date_format[1]
if hasattr(self, 'date'):
- encoded_date = self.date.strftime(
- self.date_format.encode('ascii', 'xmlcharrefreplace'))
-
- if platform == 'win32':
- self.locale_date = encoded_date.decode(stdin.encoding)
- else:
- self.locale_date = encoded_date.decode('utf')
+ self.locale_date = strftime(self.date, self.date_format)
# manage status
if not hasattr(self, 'status'):
@@ -104,40 +117,57 @@ class Page(object):
if 'summary' in metadata:
self._summary = metadata['summary']
- signals.content_object_init.send(self.__class__, instance=self)
+ signals.content_object_init.send(self)
+
+ def __str__(self):
+ if self.source_path is None:
+ return repr(self)
+ elif six.PY3:
+ return self.source_path or repr(self)
+ else:
+ return str(self.source_path.encode('utf-8', 'replace'))
def check_properties(self):
- """test that each mandatory property is set."""
+ """Test mandatory properties are set."""
for prop in self.mandatory_properties:
if not hasattr(self, prop):
raise NameError(prop)
@property
def url_format(self):
- return {
+ """Returns the URL, formatted with the proper values"""
+ metadata = copy.copy(self.metadata)
+ path = self.metadata.get('path', self.get_relative_source_path())
+ metadata.update({
+ 'path': path_to_url(path),
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
'date': getattr(self, 'date', datetime.now()),
'author': getattr(self, 'author', ''),
'category': getattr(self, 'category',
- self.settings['DEFAULT_CATEGORY']),
- }
+ self.settings['DEFAULT_CATEGORY']),
+ })
+ return metadata
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):
+ if hasattr(self, 'override_' + key):
+ return getattr(self, 'override_' + key)
key = key if self.in_default_lang else 'lang_%s' % key
return self._expand_settings(key)
def _update_content(self, content, siteurl):
- """Change all the relative paths of the content to relative paths
+ """Update the content attribute.
+
+ Change all the relative paths of the content to relative paths
suitable for the ouput content.
:param content: content resource that will be passed to the templates.
:param siteurl: siteurl which is locally generated by the writer in
- case of RELATIVE_URLS.
+ case of RELATIVE_URLS.
"""
hrefs = re.compile(r"""
(?P<\s*[^\>]* # match tag with src and href attr
@@ -151,59 +181,68 @@ class Page(object):
what = m.group('what')
value = m.group('value')
origin = m.group('path')
+
# we support only filename for now. the plan is to support
# categories, tags, etc. in the future, but let's keep things
# simple for now.
+
+ # XXX Put this in a different location.
if what == 'filename':
if value.startswith('/'):
value = value[1:]
else:
- # relative to the filename of this content
- value = self.get_relative_filename(
+ # relative to the source path of this content
+ value = self.get_relative_source_path(
os.path.join(self.relative_dir, value)
)
if value in self._context['filenames']:
origin = '/'.join((siteurl,
self._context['filenames'][value].url))
+ origin = origin.replace('\\', '/') # Fow windows paths.
else:
- logger.warning(u"Unable to find {fn}, skipping url"
- " replacement".format(fn=value))
+ logger.warning("Unable to find {fn}, skipping url"
+ " replacement".format(fn=value))
- return m.group('markup') + m.group('quote') + origin \
- + m.group('quote')
+ return ''.join((m.group('markup'), m.group('quote'), origin,
+ m.group('quote')))
return hrefs.sub(replacer, content)
@memoized
def get_content(self, siteurl):
- return self._update_content(
- self._get_content() if hasattr(self, "_get_content")
- else self._content,
- siteurl)
+
+ if hasattr(self, '_get_content'):
+ content = self._get_content()
+ else:
+ content = self._content
+ return self._update_content(content, siteurl)
@property
def content(self):
return self.get_content(self._context['localsiteurl'])
def _get_summary(self):
- """Returns the summary of an article, based on the summary metadata
- if it is set, else truncate the content."""
+ """Returns the summary of an article.
+
+ This is based on the summary metadata if set, otherwise truncate the
+ content.
+ """
if hasattr(self, '_summary'):
return self._summary
- else:
- if self.settings['SUMMARY_MAX_LENGTH']:
- return truncate_html_words(self.content,
- self.settings['SUMMARY_MAX_LENGTH'])
+
+ if self.settings['SUMMARY_MAX_LENGTH'] is None:
return self.content
+ return truncate_html_words(self.content,
+ self.settings['SUMMARY_MAX_LENGTH'])
+
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'))
@@ -213,28 +252,36 @@ class Page(object):
else:
return self.default_template
- def get_relative_filename(self, filename=None):
+ def get_relative_source_path(self, source_path=None):
"""Return the relative path (from the content path) to the given
- filename.
+ source_path.
- If no filename is specified, use the filename of this content object.
+ If no source path is specified, use the source path of this
+ content object.
"""
- if not filename:
- filename = self.filename
+ if not source_path:
+ source_path = self.source_path
+ if source_path is None:
+ return None
return os.path.relpath(
- os.path.abspath(os.path.join(self.settings['PATH'], filename)),
+ os.path.abspath(os.path.join(self.settings['PATH'], source_path)),
os.path.abspath(self.settings['PATH'])
)
@property
def relative_dir(self):
return os.path.dirname(os.path.relpath(
- os.path.abspath(self.filename),
+ os.path.abspath(self.source_path),
os.path.abspath(self.settings['PATH']))
)
+class Page(Content):
+ mandatory_properties = ('title',)
+ default_template = 'page'
+
+
class Article(Page):
mandatory_properties = ('title', 'date', 'category')
default_template = 'article'
@@ -244,82 +291,26 @@ class Quote(Page):
base_properties = ('author', 'date')
-class URLWrapper(object):
- def __init__(self, name, settings):
- self.name = unicode(name)
- self.slug = slugify(self.name)
- self.settings = settings
+@python_2_unicode_compatible
+class Static(Page):
+ @deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
+ def filepath():
+ return None
- def as_dict(self):
- return self.__dict__
+ @deprecated_attribute(old='src', new='source_path', since=(3, 2, 0))
+ def src():
+ return None
- def __hash__(self):
- return hash(self.name)
-
- def __eq__(self, other):
- return self.name == unicode(other)
-
- def __str__(self):
- return str(self.name.encode('utf-8', 'replace'))
-
- def __unicode__(self):
- return self.name
-
- def _from_settings(self, key, get_page_name=False):
- """Returns URL information as defined in settings.
- When get_page_name=True returns URL without anything after {slug}
- e.g. if in settings: CATEGORY_URL="cat/{slug}.html" this returns "cat/{slug}"
- Useful for pagination."""
- setting = "%s_%s" % (self.__class__.__name__.upper(), key)
- value = self.settings[setting]
- if not isinstance(value, basestring):
- logger.warning(u'%s is set to %s' % (setting, value))
- return value
- else:
- if get_page_name:
- return unicode(os.path.splitext(value)[0]).format(**self.as_dict())
- else:
- return unicode(value).format(**self.as_dict())
-
- page_name = property(functools.partial(_from_settings, key='URL', get_page_name=True))
- 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
-
-
-class StaticContent(object):
- def __init__(self, src, dst=None, settings=None):
- if not settings:
- settings = copy.deepcopy(_DEFAULT_CONFIG)
- self.src = src
- self.url = dst or src
- self.filepath = os.path.join(settings['PATH'], src)
- self.save_as = os.path.join(settings['OUTPUT_PATH'], self.url)
-
- def __str__(self):
- return str(self.filepath.encode('utf-8', 'replace'))
-
- def __unicode__(self):
- return self.filepath
+ @deprecated_attribute(old='dst', new='save_as', since=(3, 2, 0))
+ def dst():
+ return None
def is_valid_content(content, f):
try:
content.check_properties()
return True
- except NameError, e:
- logger.error(u"Skipping %s: impossible to find informations about"
- "'%s'" % (f, e))
+ except NameError as e:
+ logger.error("Skipping %s: could not find information about "
+ "'%s'" % (f, e))
return False
diff --git a/pelican/generators.py b/pelican/generators.py
index b5c1b944..75b61df2 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -1,26 +1,31 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
import os
import math
import random
import logging
import datetime
-import subprocess
import shutil
from codecs import open
from collections import defaultdict
from functools import partial
-from itertools import chain
+from itertools import chain, groupby
from operator import attrgetter, itemgetter
-from jinja2 import (Environment, FileSystemLoader, PrefixLoader, ChoiceLoader,
- BaseLoader, TemplateNotFound)
+from jinja2 import (
+ Environment, FileSystemLoader, PrefixLoader, ChoiceLoader, BaseLoader,
+ TemplateNotFound
+)
-from pelican.contents import Article, Page, Category, StaticContent, \
- is_valid_content
+from pelican.contents import (
+ Article, Page, Category, Static, is_valid_content
+)
from pelican.readers import read_file
-from pelican.utils import copy, process_translations, mkdir_p
+from pelican.utils import copy, process_translations, mkdir_p, DateFormatter
from pelican import signals
+import pelican.utils
logger = logging.getLogger(__name__)
@@ -42,7 +47,7 @@ class Generator(object):
self._templates_path = []
self._templates_path.append(os.path.expanduser(
os.path.join(self.theme, 'templates')))
- self._templates_path += self.settings.get('EXTRA_TEMPLATES_PATHS', [])
+ self._templates_path += self.settings['EXTRA_TEMPLATES_PATHS']
theme_path = os.path.dirname(os.path.abspath(__file__))
@@ -55,13 +60,16 @@ class Generator(object):
simple_loader, # implicit inheritance
PrefixLoader({'!simple': simple_loader}) # explicit one
]),
- extensions=self.settings.get('JINJA_EXTENSIONS', []),
+ extensions=self.settings['JINJA_EXTENSIONS'],
)
logger.debug('template list: {0}'.format(self.env.list_templates()))
+ # provide utils.strftime as a jinja filter
+ self.env.filters.update({'strftime': DateFormatter()})
+
# get custom Jinja filters from user settings
- custom_filters = self.settings.get('JINJA_FILTERS', {})
+ custom_filters = self.settings['JINJA_FILTERS']
self.env.filters.update(custom_filters)
signals.generator_init.send(self)
@@ -75,10 +83,25 @@ class Generator(object):
try:
self._templates[name] = self.env.get_template(name + '.html')
except TemplateNotFound:
- raise Exception('[templates] unable to load %s.html from %s' \
- % (name, self._templates_path))
+ raise Exception(
+ ('[templates] unable to load %s.html from %s'
+ % (name, self._templates_path)))
return self._templates[name]
+ def _include_path(self, path, extensions=None):
+ """Inclusion logic for .get_files(), returns True/False
+
+ :param path: the path which might be including
+ :param extensions: the list of allowed extensions (if False, all
+ extensions are allowed)
+ """
+ if extensions is None:
+ extensions = self.markup
+ basename = os.path.basename(path)
+ if extensions is False or basename.endswith(extensions):
+ return True
+ return False
+
def get_files(self, path, exclude=[], extensions=None):
"""Return a list of files to use, based on rules
@@ -87,29 +110,23 @@ class Generator(object):
:param extensions: the list of allowed extensions (if False, all
extensions are allowed)
"""
- if extensions is None:
- extensions = self.markup
-
files = []
- try:
- iter = os.walk(path, followlinks=True)
- except TypeError: # python 2.5 does not support followlinks
- iter = os.walk(path)
-
- for root, dirs, temp_files in iter:
- for e in exclude:
- if e in dirs:
- dirs.remove(e)
- for f in temp_files:
- if extensions is False or \
- (True in [f.endswith(ext) for ext in extensions]):
- files.append(os.sep.join((root, f)))
+ if os.path.isdir(path):
+ for root, dirs, temp_files in os.walk(path, followlinks=True):
+ for e in exclude:
+ if e in dirs:
+ dirs.remove(e)
+ for f in temp_files:
+ fp = os.path.join(root, f)
+ if self._include_path(fp, extensions):
+ files.append(fp)
+ elif os.path.exists(path) and self._include_path(path, extensions):
+ files.append(path) # can't walk non-directories
return files
- def add_filename(self, content):
- location = os.path.relpath(os.path.abspath(content.filename),
- os.path.abspath(self.path))
+ def add_source_path(self, content):
+ location = content.get_relative_source_path()
self.context['filenames'][location] = content
def _update_context(self, items):
@@ -119,7 +136,7 @@ class Generator(object):
for item in items:
value = getattr(self, item)
if hasattr(value, 'items'):
- value = value.items()
+ value = list(value.items()) # py3k safeguard for iterators
self.context[item] = value
@@ -133,10 +150,10 @@ class _FileLoader(BaseLoader):
if template != self.path or not os.path.exists(self.fullpath):
raise TemplateNotFound(template)
mtime = os.path.getmtime(self.fullpath)
- with file(self.fullpath) as f:
- source = f.read().decode('utf-8')
- return source, self.fullpath, \
- lambda: mtime == os.path.getmtime(self.fullpath)
+ with open(self.fullpath, 'r', encoding='utf-8') as f:
+ source = f.read()
+ return (source, self.fullpath,
+ lambda: mtime == os.path.getmtime(self.fullpath))
class TemplatePagesGenerator(Generator):
@@ -146,7 +163,7 @@ class TemplatePagesGenerator(Generator):
self.env.loader.loaders.insert(0, _FileLoader(source, self.path))
try:
template = self.env.get_template(source)
- rurls = self.settings.get('RELATIVE_URLS')
+ rurls = self.settings['RELATIVE_URLS']
writer.write_file(dest, template, self.context, rurls)
finally:
del self.env.loader.loaders[0]
@@ -164,8 +181,8 @@ class ArticlesGenerator(Generator):
self.categories = defaultdict(list)
self.related_posts = []
self.authors = defaultdict(list)
- super(ArticlesGenerator, self).__init__(*args, **kwargs)
self.drafts = []
+ super(ArticlesGenerator, self).__init__(*args, **kwargs)
signals.article_generator_init.send(self)
def generate_feeds(self, writer):
@@ -179,8 +196,8 @@ class ArticlesGenerator(Generator):
writer.write_feed(self.articles, self.context,
self.settings['FEED_RSS'], feed_type='rss')
- if self.settings.get('FEED_ALL_ATOM') or \
- self.settings.get('FEED_ALL_RSS'):
+ if (self.settings.get('FEED_ALL_ATOM')
+ or self.settings.get('FEED_ALL_RSS')):
all_articles = list(self.articles)
for article in self.articles:
all_articles.extend(article.translations)
@@ -192,34 +209,37 @@ class ArticlesGenerator(Generator):
if self.settings.get('FEED_ALL_RSS'):
writer.write_feed(all_articles, self.context,
- self.settings['FEED_ALL_RSS'], feed_type='rss')
+ self.settings['FEED_ALL_RSS'],
+ feed_type='rss')
for cat, arts in self.categories:
arts.sort(key=attrgetter('date'), reverse=True)
if self.settings.get('CATEGORY_FEED_ATOM'):
writer.write_feed(arts, self.context,
- self.settings['CATEGORY_FEED_ATOM'] % cat)
+ self.settings['CATEGORY_FEED_ATOM']
+ % cat.slug)
if self.settings.get('CATEGORY_FEED_RSS'):
writer.write_feed(arts, self.context,
- self.settings['CATEGORY_FEED_RSS'] % cat,
- feed_type='rss')
+ self.settings['CATEGORY_FEED_RSS']
+ % cat.slug, feed_type='rss')
- if self.settings.get('TAG_FEED_ATOM') \
- or self.settings.get('TAG_FEED_RSS'):
+ if (self.settings.get('TAG_FEED_ATOM')
+ or self.settings.get('TAG_FEED_RSS')):
for tag, arts in self.tags.items():
arts.sort(key=attrgetter('date'), reverse=True)
if self.settings.get('TAG_FEED_ATOM'):
writer.write_feed(arts, self.context,
- self.settings['TAG_FEED_ATOM'] % tag)
+ self.settings['TAG_FEED_ATOM']
+ % tag.slug)
if self.settings.get('TAG_FEED_RSS'):
writer.write_feed(arts, self.context,
- self.settings['TAG_FEED_RSS'] % tag,
+ self.settings['TAG_FEED_RSS'] % tag.slug,
feed_type='rss')
- if self.settings.get('TRANSLATION_FEED_ATOM') or \
- self.settings.get('TRANSLATION_FEED_RSS'):
+ if (self.settings.get('TRANSLATION_FEED_ATOM')
+ or self.settings.get('TRANSLATION_FEED_RSS')):
translations_feeds = defaultdict(list)
for article in chain(self.articles, self.translations):
translations_feeds[article.lang].append(article)
@@ -240,10 +260,50 @@ class ArticlesGenerator(Generator):
write(article.save_as, self.get_template(article.template),
self.context, article=article, category=article.category)
+ def generate_period_archives(self, write):
+ """Generate per-year, per-month, and per-day archives."""
+ try:
+ template = self.get_template('period_archives')
+ except Exception:
+ template = self.get_template('archives')
+
+ def _generate_period_archives(dates, key, save_as_fmt):
+ """Generate period archives from `dates`, grouped by
+ `key` and written to `save_as`.
+ """
+ # `dates` is already sorted by date
+ for _period, group in groupby(dates, key=key):
+ archive = list(group)
+ # arbitrarily grab the first date so that the usual
+ # format string syntax can be used for specifying the
+ # period archive dates
+ date = archive[0].date
+ save_as = save_as_fmt.format(date=date)
+ write(save_as, template, self.context,
+ dates=archive, blog=True)
+
+ period_save_as = {
+ 'year' : self.settings['YEAR_ARCHIVE_SAVE_AS'],
+ 'month': self.settings['MONTH_ARCHIVE_SAVE_AS'],
+ 'day' : self.settings['DAY_ARCHIVE_SAVE_AS'],
+ }
+
+ period_date_key = {
+ 'year' : attrgetter('date.year'),
+ 'month': attrgetter('date.year', 'date.month'),
+ 'day' : attrgetter('date.year', 'date.month', 'date.day')
+ }
+
+ for period in 'year', 'month', 'day':
+ save_as = period_save_as[period]
+ if save_as:
+ key = period_date_key[period]
+ _generate_period_archives(self.dates, key, save_as)
+
def generate_direct_templates(self, write):
"""Generate direct templates pages"""
- PAGINATED_TEMPLATES = self.settings.get('PAGINATED_DIRECT_TEMPLATES')
- for template in self.settings.get('DIRECT_TEMPLATES'):
+ PAGINATED_TEMPLATES = self.settings['PAGINATED_DIRECT_TEMPLATES']
+ for template in self.settings['DIRECT_TEMPLATES']:
paginated = {}
if template in PAGINATED_TEMPLATES:
paginated = {'articles': self.articles, 'dates': self.dates}
@@ -253,8 +313,8 @@ class ArticlesGenerator(Generator):
continue
write(save_as, self.get_template(template),
- self.context, blog=True, paginated=paginated,
- page_name=template)
+ self.context, blog=True, paginated=paginated,
+ page_name=os.path.splitext(save_as)[0])
def generate_tags(self, write):
"""Generate Tags pages."""
@@ -265,7 +325,7 @@ class ArticlesGenerator(Generator):
write(tag.save_as, tag_template, self.context, tag=tag,
articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
- page_name=tag.page_name)
+ page_name=tag.page_name, all_articles=self.articles)
def generate_categories(self, write):
"""Generate category pages."""
@@ -275,7 +335,7 @@ class ArticlesGenerator(Generator):
write(cat.save_as, category_template, self.context,
category=cat, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
- page_name=cat.page_name)
+ page_name=cat.page_name, all_articles=self.articles)
def generate_authors(self, write):
"""Generate Author pages."""
@@ -285,23 +345,25 @@ class ArticlesGenerator(Generator):
write(aut.save_as, author_template, self.context,
author=aut, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
- page_name=aut.page_name)
+ page_name=aut.page_name, all_articles=self.articles)
def generate_drafts(self, write):
"""Generate drafts pages."""
for article in self.drafts:
- write('drafts/%s.html' % article.slug,
+ write(os.path.join('drafts', '%s.html' % article.slug),
self.get_template(article.template), self.context,
- article=article, category=article.category)
+ article=article, category=article.category,
+ all_articles=self.articles)
def generate_pages(self, writer):
"""Generate the pages on the disk"""
write = partial(writer.write_file,
- relative_urls=self.settings.get('RELATIVE_URLS'))
+ relative_urls=self.settings['RELATIVE_URLS'])
# to minimize the number of relative path stuff modification
# in writer, articles pass first
self.generate_articles(write)
+ self.generate_period_archives(write)
self.generate_direct_templates(write)
# and subfolders after that
@@ -323,8 +385,8 @@ class ArticlesGenerator(Generator):
try:
signals.article_generate_preread.send(self)
content, metadata = read_file(f, settings=self.settings)
- except Exception, e:
- logger.warning(u'Could not process %s\n%s' % (f, str(e)))
+ except Exception as e:
+ logger.warning('Could not process %s\n%s' % (f, str(e)))
continue
# if no category is set, use the name of the path as a category
@@ -333,8 +395,7 @@ class ArticlesGenerator(Generator):
if (self.settings['USE_FOLDER_AS_CATEGORY']
and os.path.dirname(f) != article_path):
# if the article is in a subdirectory
- category = os.path.basename(os.path.dirname(f))\
- .decode('utf-8')
+ category = os.path.basename(os.path.dirname(f))
else:
# if the article is not in a subdirectory
category = self.settings['DEFAULT_CATEGORY']
@@ -352,33 +413,35 @@ class ArticlesGenerator(Generator):
signals.article_generate_context.send(self, metadata=metadata)
article = Article(content, metadata, settings=self.settings,
- filename=f, context=self.context)
+ source_path=f, context=self.context)
if not is_valid_content(article, f):
continue
- self.add_filename(article)
+ self.add_source_path(article)
if article.status == "published":
- if hasattr(article, 'tags'):
- for tag in article.tags:
- self.tags[tag].append(article)
all_articles.append(article)
elif article.status == "draft":
self.drafts.append(article)
else:
- logger.warning(u"Unknown status %s for file %s, skipping it." %
- (repr(unicode.encode(article.status, 'utf-8')),
+ logger.warning("Unknown status %s for file %s, skipping it." %
+ (repr(article.status),
repr(f)))
self.articles, self.translations = process_translations(all_articles)
for article in self.articles:
- # only main articles are listed in categories, not translations
+ # only main articles are listed in categories and tags
+ # not translations
self.categories[article.category].append(article)
+ if hasattr(article, 'tags'):
+ for tag in article.tags:
+ self.tags[tag].append(article)
# ignore blank authors as well as undefined
- if hasattr(article,'author') and article.author.name != '':
+ if hasattr(article, 'author') and article.author.name != '':
self.authors[article.author].append(article)
+
# sort the articles by date
self.articles.sort(key=attrgetter('date'), reverse=True)
self.dates = list(self.articles)
@@ -394,7 +457,7 @@ class ArticlesGenerator(Generator):
tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
- tags = map(itemgetter(1), tag_cloud)
+ tags = list(map(itemgetter(1), tag_cloud))
if tags:
max_count = max(tags)
steps = self.settings.get('TAG_CLOUD_STEPS')
@@ -416,11 +479,10 @@ class ArticlesGenerator(Generator):
# order the categories per name
self.categories = list(self.categories.items())
self.categories.sort(
- key=lambda item: item[0].name,
reverse=self.settings['REVERSE_CATEGORY_ORDER'])
self.authors = list(self.authors.items())
- self.authors.sort(key=lambda item: item[0].name)
+ self.authors.sort()
self._update_context(('articles', 'dates', 'tags', 'categories',
'tag_cloud', 'authors', 'related_posts'))
@@ -450,38 +512,41 @@ class PagesGenerator(Generator):
exclude=self.settings['PAGE_EXCLUDES']):
try:
content, metadata = read_file(f, settings=self.settings)
- except Exception, e:
- logger.warning(u'Could not process %s\n%s' % (f, str(e)))
+ except Exception as e:
+ logger.warning('Could not process %s\n%s' % (f, str(e)))
continue
signals.pages_generate_context.send(self, metadata=metadata)
page = Page(content, metadata, settings=self.settings,
- filename=f, context=self.context)
+ source_path=f, context=self.context)
if not is_valid_content(page, f):
continue
- self.add_filename(page)
+ self.add_source_path(page)
if page.status == "published":
all_pages.append(page)
elif page.status == "hidden":
hidden_pages.append(page)
else:
- logger.warning(u"Unknown status %s for file %s, skipping it." %
- (repr(unicode.encode(page.status, 'utf-8')),
+ logger.warning("Unknown status %s for file %s, skipping it." %
+ (repr(page.status),
repr(f)))
self.pages, self.translations = process_translations(all_pages)
- self.hidden_pages, self.hidden_translations = process_translations(hidden_pages)
+ self.hidden_pages, self.hidden_translations = (
+ process_translations(hidden_pages))
self._update_context(('pages', ))
self.context['PAGES'] = self.pages
+ signals.pages_generator_finalized.send(self)
+
def generate_output(self, writer):
for page in chain(self.translations, self.pages,
self.hidden_translations, self.hidden_pages):
writer.write_file(page.save_as, self.get_template(page.template),
self.context, page=page,
- relative_urls=self.settings.get('RELATIVE_URLS'))
+ relative_urls=self.settings['RELATIVE_URLS'])
class StaticGenerator(Generator):
@@ -503,25 +568,42 @@ class StaticGenerator(Generator):
for f in self.get_files(
os.path.join(self.path, static_path), extensions=False):
f_rel = os.path.relpath(f, self.path)
+ content, metadata = read_file(
+ f, fmt='static', settings=self.settings)
# TODO remove this hardcoded 'static' subdirectory
- sc = StaticContent(f_rel, os.path.join('static', f_rel),
- settings=self.settings)
+ metadata['save_as'] = os.path.join('static', f_rel)
+ metadata['url'] = pelican.utils.path_to_url(metadata['save_as'])
+ sc = Static(
+ content=None,
+ metadata=metadata,
+ settings=self.settings,
+ source_path=f_rel)
self.staticfiles.append(sc)
- self.context['filenames'][f_rel] = sc
+ self.add_source_path(sc)
# same thing for FILES_TO_COPY
for src, dest in self.settings['FILES_TO_COPY']:
- sc = StaticContent(src, dest, settings=self.settings)
+ content, metadata = read_file(
+ src, fmt='static', settings=self.settings)
+ metadata['save_as'] = dest
+ metadata['url'] = pelican.utils.path_to_url(metadata['save_as'])
+ sc = Static(
+ content=None,
+ metadata={'save_as': dest},
+ settings=self.settings,
+ source_path=src)
self.staticfiles.append(sc)
- self.context['filenames'][src] = sc
+ self.add_source_path(sc)
def generate_output(self, writer):
self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme,
- 'theme', self.output_path, '.')
- # copy all StaticContent files
+ 'theme', self.output_path, os.curdir)
+ # copy all Static files
for sc in self.staticfiles:
- mkdir_p(os.path.dirname(sc.save_as))
- shutil.copy(sc.filepath, sc.save_as)
- logger.info('copying %s to %s' % (sc.filepath, sc.save_as))
+ source_path = os.path.join(self.path, sc.source_path)
+ save_as = os.path.join(self.output_path, sc.save_as)
+ mkdir_p(os.path.dirname(save_as))
+ shutil.copy(source_path, save_as)
+ logger.info('copying {} to {}'.format(sc.source_path, sc.save_as))
class PdfGenerator(Generator):
@@ -531,12 +613,8 @@ class PdfGenerator(Generator):
super(PdfGenerator, self).__init__(*args, **kwargs)
try:
from rst2pdf.createpdf import RstToPdf
- pdf_style_path = os.path.join(self.settings['PDF_STYLE_PATH']) \
- if 'PDF_STYLE_PATH' in self.settings.keys() \
- else ''
- pdf_style = self.settings['PDF_STYLE'] if 'PDF_STYLE' \
- in self.settings.keys() \
- else 'twelvepoint'
+ pdf_style_path = os.path.join(self.settings['PDF_STYLE_PATH'])
+ pdf_style = self.settings['PDF_STYLE']
self.pdfcreator = RstToPdf(breakside=0,
stylesheets=[pdf_style],
style_path=[pdf_style_path])
@@ -544,13 +622,13 @@ class PdfGenerator(Generator):
raise Exception("unable to find rst2pdf")
def _create_pdf(self, obj, output_path):
- if obj.filename.endswith(".rst"):
+ if obj.source_path.endswith('.rst'):
filename = obj.slug + ".pdf"
output_pdf = os.path.join(output_path, filename)
- # print "Generating pdf for", obj.filename, " in ", output_pdf
- with open(obj.filename) as f:
+ # print('Generating pdf for', obj.source_path, 'in', output_pdf)
+ with open(obj.source_path) as f:
self.pdfcreator.createPdf(text=f.read(), output=output_pdf)
- logger.info(u' [ok] writing %s' % output_pdf)
+ logger.info(' [ok] writing %s' % output_pdf)
def generate_context(self):
pass
@@ -558,7 +636,7 @@ class PdfGenerator(Generator):
def generate_output(self, writer=None):
# we don't use the writer passed as argument here
# since we write our own files
- logger.info(u' Generating PDF files...')
+ logger.info(' Generating PDF files...')
pdf_path = os.path.join(self.output_path, 'pdf')
if not os.path.exists(pdf_path):
try:
@@ -573,16 +651,20 @@ class PdfGenerator(Generator):
for page in self.context['pages']:
self._create_pdf(page, pdf_path)
+
class SourceFileGenerator(Generator):
def generate_context(self):
self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION']
- def _create_source(self, obj, output_path):
- filename = os.path.splitext(obj.save_as)[0]
- dest = os.path.join(output_path, filename + self.output_extension)
- copy('', obj.filename, dest)
+ def _create_source(self, obj):
+ output_path, _ = os.path.splitext(obj.save_as)
+ dest = os.path.join(self.output_path,
+ output_path + self.output_extension)
+ copy('', obj.source_path, dest)
def generate_output(self, writer=None):
- logger.info(u' Generating source files...')
- for object in chain(self.context['articles'], self.context['pages']):
- self._create_source(object, self.output_path)
+ logger.info(' Generating source files...')
+ for obj in chain(self.context['articles'], self.context['pages']):
+ self._create_source(obj)
+ for obj_trans in obj.translations:
+ self._create_source(obj_trans)
diff --git a/pelican/log.py b/pelican/log.py
index 9590d7f6..bde8037e 100644
--- a/pelican/log.py
+++ b/pelican/log.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
__all__ = [
'init'
]
@@ -9,7 +12,7 @@ import logging
from logging import Formatter, getLogger, StreamHandler, DEBUG
-RESET_TERM = u'\033[0;m'
+RESET_TERM = '\033[0;m'
COLOR_CODES = {
'red': 31,
@@ -24,47 +27,47 @@ COLOR_CODES = {
def ansi(color, text):
"""Wrap text in an ansi escape sequence"""
code = COLOR_CODES[color]
- return u'\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
+ return '\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
class ANSIFormatter(Formatter):
- """
- Convert a `logging.LogReport' object into colored text, using ANSI escape sequences.
- """
- ## colors:
+ """Convert a `logging.LogRecord' object into colored text, using ANSI
+ escape sequences.
+ """
def format(self, record):
- if record.levelname is 'INFO':
- return ansi('cyan', '-> ') + unicode(record.msg)
- elif record.levelname is 'WARNING':
- return ansi('yellow', record.levelname) + ': ' + unicode(record.msg)
- elif record.levelname is 'ERROR':
- return ansi('red', record.levelname) + ': ' + unicode(record.msg)
- elif record.levelname is 'CRITICAL':
- return ansi('bgred', record.levelname) + ': ' + unicode(record.msg)
- elif record.levelname is 'DEBUG':
- return ansi('bggrey', record.levelname) + ': ' + unicode(record.msg)
+ msg = record.getMessage()
+ if record.levelname == 'INFO':
+ return ansi('cyan', '-> ') + msg
+ elif record.levelname == 'WARNING':
+ return ansi('yellow', record.levelname) + ': ' + msg
+ elif record.levelname == 'ERROR':
+ return ansi('red', record.levelname) + ': ' + msg
+ elif record.levelname == 'CRITICAL':
+ return ansi('bgred', record.levelname) + ': ' + msg
+ elif record.levelname == 'DEBUG':
+ return ansi('bggrey', record.levelname) + ': ' + msg
else:
- return ansi('white', record.levelname) + ': ' + unicode(record.msg)
+ return ansi('white', record.levelname) + ': ' + msg
class TextFormatter(Formatter):
"""
- Convert a `logging.LogReport' object into text.
+ Convert a `logging.LogRecord' object into text.
"""
def format(self, record):
- if not record.levelname or record.levelname is 'INFO':
- return record.msg
+ if not record.levelname or record.levelname == 'INFO':
+ return record.getMessage()
else:
- return record.levelname + ': ' + record.msg
+ return record.levelname + ': ' + record.getMessage()
def init(level=None, logger=getLogger(), handler=StreamHandler()):
logger = logging.getLogger()
- if os.isatty(sys.stdout.fileno()) \
- and not sys.platform.startswith('win'):
+ if (os.isatty(sys.stdout.fileno())
+ and not sys.platform.startswith('win')):
fmt = ANSIFormatter()
else:
fmt = TextFormatter()
diff --git a/pelican/paginator.py b/pelican/paginator.py
index fe871491..067215c2 100644
--- a/pelican/paginator.py
+++ b/pelican/paginator.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
# From django.core.paginator
from math import ceil
@@ -37,7 +40,7 @@ class Paginator(object):
Returns a 1-based range of pages for iterating through within
a template for loop.
"""
- return range(1, self.num_pages + 1)
+ return list(range(1, self.num_pages + 1))
page_range = property(_get_page_range)
diff --git a/pelican/plugins/__init__.py b/pelican/plugins/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/pelican/plugins/assets.py b/pelican/plugins/assets.py
deleted file mode 100644
index b5d1cf76..00000000
--- a/pelican/plugins/assets.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Asset management plugin for Pelican
-===================================
-
-This plugin allows you to use the `webassets`_ module to manage assets such as
-CSS and JS files.
-
-The ASSET_URL is set to a relative url to honor Pelican's RELATIVE_URLS
-setting. This requires the use of SITEURL in the templates::
-
-
-
-.. _webassets: https://webassets.readthedocs.org/
-
-"""
-
-import os
-import logging
-
-from pelican import signals
-from webassets import Environment
-from webassets.ext.jinja2 import AssetsExtension
-
-
-def add_jinja2_ext(pelican):
- """Add Webassets to Jinja2 extensions in Pelican settings."""
-
- pelican.settings['JINJA_EXTENSIONS'].append(AssetsExtension)
-
-
-def create_assets_env(generator):
- """Define the assets environment and pass it to the generator."""
-
- assets_url = 'theme/'
- assets_src = os.path.join(generator.output_path, 'theme')
- generator.env.assets_environment = Environment(assets_src, assets_url)
-
- logger = logging.getLogger(__name__)
- if logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG":
- generator.env.assets_environment.debug = True
-
-
-def register():
- """Plugin registration."""
-
- signals.initialized.connect(add_jinja2_ext)
- signals.generator_init.connect(create_assets_env)
diff --git a/pelican/plugins/github_activity.py b/pelican/plugins/github_activity.py
deleted file mode 100644
index f2ba1da7..00000000
--- a/pelican/plugins/github_activity.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- Copyright (c) Marco Milanesi
-
- A plugin to list your Github Activity
- To enable it set in your pelican config file the GITHUB_ACTIVITY_FEED
- parameter pointing to your github activity feed.
-
- for example my personal activity feed is:
-
- https://github.com/kpanic.atom
-
- in your template just write a for in jinja2 syntax against the
- github_activity variable.
-
- i.e.
-
-
-
Github Activity
-
-
- {% for entry in github_activity %}
-
{{ entry[0] }} {{ entry[1] }}
- {% endfor %}
-
-
-
- github_activity is a list containing a list. The first element is the title
- and the second element is the raw html from github
-"""
-
-from pelican import signals
-
-
-class GitHubActivity():
- """
- A class created to fetch github activity with feedparser
- """
- def __init__(self, generator):
- try:
- import feedparser
- self.activities = feedparser.parse(
- generator.settings['GITHUB_ACTIVITY_FEED'])
- except ImportError:
- raise Exception("Unable to find feedparser")
-
- def fetch(self):
- """
- returns a list of html snippets fetched from github actitivy feed
- """
-
- entries = []
- for activity in self.activities['entries']:
- entries.append(
- [element for element in [activity['title'],
- activity['content'][0]['value']]])
-
- return entries
-
-
-def fetch_github_activity(gen, metadata):
- """
- registered handler for the github activity plugin
- it puts in generator.context the html needed to be displayed on a
- template
- """
-
- if 'GITHUB_ACTIVITY_FEED' in gen.settings.keys():
- gen.context['github_activity'] = gen.plugin_instance.fetch()
-
-
-def feed_parser_initialization(generator):
- """
- Initialization of feed parser
- """
-
- generator.plugin_instance = GitHubActivity(generator)
-
-
-def register():
- """
- Plugin registration
- """
- signals.article_generator_init.connect(feed_parser_initialization)
- signals.article_generate_context.connect(fetch_github_activity)
diff --git a/pelican/plugins/global_license.py b/pelican/plugins/global_license.py
deleted file mode 100644
index 9a0f5206..00000000
--- a/pelican/plugins/global_license.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from pelican import signals
-
-"""
-License plugin for Pelican
-==========================
-
-This plugin allows you to define a LICENSE setting and adds the contents of that
-license variable to the article's context, making that variable available to use
-from within your theme's templates.
-
-Settings:
----------
-
-Define LICENSE in your settings file with the contents of your default license.
-
-"""
-
-def add_license(generator, metadata):
- if 'license' not in metadata.keys()\
- and 'LICENSE' in generator.settings.keys():
- metadata['license'] = generator.settings['LICENSE']
-
-def register():
- signals.article_generate_context.connect(add_license)
diff --git a/pelican/plugins/gravatar.py b/pelican/plugins/gravatar.py
deleted file mode 100644
index a4d11456..00000000
--- a/pelican/plugins/gravatar.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import hashlib
-
-from pelican import signals
-"""
-Gravatar plugin for Pelican
-===========================
-
-This plugin assigns the ``author_gravatar`` variable to the Gravatar URL and
-makes the variable available within the article's context.
-
-Settings:
----------
-
-Add AUTHOR_EMAIL to your settings file to define the default author's email
-address. Obviously, that email address must be associated with a Gravatar
-account.
-
-Article metadata:
-------------------
-
-:email: article's author email
-
-If one of them are defined, the author_gravatar variable is added to the
-article's context.
-"""
-
-def add_gravatar(generator, metadata):
-
- #first check email
- if 'email' not in metadata.keys()\
- and 'AUTHOR_EMAIL' in generator.settings.keys():
- metadata['email'] = generator.settings['AUTHOR_EMAIL']
-
- #then add gravatar url
- if 'email' in metadata.keys():
- gravatar_url = "http://www.gravatar.com/avatar/" + \
- hashlib.md5(metadata['email'].lower()).hexdigest()
- metadata["author_gravatar"] = gravatar_url
-
-
-def register():
- signals.article_generate_context.connect(add_gravatar)
diff --git a/pelican/plugins/gzip_cache.py b/pelican/plugins/gzip_cache.py
deleted file mode 100644
index 784a6ca0..00000000
--- a/pelican/plugins/gzip_cache.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (c) 2012 Matt Layman
-'''A plugin to create .gz cache files for optimization.'''
-
-import gzip
-import logging
-import os
-
-from pelican import signals
-
-logger = logging.getLogger(__name__)
-
-# A list of file types to exclude from possible compression
-EXCLUDE_TYPES = [
- # Compressed types
- '.bz2',
- '.gz',
-
- # Audio types
- '.aac',
- '.flac',
- '.mp3',
- '.wma',
-
- # Image types
- '.gif',
- '.jpg',
- '.jpeg',
- '.png',
-
- # Video types
- '.avi',
- '.mov',
- '.mp4',
-]
-
-def create_gzip_cache(pelican):
- '''Create a gzip cache file for every file that a webserver would
- reasonably want to cache (e.g., text type files).
-
- :param pelican: The Pelican instance
- '''
- for dirpath, _, filenames in os.walk(pelican.settings['OUTPUT_PATH']):
- for name in filenames:
- if should_compress(name):
- filepath = os.path.join(dirpath, name)
- create_gzip_file(filepath)
-
-def should_compress(filename):
- '''Check if the filename is a type of file that should be compressed.
-
- :param filename: A file name to check against
- '''
- for extension in EXCLUDE_TYPES:
- if filename.endswith(extension):
- return False
-
- return True
-
-def create_gzip_file(filepath):
- '''Create a gzipped file in the same directory with a filepath.gz name.
-
- :param filepath: A file to compress
- '''
- compressed_path = filepath + '.gz'
-
- with open(filepath, 'rb') as uncompressed:
- try:
- logger.debug('Compressing: %s' % filepath)
- compressed = gzip.open(compressed_path, 'wb')
- compressed.writelines(uncompressed)
- except Exception, ex:
- logger.critical('Gzip compression failed: %s' % ex)
- finally:
- compressed.close()
-
-def register():
- signals.finalized.connect(create_gzip_cache)
-
diff --git a/pelican/plugins/html_rst_directive.py b/pelican/plugins/html_rst_directive.py
deleted file mode 100644
index d0a656f5..00000000
--- a/pelican/plugins/html_rst_directive.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from docutils import nodes
-from docutils.parsers.rst import directives, Directive
-from pelican import log
-
-"""
-HTML tags for reStructuredText
-==============================
-
-Directives
-----------
-
-.. html::
-
- (HTML code)
-
-
-Example
--------
-
-A search engine:
-
-.. html::
-
-
-
-A contact form:
-
-.. html::
-
-
-
-"""
-
-
-class RawHtml(Directive):
- required_arguments = 0
- optional_arguments = 0
- final_argument_whitespace = True
- has_content = True
-
- def run(self):
- html = u' '.join(self.content)
- node = nodes.raw('', html, format='html')
- return [node]
-
-
-
-def register():
- directives.register_directive('html', RawHtml)
-
diff --git a/pelican/plugins/initialized.py b/pelican/plugins/initialized.py
deleted file mode 100644
index 5e4cf174..00000000
--- a/pelican/plugins/initialized.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from pelican import signals
-
-def test(sender):
- print "%s initialized !!" % sender
-
-def register():
- signals.initialized.connect(test)
diff --git a/pelican/plugins/multi_part.py b/pelican/plugins/multi_part.py
deleted file mode 100644
index 0581b501..00000000
--- a/pelican/plugins/multi_part.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Copyright (c) FELD Boris
-
-Multiple part support
-=====================
-
-Create a navigation menu for multi-part related_posts
-
-Article metadata:
-------------------
-
-:parts: a unique identifier for multi-part posts, must be the same in each
-post part.
-
-Usage
------
- {% if article.metadata.parts_articles %}
-
- {% for part_article in article.metadata.parts_articles %}
- {% if part_article == article %}
-
- {% endif %}
- {% endfor %}
-
- {% endif %}
-"""
-from collections import defaultdict
-
-from pelican import signals
-
-
-def aggregate_multi_part(generator):
- multi_part = defaultdict(list)
-
- for article in generator.articles:
- if 'parts' in article.metadata:
- multi_part[article.metadata['parts']].append(article)
-
- for part_id in multi_part:
- parts = multi_part[part_id]
-
- # Sort by date
- parts.sort(key=lambda x: x.metadata['date'])
-
- for article in parts:
- article.metadata['parts_articles'] = parts
-
-
-def register():
- signals.article_generator_finalized.connect(aggregate_multi_part)
diff --git a/pelican/plugins/related_posts.py b/pelican/plugins/related_posts.py
deleted file mode 100644
index 67715023..00000000
--- a/pelican/plugins/related_posts.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from pelican import signals
-
-"""
-Related posts plugin for Pelican
-================================
-
-Adds related_posts variable to article's context
-
-Settings
---------
-To enable, add
-
- from pelican.plugins import related_posts
- PLUGINS = [related_posts]
-
-to your settings.py.
-
-Usage
------
- {% if article.related_posts %}
-
- {% for related_post in article.related_posts %}
-
{{ related_post }}
- {% endfor %}
-
- {% endif %}
-
-
-"""
-
-related_posts = []
-
-
-def add_related_posts(generator, metadata):
- if 'tags' in metadata:
- for tag in metadata['tags']:
- #print tag
- for related_article in generator.tags[tag]:
- related_posts.append(related_article)
-
- if len(related_posts) < 1:
- return
-
- relation_score = dict(zip(set(related_posts), map(related_posts.count,
- set(related_posts))))
- ranked_related = sorted(relation_score, key=relation_score.get)
-
- metadata["related_posts"] = ranked_related[:5]
-
-
-def register():
- signals.article_generate_context.connect(add_related_posts)
diff --git a/pelican/plugins/sitemap.py b/pelican/plugins/sitemap.py
deleted file mode 100644
index ebce1f04..00000000
--- a/pelican/plugins/sitemap.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import collections
-import os.path
-
-from datetime import datetime
-from logging import warning, info
-from codecs import open
-
-from pelican import signals, contents
-
-TXT_HEADER = u"""{0}/index.html
-{0}/archives.html
-{0}/tags.html
-{0}/categories.html
-"""
-
-XML_HEADER = u"""
-
-"""
-
-XML_URL = u"""
-
-{0}/{1}
-{2}
-{3}
-{4}
-
-"""
-
-XML_FOOTER = u"""
-
-"""
-
-
-def format_date(date):
- if date.tzinfo:
- tz = date.strftime('%s')
- tz = tz[:-2] + ':' + tz[-2:]
- else:
- tz = "-00:00"
- return date.strftime("%Y-%m-%dT%H:%M:%S") + tz
-
-
-class SitemapGenerator(object):
-
- def __init__(self, context, settings, path, theme, output_path, *null):
-
- self.output_path = output_path
- self.context = context
- self.now = datetime.now()
- self.siteurl = settings.get('SITEURL')
-
- self.format = 'xml'
-
- self.changefreqs = {
- 'articles': 'monthly',
- 'indexes': 'daily',
- 'pages': 'monthly'
- }
-
- self.priorities = {
- 'articles': 0.5,
- 'indexes': 0.5,
- 'pages': 0.5
- }
-
- config = settings.get('SITEMAP', {})
-
- if not isinstance(config, dict):
- warning("sitemap plugin: the SITEMAP setting must be a dict")
- else:
- fmt = config.get('format')
- pris = config.get('priorities')
- chfreqs = config.get('changefreqs')
-
- if fmt not in ('xml', 'txt'):
- warning("sitemap plugin: SITEMAP['format'] must be `txt' or `xml'")
- warning("sitemap plugin: Setting SITEMAP['format'] on `xml'")
- elif fmt == 'txt':
- self.format = fmt
- return
-
- valid_keys = ('articles', 'indexes', 'pages')
- valid_chfreqs = ('always', 'hourly', 'daily', 'weekly', 'monthly',
- 'yearly', 'never')
-
- if isinstance(pris, dict):
- for k, v in pris.iteritems():
- if k in valid_keys and not isinstance(v, (int, float)):
- default = self.priorities[k]
- warning("sitemap plugin: priorities must be numbers")
- warning("sitemap plugin: setting SITEMAP['priorities']"
- "['{0}'] on {1}".format(k, default))
- pris[k] = default
- self.priorities.update(pris)
- elif pris is not None:
- warning("sitemap plugin: SITEMAP['priorities'] must be a dict")
- warning("sitemap plugin: using the default values")
-
- if isinstance(chfreqs, dict):
- for k, v in chfreqs.iteritems():
- if k in valid_keys and v not in valid_chfreqs:
- default = self.changefreqs[k]
- warning("sitemap plugin: invalid changefreq `{0}'".format(v))
- warning("sitemap plugin: setting SITEMAP['changefreqs']"
- "['{0}'] on '{1}'".format(k, default))
- chfreqs[k] = default
- self.changefreqs.update(chfreqs)
- elif chfreqs is not None:
- warning("sitemap plugin: SITEMAP['changefreqs'] must be a dict")
- warning("sitemap plugin: using the default values")
-
-
-
- def write_url(self, page, fd):
-
- if getattr(page, 'status', 'published') != 'published':
- return
-
- page_path = os.path.join(self.output_path, page.url)
- if not os.path.exists(page_path):
- return
-
- lastmod = format_date(getattr(page, 'date', self.now))
-
- if isinstance(page, contents.Article):
- pri = self.priorities['articles']
- chfreq = self.changefreqs['articles']
- elif isinstance(page, contents.Page):
- pri = self.priorities['pages']
- chfreq = self.changefreqs['pages']
- else:
- pri = self.priorities['indexes']
- chfreq = self.changefreqs['indexes']
-
-
- if self.format == 'xml':
- fd.write(XML_URL.format(self.siteurl, page.url, lastmod, chfreq, pri))
- else:
- fd.write(self.siteurl + '/' + loc + '\n')
-
-
- def generate_output(self, writer):
- path = os.path.join(self.output_path, 'sitemap.{0}'.format(self.format))
-
- pages = self.context['pages'] + self.context['articles'] \
- + [ c for (c, a) in self.context['categories']] \
- + [ t for (t, a) in self.context['tags']] \
- + [ a for (a, b) in self.context['authors']]
-
- for article in self.context['articles']:
- pages += article.translations
-
- info('writing {0}'.format(path))
-
- with open(path, 'w', encoding='utf-8') as fd:
-
- if self.format == 'xml':
- fd.write(XML_HEADER)
- else:
- fd.write(TXT_HEADER.format(self.siteurl))
-
- FakePage = collections.namedtuple('FakePage',
- ['status',
- 'date',
- 'url'])
-
- for standard_page_url in ['index.html',
- 'archives.html',
- 'tags.html',
- 'categories.html']:
- fake = FakePage(status='published',
- date=self.now,
- url=standard_page_url)
- self.write_url(fake, fd)
-
- for page in pages:
- self.write_url(page, fd)
-
- if self.format == 'xml':
- fd.write(XML_FOOTER)
-
-
-def get_generators(generators):
- return SitemapGenerator
-
-
-def register():
- signals.get_generators.connect(get_generators)
diff --git a/pelican/readers.py b/pelican/readers.py
index 6bd2822d..816464ef 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
import os
import re
try:
@@ -20,15 +22,23 @@ try:
asciidoc = True
except ImportError:
asciidoc = False
+try:
+ from html import escape
+except ImportError:
+ from cgi import escape
+try:
+ from html.parser import HTMLParser
+except ImportError:
+ from HTMLParser import HTMLParser
from pelican.contents import Category, Tag, Author
from pelican.utils import get_date, pelican_open
-_METADATA_PROCESSORS = {
- 'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')],
+METADATA_PROCESSORS = {
+ 'tags': lambda x, y: [Tag(tag, y) for tag in x.split(',')],
'date': lambda x, y: get_date(x),
- 'status': lambda x, y: unicode.strip(x),
+ 'status': lambda x, y: x.strip(),
'category': Category,
'author': Author,
}
@@ -36,16 +46,23 @@ _METADATA_PROCESSORS = {
class Reader(object):
enabled = True
+ file_extensions = ['static']
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)
+ if name in METADATA_PROCESSORS:
+ return METADATA_PROCESSORS[name](value, self.settings)
return value
+ def read(self, source_path):
+ "No-op parser"
+ content = None
+ metadata = {}
+ return content, metadata
+
class _FieldBodyTranslator(HTMLTranslator):
@@ -85,6 +102,9 @@ class RstReader(Reader):
enabled = bool(docutils)
file_extensions = ['rst']
+ def __init__(self, *args, **kwargs):
+ super(RstReader, self).__init__(*args, **kwargs)
+
def _parse_metadata(self, document):
"""Return the dict containing document metadata"""
output = {}
@@ -105,20 +125,26 @@ class RstReader(Reader):
output[name] = self.process_metadata(name, value)
return output
- def _get_publisher(self, filename):
- extra_params = {'initial_header_level': '2'}
+ def _get_publisher(self, source_path):
+ extra_params = {'initial_header_level': '2',
+ 'syntax_highlight': 'short',
+ 'input_encoding': 'utf-8'}
+ user_params = self.settings.get('DOCUTILS_SETTINGS')
+ if user_params:
+ extra_params.update(user_params)
+
pub = docutils.core.Publisher(
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
pub.writer.translator_class = PelicanHTMLTranslator
pub.process_programmatic_settings(None, extra_params, None)
- pub.set_source(source_path=filename)
+ pub.set_source(source_path=source_path)
pub.publish()
return pub
- def read(self, filename):
+ def read(self, source_path):
"""Parses restructured text"""
- pub = self._get_publisher(filename)
+ pub = self._get_publisher(source_path)
parts = pub.writer.parts
content = parts.get('body')
@@ -130,48 +156,142 @@ class RstReader(Reader):
class MarkdownReader(Reader):
enabled = bool(Markdown)
- file_extensions = ['md', 'markdown', 'mkd']
- extensions = ['codehilite', 'extra']
+ file_extensions = ['md', 'markdown', 'mkd', 'mdown']
+
+ def __init__(self, *args, **kwargs):
+ super(MarkdownReader, self).__init__(*args, **kwargs)
+ self.extensions = self.settings['MD_EXTENSIONS']
+ self.extensions.append('meta')
+ self._md = Markdown(extensions=self.extensions)
def _parse_metadata(self, meta):
"""Return the dict containing document metadata"""
- md = Markdown(extensions=set(self.extensions + ['meta']))
output = {}
for name, value in meta.items():
name = name.lower()
if name == "summary":
- summary_values = "\n".join(str(item) for item in value)
- summary = md.convert(summary_values)
+ summary_values = "\n".join(value)
+ # reset the markdown instance to clear any state
+ self._md.reset()
+ summary = self._md.convert(summary_values)
output[name] = self.process_metadata(name, summary)
else:
output[name] = self.process_metadata(name, value[0])
return output
- def read(self, filename):
+ def read(self, source_path):
"""Parse content and metadata of markdown files"""
- text = pelican_open(filename)
- md = Markdown(extensions=set(self.extensions + ['meta']))
- content = md.convert(text)
- metadata = self._parse_metadata(md.Meta)
+ with pelican_open(source_path) as text:
+ content = self._md.convert(text)
+
+ metadata = self._parse_metadata(self._md.Meta)
return content, metadata
-class HtmlReader(Reader):
- file_extensions = ['html', 'htm']
- _re = re.compile('\<\!\-\-\#\s?[A-z0-9_-]*\s?\:s?[A-z0-9\s_-]*\s?\-\-\>')
+class HTMLReader(Reader):
+ """Parses HTML files as input, looking for meta, title, and body tags"""
+ file_extensions = ['htm', 'html']
+ enabled = True
+
+ class _HTMLParser(HTMLParser):
+ def __init__(self, settings):
+ HTMLParser.__init__(self)
+ self.body = ''
+ self.metadata = {}
+ self.settings = settings
+
+ self._data_buffer = ''
+
+ self._in_top_level = True
+ self._in_head = False
+ self._in_title = False
+ self._in_body = False
+ self._in_tags = False
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'head' and self._in_top_level:
+ self._in_top_level = False
+ self._in_head = True
+ elif tag == 'title' and self._in_head:
+ self._in_title = True
+ self._data_buffer = ''
+ elif tag == 'body' and self._in_top_level:
+ self._in_top_level = False
+ self._in_body = True
+ self._data_buffer = ''
+ elif tag == 'meta' and self._in_head:
+ self._handle_meta_tag(attrs)
+
+ elif self._in_body:
+ self._data_buffer += self.build_tag(tag, attrs, False)
+
+ def handle_endtag(self, tag):
+ if tag == 'head':
+ if self._in_head:
+ self._in_head = False
+ self._in_top_level = True
+ elif tag == 'title':
+ self._in_title = False
+ self.metadata['title'] = self._data_buffer
+ elif tag == 'body':
+ self.body = self._data_buffer
+ self._in_body = False
+ self._in_top_level = True
+ elif self._in_body:
+ self._data_buffer += '{}>'.format(escape(tag))
+
+ def handle_startendtag(self, tag, attrs):
+ if tag == 'meta' and self._in_head:
+ self._handle_meta_tag(attrs)
+ if self._in_body:
+ self._data_buffer += self.build_tag(tag, attrs, True)
+
+ def handle_comment(self, data):
+ self._data_buffer += ''.format(data)
+
+ def handle_data(self, data):
+ self._data_buffer += data
+
+ def handle_entityref(self, data):
+ self._data_buffer += '&{};'.format(data)
+
+ def handle_charref(self, data):
+ self._data_buffer += '{};'.format(data)
+
+ def build_tag(self, tag, attrs, close_tag):
+ result = '<{}'.format(escape(tag))
+ for k, v in attrs:
+ result += ' ' + escape(k)
+ if v is not None:
+ result += '="{}"'.format(escape(v))
+ if close_tag:
+ return result + ' />'
+ return result + '>'
+
+ def _handle_meta_tag(self, attrs):
+ name = self._attr_value(attrs, 'name').lower()
+ contents = self._attr_value(attrs, 'contents', '')
+
+ if name == 'keywords':
+ name = 'tags'
+ self.metadata[name] = contents
+
+ @classmethod
+ def _attr_value(cls, attrs, name, default=None):
+ return next((x[1] for x in attrs if x[0] == name), default)
def read(self, filename):
- """Parse content and metadata of (x)HTML files"""
- with open(filename) as content:
- metadata = {'title': 'unnamed'}
- for i in self._re.findall(content):
- key = i.split(':')[0][5:].strip()
- value = i.split(':')[-1][:-3].strip()
- name = key.lower()
- metadata[name] = self.process_metadata(name, value)
+ """Parse content and metadata of HTML files"""
+ with pelican_open(filename) as content:
+ parser = self._HTMLParser(self.settings)
+ parser.feed(content)
+ parser.close()
- return content, metadata
+ metadata = {}
+ for k in parser.metadata:
+ metadata[k] = self.process_metadata(k, parser.metadata[k])
+ return parser.body, metadata
class AsciiDocReader(Reader):
@@ -179,14 +299,15 @@ class AsciiDocReader(Reader):
file_extensions = ['asc']
default_options = ["--no-header-footer", "-a newline=\\n"]
- def read(self, filename):
+ def read(self, source_path):
"""Parse content and metadata of asciidoc files"""
from cStringIO import StringIO
- text = StringIO(pelican_open(filename))
+ with pelican_open(source_path) as source:
+ text = StringIO(source)
content = StringIO()
ad = AsciiDocAPI()
- options = self.settings.get('ASCIIDOC_OPTIONS', [])
+ options = self.settings['ASCIIDOC_OPTIONS']
if isinstance(options, (str, unicode)):
options = [m.strip() for m in options.split(',')]
options = self.default_options + options
@@ -205,23 +326,26 @@ class AsciiDocReader(Reader):
return content, metadata
-_EXTENSIONS = {}
+EXTENSIONS = {}
-for cls in Reader.__subclasses__():
+for cls in [Reader] + Reader.__subclasses__():
for ext in cls.file_extensions:
- _EXTENSIONS[ext] = cls
+ EXTENSIONS[ext] = cls
-def read_file(filename, fmt=None, settings=None):
+def read_file(path, fmt=None, settings=None):
"""Return a reader object using the given format."""
- base, ext = os.path.splitext(os.path.basename(filename))
+ base, ext = os.path.splitext(os.path.basename(path))
if not fmt:
fmt = ext[1:]
- if fmt not in _EXTENSIONS:
- raise TypeError('Pelican does not know how to parse %s' % filename)
+ if fmt not in EXTENSIONS:
+ raise TypeError('Pelican does not know how to parse {}'.format(path))
- reader = _EXTENSIONS[fmt](settings)
+ if settings is None:
+ settings = {}
+
+ reader = EXTENSIONS[fmt](settings)
settings_key = '%s_EXTENSIONS' % fmt.upper()
if settings and settings_key in settings:
@@ -230,21 +354,53 @@ def read_file(filename, fmt=None, settings=None):
if not reader.enabled:
raise ValueError("Missing dependencies for %s" % fmt)
- content, metadata = reader.read(filename)
+ metadata = parse_path_metadata(
+ path=path, settings=settings, process=reader.process_metadata)
+ content, reader_metadata = reader.read(path)
+ metadata.update(reader_metadata)
# eventually filter the content with typogrify if asked so
- if settings and settings.get('TYPOGRIFY'):
+ if content and settings and settings['TYPOGRIFY']:
from typogrify.filters import typogrify
content = typogrify(content)
metadata['title'] = typogrify(metadata['title'])
- filename_metadata = settings and settings.get('FILENAME_METADATA')
- if filename_metadata:
- match = re.match(filename_metadata, base)
- if match:
- for k, v in match.groupdict().iteritems():
- if k not in metadata:
- k = k.lower() # metadata must be lowercase
- metadata[k] = reader.process_metadata(k, v)
-
return content, metadata
+
+def parse_path_metadata(path, settings=None, process=None):
+ """Extract a metadata dictionary from a file's path
+
+ >>> import pprint
+ >>> settings = {
+ ... 'FILENAME_METADATA': '(?P[^.]*).*',
+ ... 'PATH_METADATA':
+ ... '(?P[^/]*)/(?P\d{4}-\d{2}-\d{2})/.*',
+ ... }
+ >>> reader = Reader(settings=settings)
+ >>> metadata = parse_path_metadata(
+ ... path='my-cat/2013-01-01/my-slug.html',
+ ... settings=settings,
+ ... process=reader.process_metadata)
+ >>> pprint.pprint(metadata) # doctest: +ELLIPSIS
+ {'category': ,
+ 'date': datetime.datetime(2013, 1, 1, 0, 0),
+ 'slug': 'my-slug'}
+ """
+ metadata = {}
+ base, ext = os.path.splitext(os.path.basename(path))
+ if settings:
+ for key,data in [('FILENAME_METADATA', base),
+ ('PATH_METADATA', path),
+ ]:
+ regexp = settings.get(key)
+ if regexp:
+ match = re.match(regexp, data)
+ if match:
+ # .items() for py3k compat.
+ for k, v in match.groupdict().items():
+ if k not in metadata:
+ k = k.lower() # metadata must be lowercase
+ if process:
+ v = process(k, v)
+ metadata[k] = v
+ return metadata
diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py
index c677144d..fb4a6c93 100644
--- a/pelican/rstdirectives.py
+++ b/pelican/rstdirectives.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+
from docutils import nodes, utils
from docutils.parsers.rst import directives, roles, Directive
from pygments.formatters import HtmlFormatter
@@ -32,7 +34,7 @@ class Pygments(Directive):
# take an arbitrary option if more than one is given
formatter = self.options and VARIANTS[self.options.keys()[0]] \
or DEFAULT
- parsed = highlight(u'\n'.join(self.content), lexer, formatter)
+ parsed = highlight('\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')]
directives.register_directive('code-block', Pygments)
diff --git a/pelican/server.py b/pelican/server.py
new file mode 100644
index 00000000..fd99b209
--- /dev/null
+++ b/pelican/server.py
@@ -0,0 +1,29 @@
+from __future__ import print_function
+import sys
+try:
+ import SimpleHTTPServer as srvmod
+except ImportError:
+ import http.server as srvmod # NOQA
+
+try:
+ import SocketServer as socketserver
+except ImportError:
+ import socketserver # NOQA
+
+PORT = 8000
+
+Handler = srvmod.SimpleHTTPRequestHandler
+
+try:
+ httpd = socketserver.TCPServer(("", PORT), Handler)
+except OSError as e:
+ print("Could not listen on port", PORT)
+ sys.exit(getattr(e, 'exitcode', 1))
+
+
+print("serving at port", PORT)
+try:
+ httpd.serve_forever()
+except KeyboardInterrupt as e:
+ print("shutting down server")
+ httpd.socket.close()
\ No newline at end of file
diff --git a/pelican/settings.py b/pelican/settings.py
index 692fc983..34a2b42a 100644
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -1,11 +1,21 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
+import six
+
import copy
-import imp
import inspect
import os
import locale
import logging
-import re
+
+try:
+ # SourceFileLoader is the recommended way in 3.3+
+ from importlib.machinery import SourceFileLoader
+ load_source = lambda name, path: SourceFileLoader(name, path).load_module()
+except ImportError:
+ # but it does not exist in 3.2-, so fall back to imp
+ import imp
+ load_source = imp.load_source
from os.path import isabs
@@ -13,88 +23,102 @@ from os.path import isabs
logger = logging.getLogger(__name__)
-DEFAULT_THEME = os.sep.join([os.path.dirname(os.path.abspath(__file__)),
- "themes/notmyidea"])
-_DEFAULT_CONFIG = {'PATH': '.',
- 'ARTICLE_DIR': '',
- 'ARTICLE_EXCLUDES': ('pages',),
- 'PAGE_DIR': 'pages',
- 'PAGE_EXCLUDES': (),
- 'THEME': DEFAULT_THEME,
- 'OUTPUT_PATH': 'output/',
- 'MARKUP': ('rst', 'md'),
- 'STATIC_PATHS': ['images', ],
- 'THEME_STATIC_PATHS': ['static', ],
- 'FEED_ALL_ATOM': 'feeds/all.atom.xml',
- 'CATEGORY_FEED_ATOM': 'feeds/%s.atom.xml',
- 'TRANSLATION_FEED_ATOM': 'feeds/all-%s.atom.xml',
- 'FEED_MAX_ITEMS': '',
- 'SITEURL': '',
- 'SITENAME': 'A Pelican Blog',
- 'DISPLAY_PAGES_ON_MENU': True,
- 'PDF_GENERATOR': False,
- 'OUTPUT_SOURCES': False,
- 'OUTPUT_SOURCES_EXTENSION': '.text',
- 'USE_FOLDER_AS_CATEGORY': True,
- 'DEFAULT_CATEGORY': 'misc',
- 'WITH_FUTURE_DATES': True,
- 'CSS_FILE': 'main.css',
- 'NEWEST_FIRST_ARCHIVES': True,
- 'REVERSE_CATEGORY_ORDER': False,
- 'DELETE_OUTPUT_DIRECTORY': False,
- '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/{slug}.html',
- 'CATEGORY_SAVE_AS': 'category/{slug}.html',
- 'TAG_URL': 'tag/{slug}.html',
- 'TAG_SAVE_AS': 'tag/{slug}.html',
- 'AUTHOR_URL': u'author/{slug}.html',
- 'AUTHOR_SAVE_AS': u'author/{slug}.html',
- 'RELATIVE_URLS': True,
- 'DEFAULT_LANG': 'en',
- 'TAG_CLOUD_STEPS': 4,
- 'TAG_CLOUD_MAX_ITEMS': 100,
- 'DIRECT_TEMPLATES': ('index', 'tags', 'categories', 'archives'),
- 'EXTRA_TEMPLATES_PATHS': [],
- 'PAGINATED_DIRECT_TEMPLATES': ('index', ),
- 'PELICAN_CLASS': 'pelican.Pelican',
- 'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
- 'DATE_FORMATS': {},
- 'JINJA_EXTENSIONS': [],
- 'LOCALE': '', # default to user locale
- 'DEFAULT_PAGINATION': False,
- 'DEFAULT_ORPHANS': 0,
- 'DEFAULT_METADATA': (),
- 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2}).*',
- 'FILES_TO_COPY': (),
- 'DEFAULT_STATUS': 'published',
- 'ARTICLE_PERMALINK_STRUCTURE': '',
- 'TYPOGRIFY': False,
- 'SUMMARY_MAX_LENGTH': 50,
- 'PLUGINS': [],
- 'TEMPLATE_PAGES': {}
- }
+DEFAULT_THEME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ 'themes', 'notmyidea')
+DEFAULT_CONFIG = {
+ 'PATH': os.curdir,
+ 'ARTICLE_DIR': '',
+ 'ARTICLE_EXCLUDES': ('pages',),
+ 'PAGE_DIR': 'pages',
+ 'PAGE_EXCLUDES': (),
+ 'THEME': DEFAULT_THEME,
+ 'OUTPUT_PATH': 'output',
+ 'MARKUP': ('rst', 'md'),
+ 'STATIC_PATHS': ['images', ],
+ 'THEME_STATIC_PATHS': ['static', ],
+ 'FEED_ALL_ATOM': os.path.join('feeds', 'all.atom.xml'),
+ 'CATEGORY_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'),
+ 'TRANSLATION_FEED_ATOM': os.path.join('feeds', 'all-%s.atom.xml'),
+ 'FEED_MAX_ITEMS': '',
+ 'SITEURL': '',
+ 'SITENAME': 'A Pelican Blog',
+ 'DISPLAY_PAGES_ON_MENU': True,
+ 'DISPLAY_CATEGORIES_ON_MENU': True,
+ 'PDF_GENERATOR': False,
+ 'OUTPUT_SOURCES': False,
+ 'OUTPUT_SOURCES_EXTENSION': '.text',
+ 'USE_FOLDER_AS_CATEGORY': True,
+ 'DEFAULT_CATEGORY': 'misc',
+ 'WITH_FUTURE_DATES': True,
+ 'CSS_FILE': 'main.css',
+ 'NEWEST_FIRST_ARCHIVES': True,
+ 'REVERSE_CATEGORY_ORDER': False,
+ 'DELETE_OUTPUT_DIRECTORY': False,
+ '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': os.path.join('pages', '{slug}.html'),
+ 'PAGE_LANG_URL': 'pages/{slug}-{lang}.html',
+ 'PAGE_LANG_SAVE_AS': os.path.join('pages', '{slug}-{lang}.html'),
+ 'STATIC_URL': '{path}',
+ 'STATIC_SAVE_AS': '{path}',
+ 'PDF_STYLE_PATH': '',
+ 'PDF_STYLE': 'twelvepoint',
+ 'CATEGORY_URL': 'category/{slug}.html',
+ 'CATEGORY_SAVE_AS': os.path.join('category', '{slug}.html'),
+ 'TAG_URL': 'tag/{slug}.html',
+ 'TAG_SAVE_AS': os.path.join('tag', '{slug}.html'),
+ 'AUTHOR_URL': 'author/{slug}.html',
+ 'AUTHOR_SAVE_AS': os.path.join('author', '{slug}.html'),
+ 'YEAR_ARCHIVE_SAVE_AS': False,
+ 'MONTH_ARCHIVE_SAVE_AS': False,
+ 'DAY_ARCHIVE_SAVE_AS': False,
+ 'RELATIVE_URLS': False,
+ 'DEFAULT_LANG': 'en',
+ 'TAG_CLOUD_STEPS': 4,
+ 'TAG_CLOUD_MAX_ITEMS': 100,
+ 'DIRECT_TEMPLATES': ('index', 'tags', 'categories', 'archives'),
+ 'EXTRA_TEMPLATES_PATHS': [],
+ 'PAGINATED_DIRECT_TEMPLATES': ('index', ),
+ 'PELICAN_CLASS': 'pelican.Pelican',
+ 'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
+ 'DATE_FORMATS': {},
+ 'ASCIIDOC_OPTIONS': [],
+ 'MD_EXTENSIONS': ['codehilite(css_class=highlight)', 'extra'],
+ 'JINJA_EXTENSIONS': [],
+ 'JINJA_FILTERS': {},
+ 'LOCALE': [], # defaults to user locale
+ 'DEFAULT_PAGINATION': False,
+ 'DEFAULT_ORPHANS': 0,
+ 'DEFAULT_METADATA': (),
+ 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2}).*',
+ 'PATH_METADATA': '',
+ 'FILES_TO_COPY': (),
+ 'DEFAULT_STATUS': 'published',
+ 'ARTICLE_PERMALINK_STRUCTURE': '',
+ 'TYPOGRIFY': False,
+ 'SUMMARY_MAX_LENGTH': 50,
+ 'PLUGIN_PATH': '',
+ 'PLUGINS': [],
+ 'TEMPLATE_PAGES': {},
+ 'IGNORE_FILES': ['.#*'],
+ }
-
-def read_settings(filename=None, override=None):
- if filename:
- local_settings = get_settings_from_file(filename)
+def read_settings(path=None, override=None):
+ if path:
+ local_settings = get_settings_from_file(path)
# Make the paths relative to the settings file
- for p in ['PATH', 'OUTPUT_PATH', 'THEME']:
+ for p in ['PATH', 'OUTPUT_PATH', 'THEME', 'PLUGIN_PATH']:
if p in local_settings and local_settings[p] is not None \
and not isabs(local_settings[p]):
absp = os.path.abspath(os.path.normpath(os.path.join(
- os.path.dirname(filename), local_settings[p])))
- if p != 'THEME' or os.path.exists(p):
+ os.path.dirname(path), local_settings[p])))
+ if p not in ('THEME', 'PLUGIN_PATH') or os.path.exists(absp):
local_settings[p] = absp
else:
- local_settings = copy.deepcopy(_DEFAULT_CONFIG)
+ local_settings = copy.deepcopy(DEFAULT_CONFIG)
if override:
local_settings.update(override)
@@ -102,10 +126,8 @@ def read_settings(filename=None, override=None):
return configure_settings(local_settings)
-def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
- """
- Load settings from a module, returning a dict.
- """
+def get_settings_from_module(module=None, default_settings=DEFAULT_CONFIG):
+ """Loads settings from a module, returns a dictionary."""
context = copy.deepcopy(default_settings)
if module is not None:
@@ -114,61 +136,81 @@ def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
return context
-def get_settings_from_file(filename, default_settings=_DEFAULT_CONFIG):
- """
- Load settings from a file path, returning a dict.
+def get_settings_from_file(path, default_settings=DEFAULT_CONFIG):
+ """Loads settings from a file path, returning a dict."""
- """
-
- name = os.path.basename(filename).rpartition(".")[0]
- module = imp.load_source(name, filename)
+ name, ext = os.path.splitext(os.path.basename(path))
+ module = load_source(name, path)
return get_settings_from_module(module, default_settings=default_settings)
def configure_settings(settings):
- """
- Provide optimizations, error checking, and warnings for loaded settings
+ """Provide optimizations, error checking and warnings for the given
+ settings.
+
"""
if not 'PATH' in settings or not os.path.isdir(settings['PATH']):
raise Exception('You need to specify a path containing the content'
' (see pelican --help for more information)')
- # find the theme in pelican.theme if the given one does not exists
+ # lookup the theme in "pelican/themes" if the given one doesn't exist
if not os.path.isdir(settings['THEME']):
- theme_path = os.sep.join([os.path.dirname(
- os.path.abspath(__file__)), "themes/%s" % settings['THEME']])
+ theme_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'themes',
+ settings['THEME'])
if os.path.exists(theme_path):
settings['THEME'] = theme_path
else:
- raise Exception("Impossible to find the theme %s"
+ raise Exception("Could not find the theme %s"
% settings['THEME'])
- # if locales is not a list, make it one
- locales = settings['LOCALE']
+ # standardize strings to lowercase strings
+ for key in [
+ 'DEFAULT_LANG',
+ ]:
+ if key in settings:
+ settings[key] = settings[key].lower()
- if isinstance(locales, basestring):
- locales = [locales]
+ # standardize strings to lists
+ for key in [
+ 'LOCALE',
+ ]:
+ if key in settings and isinstance(settings[key], six.string_types):
+ settings[key] = [settings[key]]
+
+ # check settings that must be a particular type
+ for key, types in [
+ ('OUTPUT_SOURCES_EXTENSION', six.string_types),
+ ('FILENAME_METADATA', six.string_types),
+ ]:
+ if key in settings and not isinstance(settings[key], types):
+ value = settings.pop(key)
+ logger.warn(
+ 'Detected misconfigured {} ({}), '
+ 'falling back to the default ({})'.format(
+ key, value, DEFAULT_CONFIG[key]))
# try to set the different locales, fallback on the default.
- if not locales:
- locales = _DEFAULT_CONFIG['LOCALE']
+ locales = settings.get('LOCALE', DEFAULT_CONFIG['LOCALE'])
for locale_ in locales:
try:
- locale.setlocale(locale.LC_ALL, locale_)
+ locale.setlocale(locale.LC_ALL, str(locale_))
break # break if it is successful
except locale.Error:
pass
else:
- logger.warn("LOCALE option doesn't contain a correct value")
+ logger.warning("LOCALE option doesn't contain a correct value")
if ('SITEURL' in settings):
# If SITEURL has a trailing slash, remove it and provide a warning
siteurl = settings['SITEURL']
if (siteurl.endswith('/')):
settings['SITEURL'] = siteurl[:-1]
- logger.warn("Removed extraneous trailing slash from SITEURL.")
- # If SITEURL is defined but FEED_DOMAIN isn't, set FEED_DOMAIN = SITEURL
+ logger.warning("Removed extraneous trailing slash from SITEURL.")
+ # If SITEURL is defined but FEED_DOMAIN isn't,
+ # set FEED_DOMAIN to SITEURL
if not 'FEED_DOMAIN' in settings:
settings['FEED_DOMAIN'] = settings['SITEURL']
@@ -181,37 +223,45 @@ def configure_settings(settings):
]
if any(settings.get(k) for k in feed_keys):
- if not settings.get('FEED_DOMAIN'):
- logger.warn("Since feed URLs should always be absolute, you should specify "
- "FEED_DOMAIN in your settings. (e.g., 'FEED_DOMAIN = "
- "http://www.example.com')")
-
if not settings.get('SITEURL'):
- logger.warn("Feeds generated without SITEURL set properly may not be valid")
+ logger.warning('Feeds generated without SITEURL set properly may not'
+ ' be valid')
if not 'TIMEZONE' in settings:
- logger.warn("No timezone information specified in the settings. Assuming"
- " your timezone is UTC for feed generation. Check "
- "http://docs.notmyidea.org/alexis/pelican/settings.html#timezone "
- "for more information")
+ logger.warning(
+ 'No timezone information specified in the settings. Assuming'
+ ' your timezone is UTC for feed generation. Check '
+ 'http://docs.getpelican.com/en/latest/settings.html#timezone '
+ 'for more information')
- if 'LESS_GENERATOR' in settings:
- logger.warn("The LESS_GENERATOR setting has been removed in favor "
- "of the Webassets plugin")
+ # Save people from accidentally setting a string rather than a list
+ path_keys = (
+ 'ARTICLE_EXCLUDES',
+ 'DEFAULT_METADATA',
+ 'DIRECT_TEMPLATES',
+ 'EXTRA_TEMPLATES_PATHS',
+ 'FILES_TO_COPY',
+ 'IGNORE_FILES',
+ 'JINJA_EXTENSIONS',
+ 'MARKUP',
+ 'PAGINATED_DIRECT_TEMPLATES',
+ 'PLUGINS',
+ 'STATIC_PATHS',
+ 'THEME_STATIC_PATHS',)
+ for PATH_KEY in filter(lambda k: k in settings, path_keys):
+ if isinstance(settings[PATH_KEY], six.string_types):
+ logger.warning("Detected misconfiguration with %s setting (must "
+ "be a list), falling back to the default"
+ % PATH_KEY)
+ settings[PATH_KEY] = DEFAULT_CONFIG[PATH_KEY]
- if 'OUTPUT_SOURCES_EXTENSION' in settings:
- if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], str):
- settings['OUTPUT_SOURCES_EXTENSION'] = _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION']
- logger.warn("Detected misconfiguration with OUTPUT_SOURCES_EXTENSION."
- " falling back to the default extension " +
- _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION'])
-
- filename_metadata = settings.get('FILENAME_METADATA')
- if filename_metadata and not isinstance(filename_metadata, basestring):
- logger.error("Detected misconfiguration with FILENAME_METADATA"
- " setting (must be string or compiled pattern), falling"
- "back to the default")
- settings['FILENAME_METADATA'] = \
- _DEFAULT_CONFIG['FILENAME_METADATA']
+ for old,new,doc in [
+ ('LESS_GENERATOR', 'the Webassets plugin', None),
+ ]:
+ if old in settings:
+ message = 'The {} setting has been removed in favor of {}'
+ if doc:
+ message += ', see {} for details'
+ logger.warning(message)
return settings
diff --git a/pelican/signals.py b/pelican/signals.py
index d592599a..92bc6249 100644
--- a/pelican/signals.py
+++ b/pelican/signals.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, print_function
from blinker import signal
initialized = signal('pelican_initialized')
@@ -10,4 +12,5 @@ article_generator_finalized = signal('article_generate_finalized')
get_generators = signal('get_generators')
pages_generate_context = signal('pages_generate_context')
pages_generator_init = signal('pages_generator_init')
+pages_generator_finalized = signal('pages_generator_finalized')
content_object_init = signal('content_object_init')
diff --git a/tests/TestPages/bad_page.rst b/pelican/tests/TestPages/bad_page.rst
similarity index 100%
rename from tests/TestPages/bad_page.rst
rename to pelican/tests/TestPages/bad_page.rst
diff --git a/tests/TestPages/hidden_page.rst b/pelican/tests/TestPages/hidden_page.rst
similarity index 100%
rename from tests/TestPages/hidden_page.rst
rename to pelican/tests/TestPages/hidden_page.rst
diff --git a/tests/TestPages/hidden_page_markdown.md b/pelican/tests/TestPages/hidden_page_markdown.md
similarity index 100%
rename from tests/TestPages/hidden_page_markdown.md
rename to pelican/tests/TestPages/hidden_page_markdown.md
diff --git a/tests/TestPages/hidden_page_with_template.rst b/pelican/tests/TestPages/hidden_page_with_template.rst
similarity index 100%
rename from tests/TestPages/hidden_page_with_template.rst
rename to pelican/tests/TestPages/hidden_page_with_template.rst
diff --git a/tests/TestPages/page.rst b/pelican/tests/TestPages/page.rst
similarity index 100%
rename from tests/TestPages/page.rst
rename to pelican/tests/TestPages/page.rst
diff --git a/tests/TestPages/page_markdown.md b/pelican/tests/TestPages/page_markdown.md
similarity index 100%
rename from tests/TestPages/page_markdown.md
rename to pelican/tests/TestPages/page_markdown.md
diff --git a/tests/TestPages/page_with_template.rst b/pelican/tests/TestPages/page_with_template.rst
similarity index 100%
rename from tests/TestPages/page_with_template.rst
rename to pelican/tests/TestPages/page_with_template.rst
diff --git a/pelican/tests/__init__.py b/pelican/tests/__init__.py
new file mode 100644
index 00000000..32353ea2
--- /dev/null
+++ b/pelican/tests/__init__.py
@@ -0,0 +1,2 @@
+import logging
+logging.getLogger().addHandler(logging.NullHandler())
diff --git a/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst b/pelican/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst
similarity index 100%
rename from tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst
rename to pelican/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst
diff --git a/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md b/pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md
similarity index 100%
rename from tests/content/2012-11-30_md_w_filename_meta#foo-bar.md
rename to pelican/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md
diff --git a/tests/content/TestCategory/article_with_category.rst b/pelican/tests/content/TestCategory/article_with_category.rst
similarity index 100%
rename from tests/content/TestCategory/article_with_category.rst
rename to pelican/tests/content/TestCategory/article_with_category.rst
diff --git a/tests/content/TestCategory/article_without_category.rst b/pelican/tests/content/TestCategory/article_without_category.rst
similarity index 100%
rename from tests/content/TestCategory/article_without_category.rst
rename to pelican/tests/content/TestCategory/article_without_category.rst
diff --git a/tests/content/article.rst b/pelican/tests/content/article.rst
similarity index 100%
rename from tests/content/article.rst
rename to pelican/tests/content/article.rst
diff --git a/tests/content/article_with_asc_extension.asc b/pelican/tests/content/article_with_asc_extension.asc
similarity index 100%
rename from tests/content/article_with_asc_extension.asc
rename to pelican/tests/content/article_with_asc_extension.asc
diff --git a/tests/content/article_with_asc_options.asc b/pelican/tests/content/article_with_asc_options.asc
similarity index 100%
rename from tests/content/article_with_asc_options.asc
rename to pelican/tests/content/article_with_asc_options.asc
diff --git a/pelican/tests/content/article_with_comments.html b/pelican/tests/content/article_with_comments.html
new file mode 100644
index 00000000..289e4a66
--- /dev/null
+++ b/pelican/tests/content/article_with_comments.html
@@ -0,0 +1,8 @@
+
+
+
+
+ Body content
+
+
+
diff --git a/pelican/tests/content/article_with_keywords.html b/pelican/tests/content/article_with_keywords.html
new file mode 100644
index 00000000..c869f514
--- /dev/null
+++ b/pelican/tests/content/article_with_keywords.html
@@ -0,0 +1,6 @@
+
+
+ This is a super article !
+
+
+
diff --git a/pelican/tests/content/article_with_markdown_and_footnote.md b/pelican/tests/content/article_with_markdown_and_footnote.md
new file mode 100644
index 00000000..dc257d62
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_footnote.md
@@ -0,0 +1,8 @@
+Title: Article with markdown containing footnotes
+Date: 2012-10-31
+Summary: Summary with **inline** markup *should* be supported.
+
+This is some content[^1] with some footnotes[^footnote]
+
+[^1]: Numbered footnote
+[^footnote]: Named footnote
\ No newline at end of file
diff --git a/pelican/tests/content/article_with_markdown_and_nonascii_summary.md b/pelican/tests/content/article_with_markdown_and_nonascii_summary.md
new file mode 100644
index 00000000..e26cc009
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_and_nonascii_summary.md
@@ -0,0 +1,18 @@
+Title: マックOS X 10.8でパイソンとVirtualenvをインストールと設定
+Slug: python-virtualenv-on-mac-osx-mountain-lion-10.8
+Date: 2012-12-20
+Tags: パイソン, マック
+Category: 指導書
+Summary: パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。
+
+Writing unicode is certainly fun.
+
+パイソンとVirtualenvをまっくでインストールする方法について明確に説明します。
+
+And let's mix languages.
+
+первый пост
+
+Now another.
+
+İlk yazı çok özel değil.
diff --git a/tests/content/article_with_markdown_and_summary_metadata_multi.md b/pelican/tests/content/article_with_markdown_and_summary_metadata_multi.md
similarity index 100%
rename from tests/content/article_with_markdown_and_summary_metadata_multi.md
rename to pelican/tests/content/article_with_markdown_and_summary_metadata_multi.md
diff --git a/tests/content/article_with_markdown_and_summary_metadata_single.md b/pelican/tests/content/article_with_markdown_and_summary_metadata_single.md
similarity index 100%
rename from tests/content/article_with_markdown_and_summary_metadata_single.md
rename to pelican/tests/content/article_with_markdown_and_summary_metadata_single.md
diff --git a/pelican/tests/content/article_with_markdown_extension.markdown b/pelican/tests/content/article_with_markdown_extension.markdown
new file mode 100644
index 00000000..94e92871
--- /dev/null
+++ b/pelican/tests/content/article_with_markdown_extension.markdown
@@ -0,0 +1,10 @@
+title: Test markdown File
+category: test
+
+Test Markdown File Header
+=========================
+
+Used for pelican test
+---------------------
+
+This is another markdown test file. Uses the markdown extension.
diff --git a/tests/content/article_with_markdown_markup_extensions.md b/pelican/tests/content/article_with_markdown_markup_extensions.md
similarity index 100%
rename from tests/content/article_with_markdown_markup_extensions.md
rename to pelican/tests/content/article_with_markdown_markup_extensions.md
diff --git a/tests/content/article_with_md_extension.md b/pelican/tests/content/article_with_md_extension.md
similarity index 57%
rename from tests/content/article_with_md_extension.md
rename to pelican/tests/content/article_with_md_extension.md
index 11aa22a2..1f111796 100644
--- a/tests/content/article_with_md_extension.md
+++ b/pelican/tests/content/article_with_md_extension.md
@@ -1,5 +1,8 @@
-title: Test md File
-category: test
+Title: Test md File
+Category: test
+Tags: foo, bar, foobar
+Date: 2010-12-02 10:14
+Summary: I have a lot to test
Test Markdown File Header
=========================
diff --git a/pelican/tests/content/article_with_mdown_extension.mdown b/pelican/tests/content/article_with_mdown_extension.mdown
new file mode 100644
index 00000000..bdaf74cd
--- /dev/null
+++ b/pelican/tests/content/article_with_mdown_extension.mdown
@@ -0,0 +1,10 @@
+title: Test mdown File
+category: test
+
+Test Markdown File Header
+=========================
+
+Used for pelican test
+---------------------
+
+This is another markdown test file. Uses the mdown extension.
diff --git a/pelican/tests/content/article_with_metadata.html b/pelican/tests/content/article_with_metadata.html
new file mode 100644
index 00000000..b108ac8a
--- /dev/null
+++ b/pelican/tests/content/article_with_metadata.html
@@ -0,0 +1,15 @@
+
+
+ This is a super article !
+
+
+
+
+
+
+
+
+ Multi-line metadata should be supported
+ as well as inline markup.
+
+
diff --git a/tests/content/article_with_metadata.rst b/pelican/tests/content/article_with_metadata.rst
similarity index 100%
rename from tests/content/article_with_metadata.rst
rename to pelican/tests/content/article_with_metadata.rst
diff --git a/tests/content/article_with_mkd_extension.mkd b/pelican/tests/content/article_with_mkd_extension.mkd
similarity index 100%
rename from tests/content/article_with_mkd_extension.mkd
rename to pelican/tests/content/article_with_mkd_extension.mkd
diff --git a/pelican/tests/content/article_with_null_attributes.html b/pelican/tests/content/article_with_null_attributes.html
new file mode 100644
index 00000000..68da704c
--- /dev/null
+++ b/pelican/tests/content/article_with_null_attributes.html
@@ -0,0 +1,8 @@
+
+
+
+
+ Ensure that empty attributes are copied properly.
+
+
+
diff --git a/tests/content/article_with_template.rst b/pelican/tests/content/article_with_template.rst
similarity index 100%
rename from tests/content/article_with_template.rst
rename to pelican/tests/content/article_with_template.rst
diff --git a/pelican/tests/content/article_with_uppercase_metadata.html b/pelican/tests/content/article_with_uppercase_metadata.html
new file mode 100644
index 00000000..4fe5a9ee
--- /dev/null
+++ b/pelican/tests/content/article_with_uppercase_metadata.html
@@ -0,0 +1,6 @@
+
+
+ This is a super article !
+
+
+
diff --git a/tests/content/article_with_uppercase_metadata.rst b/pelican/tests/content/article_with_uppercase_metadata.rst
similarity index 100%
rename from tests/content/article_with_uppercase_metadata.rst
rename to pelican/tests/content/article_with_uppercase_metadata.rst
diff --git a/tests/content/article_without_category.rst b/pelican/tests/content/article_without_category.rst
similarity index 100%
rename from tests/content/article_without_category.rst
rename to pelican/tests/content/article_without_category.rst
diff --git a/pelican/tests/content/wordpress_content_decoded b/pelican/tests/content/wordpress_content_decoded
new file mode 100644
index 00000000..6e91338c
--- /dev/null
+++ b/pelican/tests/content/wordpress_content_decoded
@@ -0,0 +1,48 @@
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
diff --git a/pelican/tests/content/wordpress_content_encoded b/pelican/tests/content/wordpress_content_encoded
new file mode 100644
index 00000000..da35de3b
--- /dev/null
+++ b/pelican/tests/content/wordpress_content_encoded
@@ -0,0 +1,55 @@
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
diff --git a/tests/content/wordpressexport.xml b/pelican/tests/content/wordpressexport.xml
similarity index 94%
rename from tests/content/wordpressexport.xml
rename to pelican/tests/content/wordpressexport.xml
index 0d68f180..56d9a458 100644
--- a/tests/content/wordpressexport.xml
+++ b/pelican/tests/content/wordpressexport.xml
@@ -628,5 +628,59 @@ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]>
+
+ Code in List
+ http://thisisa.test/?p=175
+ Thu, 01 Jan 1970 00:00:00 +0000
+ bob
+ http://thisisa.test/?p=175
+
+
+
List Item One!
+
List Item Two!
+
This is a code sample
+
+
+ a = [1, 2, 3]
+ b = [4, 5, 6]
+ for i in zip(a, b):
+ print i
+
+
+
List Item Four!
+
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]]>
+
+ 175
+ 2012-02-16 15:52:55
+ 0000-00-00 00:00:00
+ open
+ open
+ code-in-list-test
+ publish
+ 0
+ 0
+ post
+
+ 0
+
+
+ _edit_last
+
+
+
diff --git a/tests/default_conf.py b/pelican/tests/default_conf.py
similarity index 85%
rename from tests/default_conf.py
rename to pelican/tests/default_conf.py
index acb7d9da..bc3a7dff 100644
--- a/tests/default_conf.py
+++ b/pelican/tests/default_conf.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
-AUTHOR = u'Alexis Métaireau'
-SITENAME = u"Alexis' log"
+from __future__ import unicode_literals, print_function
+AUTHOR = 'Alexis Métaireau'
+SITENAME = "Alexis' log"
SITEURL = 'http://blog.notmyidea.org'
TIMEZONE = 'UTC'
@@ -18,7 +19,7 @@ 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"),
+ ('Tarek Ziadé', "http://ziade.org/blog"),
('Zubin Mithra', "http://zubin71.wordpress.com/"),)
SOCIAL = (('twitter', 'http://twitter.com/ametaireau'),
@@ -29,7 +30,7 @@ SOCIAL = (('twitter', 'http://twitter.com/ametaireau'),
DEFAULT_METADATA = (('yeah', 'it is'),)
# static paths will be copied under the same name
-STATIC_PATHS = ["pictures",]
+STATIC_PATHS = ["pictures", ]
# A list of files to copy from the source to the destination
FILES_TO_COPY = (('extra/robots.txt', 'robots.txt'),)
diff --git a/tests/output/basic/a-markdown-powered-article.html b/pelican/tests/output/basic/a-markdown-powered-article.html
similarity index 52%
rename from tests/output/basic/a-markdown-powered-article.html
rename to pelican/tests/output/basic/a-markdown-powered-article.html
index 80a12212..94b6e4ca 100644
--- a/tests/output/basic/a-markdown-powered-article.html
+++ b/pelican/tests/output/basic/a-markdown-powered-article.html
@@ -1,39 +1,33 @@
+
A markdown powered article
-
-
+
-
-
-
-
-
+
+
-
Comments !
- - -