diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index 9274ba2d..1ae0e9f6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ build dist tags .tox +.coverage +htmlcov +six-*.egg/ +*.orig diff --git a/.travis.yml b/.travis.yml index ea134da4..8b292101 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,20 @@ language: python python: - - "2.6" - "2.7" + - "3.2" +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq ruby-sass install: - - pip install nose unittest2 mock --use-mirrors + - pip install nose mock --use-mirrors + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors unittest2; else pip install --use-mirrors unittest2py3k; fi - pip install . --use-mirrors - pip install Markdown + - pip install webassets + - pip install cssmin script: nosetests -s tests notifications: irc: - channels: + channels: - "irc.freenode.org#pelican" on_success: change diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 373892cf..00000000 --- a/CHANGELOG +++ /dev/null @@ -1,108 +0,0 @@ -3.0 - XX/XX/XXXX - -* Refactored the way URL are handled. -* Improved the english documentation -* Fixed packaging using setuptools entrypoints -* Added typogrify support -* Added a way to disable feed generation -* Added support for DIRECT_TEMPLATES -* Allow multiple extensions for content files -* Added less support -* Improved the import script -* Fixed a bunch of bugs :-) -* Added functional tests -* Rsync support in the generated Makefile -* Improved feed support (easily pluggable with feedburner for instance) - -2.8 - -* dotclear importer -* Allow the usage of markdown extensions -* Themes are now easily extensible -* Don't output pagination information if there is only one page. -* Add a page per author, with all their articles -* Improved the test suite -* Made the themes more easy to extend -* Removed Skribit support -* Added a "pelican-quickstart" script -* Fixed timezone-related issues -* Add some scripts for windows support -* Date can be specified in seconds -* Never fail when generating posts (skip and continue) -* Allow the use of future dates -* Support having different timezones per languages. -* Enhanced the documentation - -2.7 - -* Uses logging rather than echoing to stdout -* Support custom jinja filters -* Compatibility with python 2.5 -* Add a theme manager -* Packaged for debian -* Add draft support - -2.6 - -* changes in the output directory structure -* makes templates easier to work with / create -* Add RSS support (was only atom previously) -* Add tag support for the feeds -* Enhance the documentation -* Add another theme (brownstone) -* Add translations -* Add a way to use "cleaner urls" with a rewrite url module (or equivalent) -* Add a tag cloud -* Add an autoreloading feature: the blog is automatically regenerated each time a modification is detected -* Translate the documentation in french -* import a blog from an rss feed -* Pagination support -* Add skribit support - -2.5 - -* import from wordpress -* add some new themes (martyalchin / wide-notmyidea) -* first bug report ! -* linkedin support -* added a FAQ -* google analytics support -* twitter support -* use relative urls not static ones - -2.4 - -* minor themes changes -* add disqus support (so we have comments) -* another code refactoring -* add config settings about pages -* blog entries can also be generated in pdf - -2.3 - -* markdown support - -2.2 - -* Prettify output -* Manages static pages as well - -2.1 - -* Put the notmyidea theme by default - -2.0 - -* Refactoring to be more extensible -* Change into the setting variables - -1.2 - -* Add a debug option -* Add feeds per category -* Use filsystem to get dates if no metadata provided -* Add pygment support - -1.1: - -* first working version diff --git a/MANIFEST.in b/MANIFEST.in index 13ea58a1..2f2ea824 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include *.rst global-include *.py recursive-include pelican *.html *.css *png *.in -include LICENSE +include LICENSE THANKS docs/changelog.rst +recursive-include tests * +recursive-exclude tests *.pyc diff --git a/README.rst b/README.rst index 5012bb9c..b2648bf1 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,14 @@ Pelican -####### +======= -.. image:: https://secure.travis-ci.org/ametaireau/pelican.png?branch=master +.. image:: https://secure.travis-ci.org/getpelican/pelican.png?branch=master + :target: http://travis-ci.org/#!/getpelican/pelican + :alt: Travis-ci: continuous integration status. -Pelican is a simple weblog generator, written in `Python `_. +Pelican is a static site generator, written in Python_. -* Write your weblog entries directly with your editor of choice (vim!) - in `reStructuredText `_ or `Markdown `_ +* Write your weblog entries directly with your editor of choice (vim!) + in reStructuredText_ or Markdown_ * Includes a simple CLI tool to (re)generate the weblog * Easy to interface with DVCSes and web hooks * Completely static output is easy to host anywhere @@ -20,40 +22,49 @@ Pelican currently supports: * 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 `_) +* 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) * Import from WordPress, Dotclear, or RSS feeds * Integration with external tools: Twitter, Google Analytics, etc. (optional) -Have a look at `the documentation `_ for -more information. +Have a look at the `Pelican documentation`_ for more information. Why the name "Pelican"? ------------------------- +----------------------- -Heh, you didn't notice? "Pelican" is an anagram for « Calepin » ;) +"Pelican" is an anagram for *calepin*, which means "notebook" in French. ;) Source code ----------- -You can access the source code via git at: https://github.com/ametaireau/pelican +You can access the source code at: https://github.com/getpelican/pelican -If you feel hackish, have a look at the explanation of `Pelican's internals -`_. +If you feel hackish, have a look at the explanation of `Pelican's internals`_. Feedback / Contact us --------------------- -If you want to see new features in Pelican, don't hesitate to offer suggestions, -clone the repository, etc. There are many ways to `contribute -`_. That's open source, dude! +If you want to see new features in Pelican, don't hesitate to offer +suggestions, clone the repository, etc. There are many ways to contribute_. +That's open source, dude! -Contact me at "alexis at notmyidea dot org" for any request/feedback! You can -also join the team at `#pelican on irc.freenode.org -`_ -(or if you don't have any IRC client, use `the webchat -`_) -for quick feedback. +Send a message to "authors at getpelican dot com" with any requests/feedback! You +can also join the team at `#pelican on Freenode`_ (or if you don't have an IRC +client handy, use the webchat_ for quick feedback. + +.. Links + +.. _Python: http://www.python.org/ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Markdown: http://daringfireball.net/projects/markdown/ +.. _Jinja2: http://jinja.pocoo.org/ +.. _`Pelican documentation`: http://docs.getpelican.com/latest/ +.. _`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 +.. _contribute: http://docs.getpelican.com/en/latest/contribute.html +.. _webassets: https://github.com/miracle2k/webassets diff --git a/THANKS b/THANKS index 2a20100e..eeaf2309 100644 --- a/THANKS +++ b/THANKS @@ -1,19 +1,122 @@ -Some people have helped to improve pelican by contributing features, reporting -bugs or giving ideas. Thanks to them ! +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 -- Dan Jacka -- solsTiCe on linuxfr for reporting bugs -- Guillaume B (Gui13) -- Ronny Pfannschmidt -- Jérome Renard -- Nicolas Martin -- David Kulak -- Arnaud Bos -- nblock (Florian) -- Bruno Bord -- Laureline Guérin -- Samuel Martin -- Marcus Fredriksson -- Günter Kolousek -- Simon Liedtke -- Manuel F. Viera +If you want to contibute, check the documentation section about how to do so + + +Aaron Kavlie +Abhishek L +Albrecht Mühlenschulte +Aldiantoro Nugroho +Alen Mujezinovic +Alessandro Martin +Alexander Artemenko +Alexandre RODIERE +Alexis Daboville +Alexis Métaireau +Allan Whatmough +Andrea Crotti +Andrew Laski +Arnaud BOS +asselinpaul +Borgar +Brandon W Maister +Brendan Wholihan +Brian C. Lane +Brian Hsu +Brian St. Pierre +Bruno Binet +BunnyMan +Chris Streeter +Christophe Chauvet +Clint Howarth +Dafydd Crosby +Dana Woodman +dave mankoff +David Beitey +David Marble +derdon +Dirkjan Ochtman +Dirk Makowski +draftcode +Edward Delaporte +epatters +Eric Case +Erik Hetzner +FELD Boris +Feth Arezki +Florian Jacob +Florian Preinstorfer +Freeculture +Guillaume +Guillaume B +Guillermo López +guillermooo +Ian Cordasco +Iuri de Silvio +James Rowe +jawher +Jerome +Jiachen Yang +Jochen Breuer +joe di castro +Jökull Sólberg Auðunsson +Joshua Adelman +Julian Berman +justinmayer +Justin Mayer +Kyle Fuller +Laureline Guerin +Leonard Huang +Marcel Hellkamp +Marco Milanesi +Marcus Fredriksson +Mario Rodas +Martin Brochhaus +Massimo Santini +Matt Bowcock +Matt Layman +Meir Kriheli +Michael Guntsche +Michael Reneer +Michael Yanovich +Mike Yumatov +Mikhail Korobov +m-r-r +mviera +Nico Di Rocco +Nicolas Duhamel +Nicolas Perriault +Nicolas Steinmetz +Pavel Puchkin +Perry Roper +Philippe Pepiot +Rachid Belaid +Ranjhith Kalisamy +Remi Rampin +Rémy HUBSCHER +renhbo +Roman Skvazh +Ronny Pfannschmidt +Rory McCann +saghul +sam +Samrat Man Singh +Simon Liedtke +Skami18 +solsTiCe d'Hiver +Stéphane Bunel +Stéphane Raimbault +Stuart Colville +Tarek Ziade +Thanos Lefteris +the Bunny Man +Tobias +Tomi Pieviläinen +Trae Blain +Tshepang Lekhonkhobe +Wladislaw Merezhko +Zoresvit diff --git a/TODO b/TODO deleted file mode 100644 index ca29204d..00000000 --- a/TODO +++ /dev/null @@ -1,9 +0,0 @@ -* Add a way to support pictures (see how sphinx makes that) -* Make the program support UTF8-encoded files as input (and later: any encoding?) -* Add status support (draft, published, hidden) -* Add a serve + automatic generation behaviour. -* Recompile only the changed files, not all. -* Add a way to make the coffee (or not) -* Add a sitemap generator. -* read templates from the templates folder per default -* add support of github via ghg import diff --git a/dev_requirements.txt b/dev_requirements.txt index ec3245d1..e0189d35 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,10 +1,9 @@ -Jinja2>=2.4 -Pygments -docutils -feedgenerator -unittest2 -pytz +# Tests mock + +# Optional Packages Markdown -blinker -BeautifulSoup +BeautifulSoup4 +lxml +typogrify +webassets diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..aa18ea9e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,161 @@ +Release history +############### + +3.2 (XXXX-XX-XX) +================ + +* Support for Python 3! + +3.1 (2012-12-04) +================ + +* Importer now stores slugs within files by default. This can be disabled with + the ``--disable-slugs`` option. +* Improve handling of links to intra-site resources +* Ensure WordPress import adds paragraphs for all types of line endings + in post content +* Decode HTML entities within WordPress post titles on import +* Improve appearance of LinkedIn icon in default theme +* Add GitHub and Google+ social icons support in default theme +* Optimize social icons +* Add ``FEED_ALL_ATOM`` and ``FEED_ALL_RSS`` to generate feeds containing all posts regardless of their language +* Split ``TRANSLATION_FEED`` into ``TRANSLATION_FEED_ATOM`` and ``TRANSLATION_FEED_RSS`` +* Different feeds can now be enabled/disabled individually +* Allow for blank author: if ``AUTHOR`` setting is not set, author won't + default to ``${USER}`` anymore, and a post won't contain any author + information if the post author is empty +* Move LESS and Webassets support from Pelican core to plugin +* The ``DEFAULT_DATE`` setting now defaults to ``None``, which means that + articles won't be generated unless date metadata is specified +* Add ``FILENAME_METADATA`` setting to support metadata extraction from filename +* Add ``gzip_cache`` plugin to compress common text files into a ``.gz`` + file within the same directory as the original file, preventing the server + (e.g. Nginx) from having to compress files during an HTTP call +* Add support for AsciiDoc-formatted content +* Add ``USE_FOLDER_AS_CATEGORY`` setting so that feature can be toggled on/off +* Support arbitrary Jinja template files +* Restore basic functional tests +* New signals: ``generator_init``, ``get_generators``, and + ``article_generate_preread`` + +3.0 (2012-08-08) +================ + +* Refactored the way URLs are handled +* Improved the English documentation +* Fixed packaging using ``setuptools`` entrypoints +* Added ``typogrify`` support +* Added a way to disable feed generation +* Added support for ``DIRECT_TEMPLATES`` +* Allow multiple extensions for content files +* Added LESS support +* Improved the import script +* Added functional tests +* Rsync support in the generated Makefile +* Improved feed support (easily pluggable with Feedburner for instance) +* Added support for ``abbr`` in reST +* Fixed a bunch of bugs :-) + +2.8 (2012-02-28) +================== + +* Dotclear importer +* Allow the usage of Markdown extensions +* Themes are now easily extensible +* Don't output pagination information if there is only one page +* Add a page per author, with all their articles +* Improved the test suite +* Made the themes easier to extend +* Removed Skribit support +* Added a ``pelican-quickstart`` script +* Fixed timezone-related issues +* Added some scripts for Windows support +* Date can be specified in seconds +* Never fail when generating posts (skip and continue) +* Allow the use of future dates +* Support having different timezones per language +* Enhanced the documentation + +2.7 (2011-06-11) +================== + +* Use ``logging`` rather than echoing to stdout +* Support custom Jinja filters +* Compatibility with Python 2.5 +* Added a theme manager +* Packaged for Debian +* Added draft support + +2.6 (2011-03-08) +================== + +* Changes in the output directory structure +* Makes templates easier to work with / create +* Added RSS support (was Atom-only) +* Added tag support for the feeds +* Enhance the documentation +* Added another theme (brownstone) +* Added translations +* Added a way to use cleaner URLs with a rewrite url module (or equivalent) +* Added a tag cloud +* Added an autoreloading feature: the blog is automatically regenerated each time a modification is detected +* Translate the documentation into French +* Import a blog from an RSS feed +* Pagination support +* Added Skribit support + +2.5 (2010-11-20) +================== + +* Import from Wordpress +* Added some new themes (martyalchin / wide-notmyidea) +* First bug report! +* Linkedin support +* Added a FAQ +* Google Analytics support +* Twitter support +* Use relative URLs, not static ones + +2.4 (2010-11-06) +================ + +* Minor themes changes +* Add Disqus support (so we have comments) +* Another code refactoring +* Added config settings about pages +* Blog entries can also be generated in PDF + +2.3 (2010-10-31) +================ + +* Markdown support + +2.2 (2010-10-30) +================ + +* Prettify output +* Manages static pages as well + +2.1 (2010-10-30) +================ + +* Make notmyidea the default theme + +2.0 (2010-10-30) +================ + +* Refactoring to be more extensible +* Change into the setting variables + +1.2 (2010-09-28) +================ + +* Added a debug option +* Added per-category feeds +* Use filesystem to get dates if no metadata is provided +* Add Pygments support + +1.1 (2010-08-19) +================ + +* First working version diff --git a/docs/conf.py b/docs/conf.py index ac2d67ee..6c8ececa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import sys, os sys.path.append(os.path.abspath('..')) @@ -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__ @@ -24,7 +25,7 @@ html_theme = 'pelican' html_theme_options = { 'nosidebar': True, 'index_logo': 'pelican.png', - 'github_fork': 'ametaireau/pelican', + 'github_fork': 'getpelican/pelican', } html_static_path = ['_static'] @@ -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 fcf8d5c0..3ab86263 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -3,14 +3,17 @@ 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). -Don't hesitate to fork and make a pull request on GitHub. +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. Setting up the development environment ====================================== You're free to set up your development environment any way you like. Here is a -way using virtualenv and virtualenvwrapper. If you don't have them, you can -install these packages via:: +way using the `virtualenv `_ and `virtualenvwrapper +`_ tools. If you don't +have them, you can install these both of these packages via:: $ pip install virtualenvwrapper @@ -20,30 +23,103 @@ different projects. To create a virtual environment, use the following syntax:: - $ mkvirtualenv pelican + $ mkvirtualenv pelican -To manually install the dependencies:: +To clone the Pelican source:: + $ git clone https://github.com/getpelican/pelican.git src/pelican + +To install the development dependencies:: + + $ cd src/pelican $ pip install -r dev_requirements.txt + +To install Pelican and its dependencies:: + $ python setup.py develop Running the test suite ====================== Each time you add a feature, there are two things to do regarding tests: -checking that the existing tests pass, and adding tests for your new feature -or for the bug you're fixing. +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/ + +testing for python3 +------------------- + +On Python 3, if you have installed the Py3k compatible versions of the +plugins manual testing with ``unit2 discover`` is also straightforward. + +However, you must tell tox to use those Py3k libraries. If you forget this, +tox will pull the regular packages from PyPi and the tests will fail. + +Tell tox about the local packages thusly: enter the source directory of +smartypants and run tox there. Do this again for typogrify and webassets. +Smartypants and typogrify do not have real tests, and webassets will fail +noisily, but as a result we get these libraries neatly packaged in tox's +distshare directory. And this we need to run tox for Pelican. + Coding standards ================ -Try to respect what is described in the PEP8 -(http://www.python.org/dev/peps/pep-0008/) when providing patches. This can be -eased by the pep8 tool (http://pypi.python.org/pypi/pep8) or by Flake8, which -will give you some other cool hints about what's good or wrong -(http://pypi.python.org/pypi/flake8/) +Try to respect what is described in the `PEP8 specification +`_ when providing patches. 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. + +Python3 support +=============== + +Here are some tips that may be useful when doing some code for both python2 and +python3 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 a3829d65..a8617b30 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,7 +1,36 @@ Frequently Asked Questions (FAQ) ################################ -Here is a summary of the frequently asked questions for Pelican. +Here are some frequently asked questions about Pelican. + +What's the best way to communicate a problem, question, or suggestion? +====================================================================== + +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. + +If you are 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 +suggestions or problems you might have via IRC or the `issue tracker +`_. + +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. + +You can also contribute by creating themes and improving the documentation. Is it mandatory to have a configuration file? ============================================= @@ -15,7 +44,7 @@ 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 ``.codehilite pre`` class in your theme's CSS file. To see how various styles can be used to render Django code, for example, you can use the demo `on the project website `_. @@ -25,27 +54,90 @@ How do I create my own theme? Please refer to :ref:`theming-pelican`. -How can I help? -================ - -There are several ways to help out. First, you can use Pelican and report any -suggestions or problems you might have on `the bugtracker -`_. - -If you want to contribute, please fork `the git repository -`_, make your changes, and issue -a pull request. I'll review your changes as soon as possible. - -You can also contribute by creating themes and improving the documentation. - I want to use Markdown, but I got an error. =========================================== Markdown is not a hard dependency for Pelican, so you will need to explicitly -install it. You can do so by typing:: +install it. You can do so by typing the following, including ``sudo`` if +required:: - $ (sudo) pip install markdown + (sudo) pip install markdown -In case you don't have pip installed, consider installing it via:: +If you don't have pip installed, consider installing the pip installer via:: - $ (sudo) easy_install pip + (sudo) easy_install pip + +Can I use arbitrary meta-data in my templates? +============================================== + +Yes. For example, to include a modified date in a Markdown post, one could +include the following at the top of the article:: + + Modified: 2012-08-08 + +That meta-data can then be accessed in the template:: + + {% if article.modified %} + Last modified: {{ article.modified }} + {% endif %} + +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. + + :template: template_name + +Then just make sure your theme contains the relevant template file (e.g. +``template_name.html``). + +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:: + + FEED_ALL_ATOM = None + CATEGORY_FEED_ATOM = None + +Please note that ``None`` and ``''`` are not the same thing. The word ``None`` +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 +`_. +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. + +Feeds are still generated when this warning is displayed but 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:: + + FEED -> FEED_ATOM + TAG_FEED -> TAG_FEED_ATOM + CATEGORY_FEED -> CATEGORY_FEED_ATOM + +Starting in 3.1, the new feed ``FEED_ALL_ATOM`` has been introduced: this +feed will aggregate all posts regardless of their language. This setting +generates ``'feeds/all.atom.xml'`` by default and ``FEED_ATOM`` now defaults to +``None``. The following feed setting has also been renamed:: + + TRANSLATION_FEED -> TRANSLATION_FEED_ATOM + +Older themes that referenced the old setting names may not link properly. +In order to rectify this, please update your theme for compatibility by changing +the relevant values in your template files. For an example of complete feed +headers and usage please check out the ``simple`` theme. diff --git a/docs/fr/configuration.rst b/docs/fr/configuration.rst index fadaf258..5388dae3 100644 --- a/docs/fr/configuration.rst +++ b/docs/fr/configuration.rst @@ -52,19 +52,25 @@ détails au prochain chapitre. Flux de syndication =================== -CATEGORY_FEED : +CATEGORY_FEED_ATOM : Chemin d’écriture des flux Atom liés aux catégories ; CATEGORY_FEED_RSS : Idem pour les flux rss (Optionnel); -FEED : - Chemin du flux Atom global ; +FEED_ATOM : + Chemin du flux Atom global; FEED_RSS : Chemin du flux Rss global (Optionnel); -TAG_FEED : +FEED_ALL_ATOM : + Chemin du flux Atom global qui inclut la totalité des posts, indépendamment de la langue; + +FEED_ALL_RSS : + Chemin du flux Rss global qui inclut la totalité des posts, indépendamment de la langue (Optionnel); + +TAG_FEED_ATOM : Chemin des flux Atom pour les tags (Optionnel); TAG_FEED_RSS : @@ -77,8 +83,11 @@ Traductions DEFAULT_LANG : Le langage par défaut à utiliser. «*en*» par défaut ; -TRANSLATION_FEED : - Chemin du flux pour les traductions. +TRANSLATION_FEED_ATOM : + Chemin du flux Atom pour les traductions. + +TRANSLATION_FEED_RSS : + Chemin du flux RSS pour les traductions. Thèmes @@ -111,7 +120,7 @@ LINKS : PDF_PROCESSOR : Génère ou non les articles et pages au format pdf ; -REVERSE_ARCHIVE_ORDER : +NEWEST_FIRST_ARCHIVES : Met les articles plus récent en tête de l'archive ; SOCIAL : @@ -133,8 +142,12 @@ Pelican est fournit avec :doc:`pelican-themes`, un script permettant de gérer l Paramètres divers ================= -FALLBACK_ON_FS_DATE : - Si *True*, Pelican se basera sur le *mtime* du fichier s'il n'y a pas de date spécifiée dans le fichier de l'article ; +DEFAULT_DATE: + Date par défaut à utiliser si l'information de date n'est pas spécifiée + dans les metadonnées de l'article. + Si 'fs', Pelican se basera sur le *mtime* du fichier. + Si c'est un tuple, il sera passé au constructeur datetime.datetime pour + générer l'objet datetime utilisé par défaut. KEEP_OUTPUT DIRECTORY : Ne génère que les fichiers modifiés et n'efface pas le repertoire de sortie ; @@ -150,8 +163,3 @@ SITEURL : STATIC_PATHS : Les chemins statiques que vous voulez avoir accès sur le chemin de sortie "statique" ; - - - - - diff --git a/docs/fr/index.rst b/docs/fr/index.rst index 9fd0c0f0..2deb5050 100644 --- a/docs/fr/index.rst +++ b/docs/fr/index.rst @@ -30,7 +30,7 @@ Code source =========== Vous pouvez accéder au code source via git à l'adresse -http://github.com/ametaireau/pelican/ +http://github.com/getpelican/pelican/ Feedback ! ========== diff --git a/docs/fr/installation.rst b/docs/fr/installation.rst index e853979d..da327725 100644 --- a/docs/fr/installation.rst +++ b/docs/fr/installation.rst @@ -24,7 +24,7 @@ Pour installer Pelican en reprenant le code via Github, nous aurons besoin du pa git-core pour récupérez les sources de Pelican. Puis nous procédons à l’installation :: # apt-get install git-core - $ git clone https://github.com/ametaireau/pelican.git + $ git clone https://github.com/getpelican/pelican.git $ cd pelican # python setup.py install diff --git a/docs/fr/themes.rst b/docs/fr/themes.rst index c6f45300..20d9d41f 100644 --- a/docs/fr/themes.rst +++ b/docs/fr/themes.rst @@ -10,7 +10,7 @@ Pelican utlise le très bon moteur de template `jinja2 ` pour produire de l'HTML. La syntaxe de jinja2 est vraiment très simple. Si vous voulez créer votre propre thème, soyez libre de prendre inspiration sur le theme "simple" qui est disponible `ici -`_ +`_ Structure ========= diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 93d578a0..0952c7d9 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -14,17 +14,19 @@ 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:: +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:: - $ sudo pip install --upgrade virtualenv virtualenvwrapper $ mkvirtualenv pelican - $ pip install pelican Once the virtual environment has been created and activated, Pelican can be be installed via ``pip`` or ``easy_install`` as noted above. Alternatively, if -you have the project source, you can install Pelican using the distutils +you have the project source, you can install Pelican using the distutils method:: $ cd path-to-Pelican-source @@ -33,19 +35,23 @@ method:: If you have Git installed and prefer to install the latest bleeding-edge version of Pelican rather than a stable release, use the following command:: - $ pip install -e git://github.com/ametaireau/pelican#egg=pelican + $ pip install -e git://github.com/getpelican/pelican#egg=pelican If you plan on using Markdown as a markup format, you'll need to install the Markdown library as well:: $ pip install Markdown +If you want to use AsciiDoc you need to install it from `source +`_ or use your operating +system's package manager. + Upgrading --------- -If you installed a stable Pelican release via pip or easy_install and wish to -upgrade to the latest stable release, you can do so by adding ``--upgrade`` to -the relevant command. For pip, that would be:: +If you installed a stable Pelican release via ``pip`` or ``easy_install`` and +wish to upgrade to the latest stable release, you can do so by adding +``--upgrade`` to the relevant command. For pip, that would be:: $ pip install --upgrade pelican @@ -55,28 +61,38 @@ perform the same step to install the most recent version. Dependencies ------------ -At this time, Pelican is dependent on the following Python packages: +At this time, Pelican core is dependent on the following Python packages: -* 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. Optionally: -* pygments, for syntax highlighting -* Markdown, for supporting Markdown as an input format +* `markdown `_, for supporting Markdown as + an input format +* `typogrify `_, for typographical + enhancements Kickstart a blog ================ Following is a brief tutorial for those who want to get started right away. -We're going to assume Pelican was installed in a virtual environment via the -following steps (if you're not using a virtual environment for Pelican, you can -skip to the ``pelican-quickstart`` command):: +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:: - $ sudo pip install --upgrade virtualenv virtualenvwrapper $ mkvirtualenv pelican $ pip install pelican Markdown @@ -107,11 +123,21 @@ instead:: $ make regenerate -To serve the site so it can be previewed in your browser:: +To serve the site so it can be previewed in your browser at +http://localhost:8000:: $ make serve -Visit http://localhost:8000 in your browser to see your site. +Normally you would need to run ``make regenerate`` and ``make serve`` in two +separate terminal sessions, but you can run both at once via:: + + $ make devserver + +The above command will simultaneously run Pelican in regeneration mode as well +as serve the output at http://localhost:8000. Once you are done testing your +changes, you should stop the development server via:: + + $ ./develop_server.sh stop When you're ready to publish your site, you can upload it via the method(s) you chose during the ``pelican-quickstart`` questionnaire. For this example, we'll @@ -140,36 +166,62 @@ following syntax (give your file the ``.rst`` extension):: :date: 2010-10-03 10:20 :tags: thats, awesome :category: yeah + :slug: my-super-post :author: Alexis Metaireau + :summary: Short version for index and feeds -You can also use Markdown syntax (with a file ending in ``.md``). -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:: +Pelican implements an extension to reStructuredText to enable support for the +``abbr`` HTML tag. To use it, write something like this in your post:: + + 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:: - Date: 2010-12-03 Title: My super title + Date: 2010-12-03 10:20 Tags: thats, awesome + Category: yeah 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 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``. +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 +``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 +``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.*)'`` + +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 +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 ``content/`` +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. @@ -189,20 +241,68 @@ run the ``pelican`` command with the ``-r`` or ``--autoreload`` option. Pages ----- -If you create a folder named ``pages``, all the files in it will be used to -generate static pages. +If you create a folder named ``pages`` inside the content folder, all the +files in it will be used to generate static pages. -Then, use the ``DISPLAY_PAGES_ON_MENU`` setting, which will add all the pages to -the menu. +Then, use the ``DISPLAY_PAGES_ON_MENU`` setting to add all those pages to +the primary navigation menu. 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 things like making error pages that fit the generated theme of your site. +Linking to internal content +--------------------------- + +From Pelican 3.1 onwards, it is now possible to specify intra-site links to +files in the *source content* hierarchy instead of files in the *generated* +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``. + +For example, you may want to add links between "article1" and "article2" given +the structure:: + + website/ + ├── content + │   ├── article1.rst + │   └── cat/ + │      └── article2.md + └── pelican.conf.py + +In this example, ``article1.rst`` could look like:: + + Title: The first article + Date: 2012-12-01 + + See below intra-site link examples in reStructuredText format. + + `a link relative to content root <|filename|/cat/article2.md>`_ + `a link relative to current file <|filename|cat/article2.md>`_ + +and ``article2.md``:: + + Title: The second article + Date: 2012-12-01 + + 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:: + + 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"]``. + Importing an existing blog -------------------------- -It is possible to import your blog from Dotclear, WordPress, and RSS feeds using +It is possible to import your blog from Dotclear, WordPress, and RSS feeds using a simple script. See :ref:`import`. Translations @@ -248,25 +348,27 @@ then instead ensure that the translated article titles are identical, since the slug will be auto-generated from the article title. Syntax highlighting ---------------------- +------------------- Pelican is able to provide colorized syntax highlighting for your code blocks. -To do so, you have to use the following conventions (you need to put this in -your content files). +To do so, you have to use the following conventions inside your content files. -For RestructuredText:: +For reStructuredText, use the code-block directive:: .. code-block:: identifier - your code goes here + -For Markdown, format your code blocks thusly:: +For Markdown, include the language identifier just above the code block, +indenting both the identifier and code:: - :::identifier - your code goes here + A block of text. -The specified identifier should be one that appears on the -`list of available lexers `_. + :::identifier + + +The specified identifier (e.g. ``python``, ``ruby``) should be one that +appears on the `list of available lexers `_. Publishing drafts ----------------- @@ -284,9 +386,11 @@ 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 + 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 ccf3ffe2..7ca3142d 100644 --- a/docs/importer.rst +++ b/docs/importer.rst @@ -4,71 +4,90 @@ 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 - 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 +- *BeautifulSoup*, for WordPress and Dotclear import. Can be installed like + any other Python package (``pip install BeautifulSoup``). +- *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] -| input +:: + + pelican-import [-h] [--wpfile] [--dotclear] [--feed] [-o OUTPUT] + [-m MARKUP] [--dir-cat] [--strip-raw] [--disable-slugs] + input + +Positional arguments +-------------------- + + input The input file to read Optional arguments -"""""""""""""""""" +------------------ - -h, --help show this help message and exit - --wpfile Wordpress XML export - --dotclear Dotclear export - --feed Feed to parse + -h, --help Show this help message and exit + --wpfile WordPress XML export (default: False) + --dotclear Dotclear export (default: False) + --feed Feed to parse (default: False) -o OUTPUT, --output OUTPUT - Output path - -m MARKUP Output markup + Output path (default: output) + -m MARKUP, --markup MARKUP + Output markup format (supports rst & markdown) + (default: rst) --dir-cat Put files in directories with categories name + (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) + --disable-slugs Disable storing slugs from imported posts within + output. With this disabled, your Pelican URLs may not + be consistent with your original posts. (default: + False) + Examples ======== -for WordPress:: +For WordPress:: $ pelican-import --wpfile -o ~/output ~/posts.xml -for Dotclear:: +For Dotclear:: $ pelican-import --dotclear -o ~/output ~/backup.txt + Tests ===== To test the module, one can use sample files: -- for Wordpress: http://wpcandy.com/made/the-sample-post-collection +- for 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 34a1355c..ebe1ace6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,16 +1,16 @@ Pelican -####### +======= -Pelican is a simple weblog generator, written in Python. +Pelican is a static site generator, written in Python_. -* Write your weblog entries directly with your editor of choice (vim!) in - reStructuredText or Markdown -* A simple CLI tool to (re)generate the weblog +* 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 * Completely static output is easy to host anywhere Features -======== +-------- Pelican currently supports: @@ -18,40 +18,38 @@ Pelican currently supports: * 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 `_) +* 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 -* Compilation of less css (optional) +* Asset management with `webassets`_ (optional) * Import from WordPress, Dotclear, or RSS feeds * Integration with external tools: Twitter, Google Analytics, etc. (optional) -Why the name "Pelican" ? -======================== +Why the name "Pelican"? +----------------------- -Heh, you didn't notice? "Pelican" is an anagram for « Calepin » ;) +"Pelican" is an anagram for *calepin*, which means "notebook" in French. ;) Source code -=========== +----------- -You can access the source code via git at http://github.com/ametaireau/pelican/ +You can access the source code at: https://github.com/getpelican/pelican Feedback / Contact us -===================== +--------------------- -If you want to see new features in Pelican, don't hesitate to tell me, to clone -the repository, etc. That's open source, dude! +If you want to see new features in Pelican, don't hesitate to offer suggestions, +clone the repository, etc. There are many ways to :doc:`contribute`. +That's open source, dude! -Contact me at "alexis at notmyidea dot org" for any request/feedback! You can -also join the team at `#pelican on irc.freenode.org -`_ -(or if you don't have any IRC client, use `the webchat -`_) -for quick feedback. +Send a message to "authors at getpelican dot com" with any requests/feedback! You +can also join the team at `#pelican on Freenode`_ (or if you don't have an IRC +client handy, use the webchat_ for quick feedback. Documentation -============= +------------- A French version of the documentation is available at :doc:`fr/index`. @@ -69,3 +67,17 @@ A French version of the documentation is available at :doc:`fr/index`. tips contribute report + changelog + +.. Links + +.. _Python: http://www.python.org/ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Markdown: http://daringfireball.net/projects/markdown/ +.. _AsciiDoc: http://www.methods.co.nz/asciidoc/index.html +.. _Jinja2: http://jinja.pocoo.org/ +.. _`Pelican documentation`: http://docs.getpelican.com/latest/ +.. _`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 6b6f991f..cadd300b 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -12,8 +12,8 @@ original author wrote with some software design information. Overall structure ================= -What Pelican does is take a list of files and process them into some -sort of output. Usually, the input files are reStructuredText and Markdown +What Pelican does is take a list of files and process them into some sort of +output. Usually, the input files are reStructuredText, Markdown and AsciiDoc files, and the output is a blog, but both input and output can be anything you want. @@ -23,9 +23,9 @@ 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 (Markdown and - reStructuredText for now, but the system is extensible). Given a file, they return - metadata (author, tags, category, etc.) and content (HTML-formatted). +* **Readers** are used to read from various formats (AsciiDoc, Markdown and + reStructuredText for now, but the system is extensible). Given a file, they + return metadata (author, tags, category, etc.) and content (HTML-formatted). * **Generators** generate the different outputs. For instance, Pelican comes with ``ArticlesGenerator`` and ``PageGenerator``. Given a configuration, they can do @@ -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? @@ -81,7 +79,7 @@ both; only the existing ones will be called. context is shared between all generators, and will be passed to the templates. For instance, the ``PageGenerator`` ``generate_context`` method finds all the pages, transforms them into objects, and populates the context - with them. Be careful *not* to output anything using this context at this + with them. Be careful *not* to output anything using this context at this stage, as it is likely to change by the effect of other generators. * ``generate_output`` is then called. And guess what is it made for? Oh, diff --git a/docs/pelican-themes.rst b/docs/pelican-themes.rst index c7cbc5b7..23be8355 100644 --- a/docs/pelican-themes.rst +++ b/docs/pelican-themes.rst @@ -64,7 +64,7 @@ In this example, we can see there are three themes available: ``notmyidea``, ``s Note that you can combine the ``--list`` option with the ``-v`` or ``--verbose`` option to get more verbose output, like this: .. code-block:: console - + $ pelican-themes -v -l /usr/local/lib/python2.6/dist-packages/pelican-2.6.0-py2.6.egg/pelican/themes/notmyidea /usr/local/lib/python2.6/dist-packages/pelican-2.6.0-py2.6.egg/pelican/themes/two-column (symbolic link to `/home/skami/Dev/Python/pelican-themes/two-column') @@ -118,7 +118,7 @@ Creating symbolic links To symbolically link a theme, you can use the ``-s`` or ``--symlink``, which works exactly as the ``--install`` option: .. code-block:: console - + # pelican-themes --symlink ~/Dev/Python/pelican-themes/two-column In this example, the ``two-column`` theme is now symbolically linked to the Pelican themes path, so we can use it, but we can also modify it without having to reinstall it after each modification. @@ -130,11 +130,11 @@ This is useful for theme development: $ sudo pelican-themes -s ~/Dev/Python/pelican-themes/two-column $ pelican ~/Blog/content -o /tmp/out -t two-column $ firefox /tmp/out/index.html - $ vim ~/Dev/Pelican/pelican-themes/two-coumn/static/css/main.css + $ vim ~/Dev/Pelican/pelican-themes/two-coumn/static/css/main.css $ pelican ~/Blog/content -o /tmp/out -t two-column $ cp /tmp/bg.png ~/Dev/Pelican/pelican-themes/two-coumn/static/img/bg.png $ pelican ~/Blog/content -o /tmp/out -t two-column - $ vim ~/Dev/Pelican/pelican-themes/two-coumn/templates/index.html + $ vim ~/Dev/Pelican/pelican-themes/two-coumn/templates/index.html $ pelican ~/Blog/content -o /tmp/out -t two-column @@ -152,7 +152,7 @@ The ``--install``, ``--remove`` and ``--symlink`` option are not mutually exclus --symlink ~/Dev/Python/pelican-themes/two-column \ --verbose -In this example, the theme ``notmyidea-cms`` is replaced by the theme ``notmyidea-cms-fr`` +In this example, the theme ``notmyidea-cms`` is replaced by the theme ``notmyidea-cms-fr`` @@ -162,5 +162,3 @@ See also - http://docs.notmyidea.org/alexis/pelican/ - ``/usr/share/doc/pelican/`` if you have installed Pelican using the `APT repository `_ - - diff --git a/docs/plugins.rst b/docs/plugins.rst index 3e009e33..c48e1c15 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -3,25 +3,25 @@ Plugins ####### -Since version 3.0, Pelican manages plugins. Plugins are a way to add features -to Pelican without having to directly hack Pelican code. +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 core plugins, but you can easily implement -your own (and this page describes how). +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 ================== -To load plugins, you have to specify them in your settings file. You have two -ways to do so. -Either by specifying strings with the path to the callables:: +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 = ['pelican.plugins.gravatar',] -Or by importing them and adding them to the list:: +Alternatively, another method is to import them and add them to the list:: from pelican.plugins import gravatar - PLUGINS = [gravatar, ] + PLUGINS = [gravatar,] If your plugins are not in an importable path, you can specify a ``PLUGIN_PATH`` in the settings:: @@ -33,7 +33,7 @@ How to create plugins ===================== Plugins are based on the concept of signals. Pelican sends signals, and plugins -subscribe to those signals. The list of signals are defined in a following +subscribe to those signals. The list of signals are defined in a subsequent section. The only rule to follow for plugins is to define a ``register`` callable, in @@ -48,47 +48,172 @@ which you map the signals to your plugin logic. Let's take a simple example:: signals.initialized.connect(test) + List of signals =============== Here is the list of currently implemented signals: -========================= ============================ ========================================= -Signal Arguments Description -========================= ============================ ========================================= -initialized pelican object -article_generate_context article_generator, metadata -article_generator_init article_generator invoked in the ArticlesGenerator.__init__ -========================= ============================ ========================================= +============================= ============================ =========================================================================== +Signal Arguments Description +============================= ============================ =========================================================================== +initialized pelican object +finalized pelican object invoked after all the generators are executed and just before pelican exits + usefull for custom post processing actions, such as: + - minifying js/css assets. + - notify/ping search engines with an updated sitemap. +generator_init generator invoked in the Generator.__init__ +article_generate_context article_generator, metadata +article_generate_preread article_generator invoked before a article is read in ArticlesGenerator.generate_context; + use if code needs to do something before every article is parsed +article_generator_init article_generator invoked in the ArticlesGenerator.__init__ +article_generator_finalized article_generator invoked at the end of ArticlesGenerator.generate_context +get_generators generators invoked in Pelican.get_generator_classes, + can return a Generator, or several + generator in a tuple or in a list. +pages_generate_context pages_generator, metadata +pages_generator_init pages_generator invoked in the PagesGenerator.__init__ +============================= ============================ =========================================================================== -The list is currently small, don't hesitate to add signals and make a pull +The list is currently small, so don't hesitate to add signals and make a pull request if you need them! +.. note:: + + The signal ``content_object_init`` can send a different type of object as + the argument. If you want to register only one type of object then you will + need to specify the sender when you are connecting to the signal. + + :: + + from pelican import signals + from pelican import contents + + def test(sender, instance): + print "%s : %s content initialized !!" % (sender, instance) + + def register(): + signals.content_object_init.connect(test, sender=contents.Article) + + + List of plugins =============== -Not all the list are described here, but a few of them have been extracted from -the Pelican core and provided in ``pelican.plugins``. They are described here: +The following plugins are currently included with Pelican: -Tag cloud ---------- +* `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`` -Translation ------------ +Ideas for plugins that haven't been written yet: -Github Activity +* 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. + +Many of Webasset's available compilers have additional configuration options +(i.e. 'Less', 'Sass', 'Stylus', 'Closure_js'). You can pass these options to +Webassets using the ``ASSET_CONFIG`` in your settings file. + +The following will handle Google Closure's compilation level and locate +LessCSS's binary: + +.. code-block:: python + + ASSET_CONFIG = (('closure_compressor_optimization', 'WHITESPACE_ONLY'), + ('less_bin', 'lessc.cmd'), ) + +.. _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 +This plugin makes use of the `feedparser`_ library that you'll need to install. -Set the ``GITHUB_ACTIVITY_FEED`` parameter to your Github activity feed. -For example, my setting would look like:: +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/kpanic.atom' + GITHUB_ACTIVITY_FEED = 'https://github.com/getpelican.atom' -On the templates side, you just have to iterate over the ``github_activity`` -variable, as in the example:: +On the template side, you just have to iterate over the ``github_activity`` +variable, as in this example:: {% if GITHUB_ACTIVITY_FEED %} {% 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/ -``github_activity`` is a list of lists. The first element is the title -and the second element is the raw HTML from Github. +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 %} + + {% 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/report.rst b/docs/report.rst index f12f3048..f3ddff31 100644 --- a/docs/report.rst +++ b/docs/report.rst @@ -4,7 +4,7 @@ Some history about Pelican .. warning:: This page comes from a report the original author (Alexis Métaireau) wrote - right after writing Pelican, in December 2010. The information may not be + right after writing Pelican, in December 2010. The information may not be up-to-date. Pelican is a simple static blog generator. It parses markup files @@ -113,7 +113,7 @@ concepts. Here is what happens when calling the ``generate_context`` method: * Read the folder “path”, looking for restructured text files, load - each of them, and construct a content object (``Article``) with it. To do so, + each of them, and construct a content object (``Article``) with it. To do so, use ``Reader`` objects. * Update the ``context`` with all those articles. diff --git a/docs/settings.rst b/docs/settings.rst index 3c557e87..baf9f60c 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -8,10 +8,16 @@ the command line:: Settings are configured in the form of a Python module (a file). You can see an example by looking at `/samples/pelican.conf.py -`_ +`_ All the setting identifiers must be set in all-caps, otherwise they will not be -processed. +processed. Setting values that are numbers (5, 20, etc.), booleans (True, +False, None, etc.), dictionaries, or tuples should *not* be enclosed in +quotation marks. All other values (i.e., strings) *must* be enclosed in +quotation marks. + +Unless otherwise specified, settings that refer to paths can be either absolute +or relative to the configuration file. The settings you define in the configuration file will be passed to the templates, which allows you to use your settings to add site-wide content. @@ -25,40 +31,65 @@ Basic settings Setting name (default value) What does it do? ===================================================================== ===================================================================== `AUTHOR` Default author (put your name) -`DATE_FORMATS` (``{}``) If you do manage multiple languages, you can - set the date formatting here. See "Date format and locales" - section below for details. +`DATE_FORMATS` (``{}``) If you manage multiple languages, you can set the date formatting + here. See the "Date format and locales" section below for details. +`USE_FOLDER_AS_CATEGORY` (``True``) When you don't specify a category in your post metadata, set this + setting to ``True``, and organize your articles in subfolders, the + subfolder will become the category of your post. If set to ``False``, + ``DEFAULT_CATEGORY`` will be used as a fallback. `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 not honor this setting. -`FALLBACK_ON_FS_DATE` (``True``) If True, Pelican will use the file system +`DEFAULT_DATE` (``None``) The default date you want to use. + If ``fs``, Pelican will use the file system timestamp information (mtime) if it can't get date information from the metadata. -`JINJA_EXTENSIONS` (``[]``) A list of any Jinja2 extensions you want to use. + If set to a tuple object, the default datetime object will instead + be generated by passing the tuple to the + ``datetime.datetime`` constructor. +`DEFAULT_METADATA` (``()``) The default metadata you want to use for all articles + and pages. +`FILENAME_METADATA` (``'(?P\d{4}-\d{2}-\d{2}).*'``) The regexp that will be used to extract any metadata + from the filename. All named groups that are matched + will be set in the metadata object. + The default value 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.*)'``. `DELETE_OUTPUT_DIRECTORY` (``False``) Delete the content of the output directory before generating new files. +`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. `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` and `md`. + 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. `OUTPUT_PATH` (``'output/'``) Where to output the generated files. -`PATH` (``None``) Path to look at for input files. -`PAGE_DIR` (``'pages'``) Directory to look at for pages. +`PATH` (``None``) Path to content directory to be processed by Pelican. +`PAGE_DIR` (``'pages'``) Directory to look at for pages, relative to `PATH`. `PAGE_EXCLUDES` (``()``) A list of directories to exclude when looking for pages. -`ARTICLE_DIR` (``''``) Directory to look at for articles. +`ARTICLE_DIR` (``''``) Directory to look at for articles, relative to `PATH`. `ARTICLE_EXCLUDES`: (``('pages',)``) A list of directories to exclude when looking for articles. -`PDF_GENERATOR` (``False``) Set to True if you want to have PDF versions - of your documents. You will need to install - `rst2pdf`. +`PDF_GENERATOR` (``False``) Set to ``True`` if you want PDF versions of your documents to be. + generated. You will need to install ``rst2pdf``. +`OUTPUT_SOURCES` (``False``) Set to True if you want to copy the articles and pages in their + original format (e.g. Markdown or reStructuredText) to the + specified OUTPUT_PATH. +`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. @@ -66,41 +97,42 @@ Setting name (default value) What doe `SITENAME` (``'A Pelican Blog'``) Your site name `SITEURL` Base URL of your website. Not defined by default, so it is best to specify your SITEURL; if you do not, feeds - will not be generated with properly-formed URLs. You should + will not be generated with properly-formed URLs. You should include ``http://`` and your domain, with no trailing slash at the end. Example: ``SITEURL = 'http://mydomain.com'`` +`TEMPLATE_PAGES` (``None``) A mapping containing template pages that will be rendered with + the blog entries. See :ref:`template_pages`. `STATIC_PATHS` (``['images']``) The static paths you want to have accessible on the output path "static". By default, - Pelican will copy the 'images' folder to the + Pelican will copy the "images" folder to the output folder. `TIMEZONE` The timezone used in the date information, to - generate Atom and RSS feeds. See the "timezone" + generate Atom and RSS feeds. See the *Timezone* section below for more info. -`TYPOGRIFY` (``False``) If set to true, some - additional transformations will be done on the - generated HTML, using the `Typogrify +`TYPOGRIFY` (``False``) If set to True, several typographical improvements will be + incorporated into the generated HTML via the `Typogrify `_ - library -`LESS_GENERATOR` (``FALSE``) Set to True or complete path to `lessc` (if not - found in system PATH) to enable compiling less - css files. Requires installation of `less css`_. + library, which can be installed via: ``pip install typogrify`` `DIRECT_TEMPLATES` (``('index', 'tags', 'categories', 'archives')``) List of templates that are used directly to render content. Typically direct templates are used to generate - index pages for collections of content e.g. tags and - category index pages. + index pages for collections of content (e.g. tags and + category index pages). `PAGINATED_DIRECT_TEMPLATES` (``('index',)``) Provides the direct templates that should be paginated. -`SUMMARY_MAX_LENGTH` (``50``) When creating a short summary of an article, this will +`SUMMARY_MAX_LENGTH` (``50``) When creating a short summary of an article, this will be the default length in words of the text created. - This only applies if your content does not otherwise - specify a summary. Setting to None will cause the summary + This only applies if your content does not otherwise + specify a summary. Setting to ``None`` will cause the summary to be a copy of the original content. - +`EXTRA_TEMPLATES_PATHS` (``[]``) A list of paths you want Jinja2 to search for templates. + Can be used to separate templates from the theme. + Example: projects, resume, profile ... + These templates need to use ``DIRECT_TEMPLATES`` setting. +`ASCIIDOC_OPTIONS` (``[]``) A list of options to pass to AsciiDoc. See the `manpage + `_ ===================================================================== ===================================================================== .. [#] Default is the system locale. -.. _less css: http://lesscss.org/ - URL settings ------------ @@ -111,13 +143,13 @@ when testing locally, and absolute URLs are reliable and most useful when publishing. One method of supporting both is to have one Pelican configuration file for local development and another for publishing. To see an example of this type of setup, use the ``pelican-quickstart`` script as described at the top of -the :doc:`Getting Started` page, which will produce two separate +the :doc:`Getting Started ` page, which will produce two separate configuration files for local development and publishing, respectively. You can customize the URLs and locations where files will be saved. The URLs and SAVE_AS variables use Python's format strings. These variables allow you to place -your articles in a location such as '{slug}/index.html' and link to them as -'{slug}' for clean URLs. These settings give you the flexibility to place your +your articles in a location such as ``{slug}/index.html`` and link to them as +``{slug}`` for clean URLs. These settings give you the flexibility to place your articles and pages anywhere you want. .. note:: @@ -138,41 +170,41 @@ Also, you can use other file metadata attributes as well: Example usage: -* ARTICLE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/' -* ARTICLE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/index.html' +* ARTICLE_URL = ``'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/'`` +* ARTICLE_SAVE_AS = ``'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/index.html'`` -This would save your articles in something like '/posts/2011/Aug/07/sample-post/index.html', -and the URL to this would be '/posts/2011/Aug/07/sample-post/'. +This would save your articles in something like ``/posts/2011/Aug/07/sample-post/index.html``, +and the URL to this would be ``/posts/2011/Aug/07/sample-post/``. -================================================ ===================================================== -Setting name (default value) what does it do? -================================================ ===================================================== -`ARTICLE_URL` ('{slug}.html') The URL to refer to an ARTICLE. -`ARTICLE_SAVE_AS` ('{slug}.html') The place where we will save an article. -`ARTICLE_LANG_URL` ('{slug}-{lang}.html') The URL to refer to an ARTICLE which doesn't use the - default language. -`ARTICLE_LANG_SAVE_AS` ('{slug}-{lang}.html' The place where we will save an article which - doesn't use the default language. -`PAGE_URL` ('pages/{slug}.html') The URL we will use to link to a page. -`PAGE_SAVE_AS` ('pages/{slug}.html') The location we will save the page. -`PAGE_LANG_URL` ('pages/{slug}-{lang}.html') The URL we will use to link to a page which doesn't - use the default language. -`PAGE_LANG_SAVE_AS` ('pages/{slug}-{lang}.html') The location we will save the page which doesn't - use the default language. -`AUTHOR_URL` ('author/{name}.html') The URL to use for an author. -`AUTHOR_SAVE_AS` ('author/{name}.html') The location to save an author. -`CATEGORY_URL` ('category/{name}.html') The URL to use for a category. -`CATEGORY_SAVE_AS` ('category/{name}.html') The location to save a category. -`TAG_URL` ('tag/{name}.html') The URL to use for a tag. -`TAG_SAVE_AS` ('tag/{name}.html') The location to save the tag page. -`_SAVE_AS` The location to save content generated from direct - templates. Where is the - upper case template name. -================================================ ===================================================== +==================================================== ===================================================== +Setting name (default value) What does it do? +==================================================== ===================================================== +`ARTICLE_URL` (``'{slug}.html'``) The URL to refer to an ARTICLE. +`ARTICLE_SAVE_AS` (``'{slug}.html'``) The place where we will save an article. +`ARTICLE_LANG_URL` (``'{slug}-{lang}.html'``) The URL to refer to an ARTICLE which doesn't use the + default language. +`ARTICLE_LANG_SAVE_AS` (``'{slug}-{lang}.html'``) The place where we will save an article which + doesn't use the default language. +`PAGE_URL` (``'pages/{slug}.html'``) The URL we will use to link to a page. +`PAGE_SAVE_AS` (``'pages/{slug}.html'``) The location we will save the page. +`PAGE_LANG_URL` (``'pages/{slug}-{lang}.html'``) The URL we will use to link to a page which doesn't + use the default language. +`PAGE_LANG_SAVE_AS` (``'pages/{slug}-{lang}.html'``) The location we will save the page which doesn't + use the default language. +`AUTHOR_URL` (``'author/{slug}.html'``) The URL to use for an author. +`AUTHOR_SAVE_AS` (``'author/{slug}.html'``) The location to save an author. +`CATEGORY_URL` (``'category/{slug}.html'``) The URL to use for a category. +`CATEGORY_SAVE_AS` (``'category/{slug}.html'``) The location to save a category. +`TAG_URL` (``'tag/{slug}.html'``) The URL to use for a tag. +`TAG_SAVE_AS` (``'tag/{slug}.html'``) The location to save the tag page. +`_SAVE_AS` The location to save content generated from direct + templates. Where is the + upper case template name. +==================================================== ===================================================== .. note:: - When any of `*_SAVE_AS` is set to False, files will not be created. + When any of the `*_SAVE_AS` settings is set to False, files will not be created. Timezone -------- @@ -191,14 +223,14 @@ Have a look at `the wikipedia page`_ to get a list of valid timezone values. Date format and locale ---------------------- -If no DATE_FORMAT is set, fall back to DEFAULT_DATE_FORMAT. If you need to +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`_ : .. parsed-literal:: - DATE_FORMAT = { + DATE_FORMATS = { 'en': '%a, %d %b %Y', 'jp': '%Y-%m-%d(%a)', } @@ -217,13 +249,13 @@ above: .. parsed-literal:: # On Unix/Linux - DATE_FORMAT = { + DATE_FORMATS = { 'en': ('en_US','%a, %d %b %Y'), 'jp': ('ja_JP','%Y-%m-%d(%a)'), } # On Windows - DATE_FORMAT = { + DATE_FORMATS = { 'en': ('usa','%a, %d %b %Y'), 'jp': ('jpn','%Y-%m-%d(%a)'), } @@ -239,6 +271,23 @@ can get a list of available locales via the ``locale -a`` command; see manpage .. _locale(1): http://linux.die.net/man/1/locale + +.. _template_pages: + +Template pages +============== + +If you want to generate custom pages besides your blog entries, you can point +any Jinja2 template file with a path pointing to the file and the destination +path for the generated file. + +For instance, if you have a blog with three static pages — a list of books, +your resume, and a contact page — you could have:: + + TEMPLATE_PAGES = {'src/books.html': 'dest/books.html', + 'src/resume.html': 'dest/resume.html', + 'src/contact.html': 'dest/contact.html'} + Feed settings ============= @@ -247,7 +296,7 @@ feeds if you prefer. Pelican generates category feeds as well as feeds for all your articles. It does not generate feeds for tags by default, but it is possible to do so using -the ``TAG_FEED`` and ``TAG_FEED_RSS`` settings: +the ``TAG_FEED_ATOM`` and ``TAG_FEED_RSS`` settings: ================================================ ===================================================== Setting name (default value) What does it do? @@ -257,20 +306,25 @@ Setting name (default value) What does it do? to define this (e.g., "http://feeds.example.com"). If you have already explicitly defined SITEURL (see above) and want to use the same domain for your - feeds, you can just set: `FEED_DOMAIN = SITEURL` -`FEED` (``'feeds/all.atom.xml'``) Relative URL to output the Atom feed. + feeds, you can just set: ``FEED_DOMAIN = SITEURL``. +`FEED_ATOM` (``None``, i.e. no Atom feed) Relative URL to output the Atom feed. `FEED_RSS` (``None``, i.e. no RSS) Relative URL to output the RSS feed. -`CATEGORY_FEED` ('feeds/%s.atom.xml'[2]_) Where to put the category Atom feeds. +`FEED_ALL_ATOM` (``'feeds/all.atom.xml'``) Relative URL to output the all posts Atom feed: + this feed will contain all posts regardless of their + language. +`FEED_ALL_RSS` (``None``, i.e. no all RSS) Relative URL to output the all posts RSS feed: + this feed will contain all posts regardless of their + language. +`CATEGORY_FEED_ATOM` ('feeds/%s.atom.xml'[2]_) Where to put the category Atom feeds. `CATEGORY_FEED_RSS` (``None``, i.e. no RSS) Where to put the category RSS feeds. -`TAG_FEED` (``None``, i.e. no tag feed) Relative URL to output the tag Atom feed. It should +`TAG_FEED_ATOM` (``None``, i.e. no tag feed) Relative URL to output the tag Atom feed. It should be defined using a "%s" match in the tag name. `TAG_FEED_RSS` (``None``, ie no RSS tag feed) Relative URL to output the tag RSS feed `FEED_MAX_ITEMS` Maximum number of items allowed in a feed. Feed item quantity is unrestricted by default. ================================================ ===================================================== -If you don't want to generate some of these feeds, set ``None`` to the -variables above. +If you don't want to generate some or any of these feeds, set the above variables to ``None``. .. [2] %s is the name of the category. @@ -281,7 +335,7 @@ If you want to use FeedBurner for your feed, you will likely need to decide upon a unique identifier. For example, if your site were called "Thyme" and hosted on the www.example.com domain, you might use "thymefeeds" as your unique identifier, which we'll use throughout this section for illustrative -purposes. In your Pelican settings, set the `FEED` attribute to +purposes. In your Pelican settings, set the `FEED_ATOM` attribute to "thymefeeds/main.xml" to create an Atom feed with an original address of `http://www.example.com/thymefeeds/main.xml`. Set the `FEED_DOMAIN` attribute to `http://feeds.feedburner.com`, or `http://feeds.example.com` if you are @@ -306,10 +360,10 @@ You can use the following settings to configure the pagination. ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`DEFAULT_ORPHANS` (0) The minimum number of articles allowed on the +`DEFAULT_ORPHANS` (``0``) The minimum number of articles allowed on the last page. Use this when you don't want to have a last page with very few articles. -`DEFAULT_PAGINATION` (False) The maximum number of articles to include on a +`DEFAULT_PAGINATION` (``False``) The maximum number of articles to include on a page, not including orphans. False to disable pagination. ================================================ ===================================================== @@ -323,9 +377,9 @@ following settings. ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`TAG_CLOUD_STEPS` (4) Count of different font sizes in the tag +`TAG_CLOUD_STEPS` (``4``) Count of different font sizes in the tag cloud. -`TAG_CLOUD_MAX_ITEMS` (100) Maximum number of tags in the cloud. +`TAG_CLOUD_MAX_ITEMS` (``100``) Maximum number of tags in the cloud. ================================================ ===================================================== The default theme does not support tag clouds, but it is pretty easy to add:: @@ -342,15 +396,16 @@ N matches `TAG_CLOUD_STEPS` -1). Translations ============ -Pelican offers a way to translate articles. See the Getting Started section for +Pelican offers a way to translate articles. See the :doc:`Getting Started ` section for more information. -================================================ ===================================================== -Setting name (default value) What does it do? -================================================ ===================================================== -`DEFAULT_LANG` (``'en'``) The default language to use. -`TRANSLATION_FEED` ('feeds/all-%s.atom.xml'[3]_) Where to put the feed for translations. -================================================ ===================================================== +===================================================== ===================================================== +Setting name (default value) What does it do? +===================================================== ===================================================== +`DEFAULT_LANG` (``'en'``) The default language to use. +`TRANSLATION_FEED_ATOM` ('feeds/all-%s.atom.xml'[3]_) Where to put the Atom feed for translations. +`TRANSLATION_FEED_RSS` (``None``, i.e. no RSS) Where to put the RSS feed for translations. +===================================================== ===================================================== .. [3] %s is the language @@ -360,48 +415,57 @@ Ordering content ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`REVERSE_ARCHIVE_ORDER` (``False``) Reverse the archives list order. (True: orders by date - in descending order, with newer articles first.) +`NEWEST_FIRST_ARCHIVES` (``True``) Order archives by newest first by date. (False: + orders by date with older articles first.) `REVERSE_CATEGORY_ORDER` (``False``) Reverse the category order. (True: lists by reverse alphabetical order; default lists alphabetically.) ================================================ ===================================================== -Theming -======= +Themes +====== -Theming is addressed in a dedicated section (see :ref:`theming-pelican`). -However, here are the settings that are related to theming. +Creating Pelican themes is addressed in a dedicated section (see :ref:`theming-pelican`). +However, here are the settings that are related to themes. ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`THEME` Theme to use to produce the output. Can be the - complete static path to a theme folder, or - chosen between the list of default themes (see - below) +`THEME` Theme to use to produce the output. Can be a relative + or absolute path to a theme folder, or the name of a + default theme or a theme installed via + ``pelican-themes`` (see below). `THEME_STATIC_PATHS` (``['static']``) Static theme paths you want to copy. Default value is `static`, but if your theme has other static paths, you can put them here. `CSS_FILE` (``'main.css'``) Specify the CSS file you want to load. -`WEBASSETS` (``False``) Asset management with `webassets` (see below) ================================================ ===================================================== -By default, two themes are available. You can specify them using the `-t` option: + +By default, two themes are available. You can specify them using the `THEME` setting or by passing the +``-t`` option to the ``pelican`` command: * notmyidea -* simple (a synonym for "full text" :) - -You can define your own theme too, and specify its placement in the same -manner. (Be sure to specify the full absolute path to it.) - -Here is :doc:`a guide on how to create your theme ` - -You can find a list of themes at http://github.com/ametaireau/pelican-themes. +* simple (a synonym for "plain text" :) +There are a number of other themes available at http://github.com/getpelican/pelican-themes. Pelican comes with :doc:`pelican-themes`, a small script for managing themes. -The `notmyidea` theme can make good use of the following settings. I recommend -using them in your themes as well. +You can define your own theme, either by starting from scratch or by duplicating +and modifying a pre-existing theme. Here is :doc:`a guide on how to create your theme `. + +Following are example ways to specify your preferred theme:: + + # Specify name of a built-in theme + THEME = "notmyidea" + # Specify name of a theme installed via the pelican-themes tool + THEME = "chunk" + # Specify a customized theme, via path relative to the settings file + THEME = "themes/mycustomtheme" + # Specify a customized theme, via absolute path + THEME = "~/projects/mysite/themes/mycustomtheme" + +The built-in ``notmyidea`` theme can make good use of the following settings. Feel +free to use them in your themes as well. ======================= ======================================================= Setting name What does it do ? @@ -429,64 +493,11 @@ Setting name What does it do ? if you want this button to appear. ======================= ======================================================= -In addition, you can use the "wide" version of the `notmyidea` theme by +In addition, you can use the "wide" version of the ``notmyidea`` theme by adding the following to your configuration:: CSS_FILE = "wide.css" -Asset management ----------------- - -The `WEBASSETS` setting allows to use the `webassets`_ module to manage assets -(css, js). The module must first be installed:: - - pip install webassets - -`webassets` allows to concatenate your assets and to use almost all of the -hype tools of the moment (see the `documentation`_): - -* css minifier (`cssmin`, `yuicompressor`, ...) -* css compiler (`less`, `sass`, ...) -* js minifier (`uglifyjs`, `yuicompressor`, `closure`, ...) - -Others filters include gzip compression, integration of images in css with -`datauri` and more. Webassets 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. - -When using it with Pelican, `webassets` is configured to process assets in the -``OUTPUT_PATH/theme`` directory. You can use it in your templates with a -template tag, 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 the version identifier: - -.. code-block:: html - - - -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 %} - -will produce a minified and gzipped js file: - -.. code-block:: html - - - -.. _webassets: https://github.com/miracle2k/webassets -.. _documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html - Example settings ================ diff --git a/docs/themes.rst b/docs/themes.rst index 8e432a95..19fd9274 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -6,7 +6,7 @@ How to create themes for Pelican Pelican uses the great `Jinja2 `_ templating engine to generate its HTML output. Jinja2 syntax is really simple. If you want to create your own theme, feel free to take inspiration from the `"simple" theme -`_. +`_. Structure ========= @@ -29,13 +29,13 @@ To make your own theme, you must follow the following structure:: └── 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/static` folder. I've put the CSS and image folders here, but they are + `theme` folder. I've put the CSS and image folders here, but they are just examples. Put what you need here. * `templates` contains all the templates that will be used to generate the content. I've just put the mandatory templates here; you can define your own if it helps you keep things organized while creating your theme. - + Templates and variables ======================= @@ -44,7 +44,7 @@ This document describes which templates should exist in a theme, and which variables will be passed to each template at generation time. All templates will receive the variables defined in your settings file, if they -are in all-caps. You can access them directly. +are in all-caps. You can access them directly. Common variables ---------------- @@ -55,18 +55,32 @@ All of these settings will be available to all templates. Variable Description ============= =================================================== articles The list of articles, ordered descending by date - All the elements are `Article` objects, so you can + All the elements are `Article` objects, so you can access their attributes (e.g. title, summary, author etc.) dates The same list of articles, but ordered by date, ascending -tags A key-value dict containing the tags (the keys) and - the list of respective articles (the values) -categories A key-value dict containing the categories (keys) +tags A list of (tag, articles) tuples, containing all + the tags. +categories A list of (category, articles) tuples, containing + all the categories. and the list of respective articles (values) 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 + index.html ---------- @@ -92,8 +106,8 @@ author.html This template will be processed for each of the existing authors, with output generated at output/author/`author_name`.html. -If pagination is active, subsequent pages will reside at -output/author/`author_name``n`.html. +If pagination is active, subsequent pages will reside as defined by setting +AUTHOR_SAVE_AS (`Default:` output/author/`author_name'n'`.html). =================== =================================================== Variable Description @@ -108,8 +122,8 @@ dates_paginator A paginator object for the article list, ordered by date, ascending. dates_page The current page of articles, ordered by date, ascending. -page_name 'author/`author_name`' -- useful for pagination - links +page_name AUTHOR_URL where everything after `{slug}` is + removed -- useful for pagination links =================== =================================================== category.html @@ -118,8 +132,8 @@ category.html This template will be processed for each of the existing categories, with output generated at output/category/`category_name`.html. -If pagination is active, subsequent pages will reside at -output/category/`category_name``n`.html. +If pagination is active, subsequent pages will reside as defined by setting +CATEGORY_SAVE_AS (`Default:` output/category/`category_name'n'`.html). =================== =================================================== Variable Description @@ -134,8 +148,8 @@ dates_paginator A paginator object for the list of articles, ordered by date, ascending dates_page The current page of articles, ordered by date, ascending -page_name 'category/`category_name`' -- useful for pagination - links +page_name CATEGORY_URL where everything after `{slug}` is + removed -- useful for pagination links =================== =================================================== article.html @@ -170,8 +184,8 @@ tag.html This template will be processed for each tag, with corresponding .html files saved as output/tag/`tag_name`.html. -If pagination is active, subsequent pages will reside at -output/tag/`tag_name``n`.html. +If pagination is active, subsequent pages will reside as defined in setting +TAG_SAVE_AS (`Default:` output/tag/`tag_name'n'`.html). =================== =================================================== Variable Description @@ -182,13 +196,33 @@ dates Articles related to this tag, but ordered by date, ascending articles_paginator A paginator object for the list of articles articles_page The current page of articles -dates_paginator A paginator object for the list of articles, +dates_paginator A paginator object for the list of articles, ordered by date, ascending dates_page The current page of articles, ordered by date, ascending -page_name 'tag/`tag_name`' -- useful for pagination links +page_name TAG_URL where everything after `{slug}` is removed + -- useful for pagination links =================== =================================================== +Feeds +===== + +The feed variables changed in 3.0. Each variable now explicitly lists ATOM or +RSS in the name. ATOM is still the default. Old themes will need to be updated. +Here is a complete list of the feed variables:: + + FEED_ATOM + FEED_RSS + FEED_ALL_ATOM + FEED_ALL_RSS + CATEGORY_FEED_ATOM + CATEGORY_FEED_RSS + TAG_FEED_ATOM + TAG_FEED_RSS + TRANSLATION_FEED_ATOM + TRANSLATION_FEED_RSS + + Inheritance =========== diff --git a/docs/tips.rst b/docs/tips.rst index 8905103b..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 d42526a3..a4d61b73 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import six + import os import re import sys @@ -8,70 +12,59 @@ import argparse from pelican import signals from pelican.generators import (ArticlesGenerator, PagesGenerator, - StaticGenerator, PdfGenerator, LessCSSGenerator) + StaticGenerator, PdfGenerator, + SourceFileGenerator, TemplatePagesGenerator) from pelican.log import init -from pelican.settings import read_settings, _DEFAULT_CONFIG -from pelican.utils import clean_output_dir, files_changed, file_changed +from pelican.settings import read_settings +from pelican.utils import (clean_output_dir, files_changed, file_changed, + NoFilesError) from pelican.writers import Writer __major__ = 3 -__minor__ = 0 -__version__ = "{0}.{1}".format(__major__, __minor__) +__minor__ = 2 +__micro__ = 0 +__version__ = "{0}.{1}.{2}".format(__major__, __minor__, __micro__) logger = logging.getLogger(__name__) class Pelican(object): - def __init__(self, settings=None, path=None, theme=None, output_path=None, - markup=None, delete_outputdir=False, plugin_path=None): - """Read the settings, and performs some checks on the environment - before doing anything else. + def __init__(self, settings): + """ + Pelican initialisation, performs some checks on the environment before + doing anything else. """ - if settings is None: - settings = _DEFAULT_CONFIG - - self.path = path or settings['PATH'] - if not self.path: - raise Exception('You need to specify a path containing the content' - ' (see pelican --help for more information)') - - if self.path.endswith('/'): - self.path = self.path[:-1] # define the default settings self.settings = settings - self._handle_deprecation() - self.theme = theme or settings['THEME'] - output_path = output_path or settings['OUTPUT_PATH'] - self.output_path = os.path.realpath(output_path) - self.markup = markup or settings['MARKUP'] - self.delete_outputdir = delete_outputdir \ - or settings['DELETE_OUTPUT_DIRECTORY'] - - # find the theme in pelican.theme if the given one does not exists - if not os.path.exists(self.theme): - theme_path = os.sep.join([os.path.dirname( - os.path.abspath(__file__)), "themes/%s" % self.theme]) - if os.path.exists(theme_path): - self.theme = theme_path - else: - raise Exception("Impossible to find the theme %s" % theme) + self.path = settings['PATH'] + 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() self.init_plugins() signals.initialized.send(self) + def init_path(self): + if not any(p in sys.path for p in ['', '.']): + 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: # if it's a string, then import it - if isinstance(plugin, basestring): + if isinstance(plugin, six.string_types): logger.debug("Loading plugin `{0}' ...".format(plugin)) plugin = __import__(plugin, globals(), locals(), 'module') - logger.debug("Registering plugin `{0}' ...".format(plugin.__name__)) + logger.debug("Registering plugin `{0}'".format(plugin.__name__)) plugin.register() def _handle_deprecation(self): @@ -114,10 +107,41 @@ class Pelican(object): self.settings[setting]) logger.warning("%s = '%s'" % (setting, self.settings[setting])) + if self.settings.get('FEED', False): + logger.warning('Found deprecated `FEED` in settings. Modify FEED' + ' to FEED_ATOM in your settings and theme for the same behavior.' + ' Temporarily setting FEED_ATOM for backwards compatibility.') + self.settings['FEED_ATOM'] = self.settings['FEED'] + + if self.settings.get('TAG_FEED', False): + logger.warning('Found deprecated `TAG_FEED` in settings. Modify ' + ' TAG_FEED to TAG_FEED_ATOM in your settings and theme for the ' + 'same behavior. Temporarily setting TAG_FEED_ATOM for backwards ' + 'compatibility.') + self.settings['TAG_FEED_ATOM'] = self.settings['TAG_FEED'] + + if self.settings.get('CATEGORY_FEED', False): + logger.warning('Found deprecated `CATEGORY_FEED` in settings. ' + 'Modify CATEGORY_FEED to CATEGORY_FEED_ATOM in your settings and ' + 'theme for the same behavior. Temporarily setting ' + 'CATEGORY_FEED_ATOM for backwards compatibility.') + self.settings['CATEGORY_FEED_ATOM'] =\ + self.settings['CATEGORY_FEED'] + + if self.settings.get('TRANSLATION_FEED', False): + logger.warning('Found deprecated `TRANSLATION_FEED` in settings. ' + 'Modify TRANSLATION_FEED to TRANSLATION_FEED_ATOM in your ' + 'settings and theme for the same behavior. Temporarily setting ' + 'TRANSLATION_FEED_ATOM for backwards compatibility.') + self.settings['TRANSLATION_FEED_ATOM'] =\ + self.settings['TRANSLATION_FEED'] + def run(self): """Run the generators and return""" context = self.settings.copy() + context['filenames'] = {} # share the dict between all the generators + context['localsiteurl'] = self.settings.get('SITEURL') # share generators = [ cls( context, @@ -126,7 +150,6 @@ class Pelican(object): self.theme, self.output_path, self.markup, - self.delete_outputdir ) for cls in self.get_generator_classes() ] @@ -142,21 +165,33 @@ class Pelican(object): writer = self.get_writer() - # pass the assets environment to the generators - if self.settings['WEBASSETS']: - generators[1].env.assets_environment = generators[0].assets_env - generators[2].env.assets_environment = generators[0].assets_env - for p in generators: if hasattr(p, 'generate_output'): p.generate_output(writer) + signals.finalized.send(self) + def get_generator_classes(self): generators = [StaticGenerator, ArticlesGenerator, PagesGenerator] + + if self.settings['TEMPLATE_PAGES']: + generators.append(TemplatePagesGenerator) if self.settings['PDF_GENERATOR']: generators.append(PdfGenerator) - if self.settings['LESS_GENERATOR']: # can be True or PATH to lessc - generators.append(LessCSSGenerator) + if self.settings['OUTPUT_SOURCES']: + generators.append(SourceFileGenerator) + + for pair in signals.get_generators.send(self): + (funct, value) = pair + + if not isinstance(value, (tuple, list)): + value = (value, ) + + for v in value: + if isinstance(v, type): + logger.debug('Found generator: {0}'.format(v)) + generators.append(v) + return generators def get_writer(self): @@ -213,31 +248,44 @@ def parse_arguments(): return parser.parse_args() -def get_instance(args): - markup = [a.strip().lower() for a in args.markup.split(',')]\ - if args.markup else None +def get_config(args): + config = {} + if args.path: + config['PATH'] = os.path.abspath(os.path.expanduser(args.path)) + if args.output: + config['OUTPUT_PATH'] = \ + os.path.abspath(os.path.expanduser(args.output)) + if args.markup: + config['MARKUP'] = [a.strip().lower() for a in args.markup.split(',')] + if args.theme: + abstheme = os.path.abspath(os.path.expanduser(args.theme)) + 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 + return config - settings = read_settings(args.settings) + +def get_instance(args): + + settings = read_settings(args.settings, override=get_config(args)) cls = settings.get('PELICAN_CLASS') - if isinstance(cls, basestring): + if isinstance(cls, six.string_types): module, cls_name = cls.rsplit('.', 1) module = __import__(module) cls = getattr(module, cls_name) - return cls(settings, args.path, args.theme, args.output, markup, - args.delete_outputdir) + return cls(settings) def main(): args = parse_arguments() init(args.verbosity) - # Split the markup languages only if some have been given. Otherwise, - # populate the variable with None. pelican = get_instance(args) try: if args.autoreload: + files_found_error = True while True: try: # Check source dir for changed files ending with the given @@ -245,8 +293,10 @@ 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 files_changed(pelican.path, pelican.markup, pelican.ignore_files) or \ + files_changed(pelican.theme, [''], pelican.ignore_files): + if not files_found_error: + files_found_error = True pelican.run() # reload also if settings.py changed @@ -258,11 +308,23 @@ def main(): 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 as e: + logger.warning( + "Caught exception \"{}\". Reloading.".format(e) + ) + continue else: pelican.run() - except Exception, e: - logger.critical(unicode(e)) + except Exception as e: + logger.critical(e) if (args.verbosity == logging.DEBUG): raise diff --git a/pelican/contents.py b/pelican/contents.py index b8bb0993..d7d7e558 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -1,19 +1,28 @@ # -*- 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 os import getenv from sys import platform, stdin from pelican.settings import _DEFAULT_CONFIG -from pelican.utils import slugify, truncate_html_words - +from pelican.utils import (slugify, truncate_html_words, memoized, + python_2_unicode_compatible, deprecated_attribute) +from pelican import signals +import pelican.utils logger = logging.getLogger(__name__) + class Page(object): """Represents a page Given a content, and metadata, create an adequate object. @@ -21,17 +30,23 @@ class Page(object): :param content: the string to parse, containing the original content. """ 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): + source_path=None, context=None): # init parameters if not metadata: metadata = {} if not settings: - settings = _DEFAULT_CONFIG + settings = copy.deepcopy(_DEFAULT_CONFIG) self.settings = settings self._content = content + self._context = context self.translations = [] local_metadata = dict(settings.get('DEFAULT_METADATA', ())) @@ -44,15 +59,13 @@ class Page(object): # also keep track of the metadata attributes available self.metadata = local_metadata + #default template if it's not defined in page + self.template = self._get_template() + # default author to the one in settings if not defined if not hasattr(self, 'author'): if 'AUTHOR' in settings: self.author = Author(settings['AUTHOR'], settings) - else: - title = filename.decode('utf-8') if filename else self.title - self.author = Author(getenv('USER', 'John Doe'), settings) - logger.warning(u"Author of `{0}' unknown, assuming that his name is " - "`{1}'".format(title, self.author)) # manage languages self.in_default_lang = True @@ -67,8 +80,8 @@ class Page(object): if not hasattr(self, 'slug') and hasattr(self, 'title'): self.slug = slugify(self.title) - if filename: - self.filename = filename + if source_path: + self.source_path = source_path # manage the date format if not hasattr(self, 'date_format'): @@ -78,17 +91,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 = pelican.utils.strftime(self.date, + self.date_format) # manage status if not hasattr(self, 'status'): @@ -101,6 +112,8 @@ class Page(object): if 'summary' in metadata: self._summary = metadata['summary'] + signals.content_object_init.send(self.__class__, instance=self) + def check_properties(self): """test that each mandatory property is set.""" for prop in self.mandatory_properties: @@ -109,13 +122,16 @@ class Page(object): @property def url_format(self): - return { + metadata = copy.copy(self.metadata) + metadata.update({ 'slug': getattr(self, 'slug', ''), 'lang': getattr(self, 'lang', 'en'), 'date': getattr(self, 'date', datetime.now()), - 'author': self.author, - 'category': getattr(self, 'category', 'misc'), - } + 'author': getattr(self, 'author', ''), + 'category': getattr(self, 'category', + self.settings['DEFAULT_CATEGORY']), + }) + return metadata def _expand_settings(self, key): fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper() @@ -125,13 +141,60 @@ class Page(object): 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 + 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. + """ + hrefs = re.compile(r""" + (?P<\s*[^\>]* # match tag with src and href attr + (?:href|src)\s*=) + + (?P["\']) # require value to be quoted + (?P\|(?P.*?)\|(?P.*?)) # the url value + \2""", re.X) + + def replacer(m): + 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. + if what == 'filename': + if value.startswith('/'): + value = value[1:] + else: + # 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)) + else: + logger.warning("Unable to find {fn}, skipping url" + " replacement".format(fn=value)) + + return 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) + @property def content(self): - if hasattr(self, "_get_content"): - content = self._get_content() - else: - content = self._content - return content + return self.get_content(self._context['localsiteurl']) def _get_summary(self): """Returns the summary of an article, based on the summary metadata @@ -140,7 +203,8 @@ class Page(object): return self._summary else: if self.settings['SUMMARY_MAX_LENGTH']: - return truncate_html_words(self.content, self.settings['SUMMARY_MAX_LENGTH']) + return truncate_html_words(self.content, + self.settings['SUMMARY_MAX_LENGTH']) return self.content def _set_summary(self, summary): @@ -153,18 +217,49 @@ class Page(object): url = property(functools.partial(get_url_setting, key='url')) save_as = property(functools.partial(get_url_setting, key='save_as')) + def _get_template(self): + if hasattr(self, 'template') and self.template is not None: + return self.template + else: + return self.default_template + + def get_relative_source_path(self, source_path=None): + """Return the relative path (from the content path) to the given + source_path. + + If no source path is specified, use the source path of this + content object. + """ + if not source_path: + source_path = self.source_path + + return os.path.relpath( + 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.source_path), + os.path.abspath(self.settings['PATH'])) + ) + class Article(Page): mandatory_properties = ('title', 'date', 'category') + default_template = 'article' class Quote(Page): base_properties = ('author', 'date') +@python_2_unicode_compatible +@functools.total_ordering class URLWrapper(object): def __init__(self, name, settings): - self.name = unicode(name) + self.name = name self.slug = slugify(self.name) self.settings = settings @@ -174,24 +269,41 @@ class URLWrapper(object): 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): + def _key(self): return self.name - def _from_settings(self, key): + def _normalize_key(self, key): + return six.text_type(key) + + def __eq__(self, other): + return self._key() == self._normalize_key(other) + + def __ne__(self, other): + return self._key() != self._normalize_key(other) + + def __lt__(self, other): + return self._key() < self._normalize_key(other) + + def __str__(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)) + if not isinstance(value, six.string_types): + logger.warning('%s is set to %s' % (setting, value)) return value else: - return unicode(value).format(**self.as_dict()) + if get_page_name: + return os.path.splitext(value)[0].format(**self.as_dict()) + else: + return 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')) @@ -202,18 +314,36 @@ class Category(URLWrapper): class Tag(URLWrapper): def __init__(self, name, *args, **kwargs): - super(Tag, self).__init__(unicode.strip(name), *args, **kwargs) + super(Tag, self).__init__(name.strip(), *args, **kwargs) class Author(URLWrapper): pass +@python_2_unicode_compatible +class StaticContent(object): + @deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0)) + def filepath(): + return None + + 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.source_path = os.path.join(settings['PATH'], src) + self.save_as = os.path.join(settings['OUTPUT_PATH'], self.url) + + def __str__(self): + return self.source_path + + 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: impossible to find informations about " + "'%s'" % (f, e)) return False diff --git a/pelican/generators.py b/pelican/generators.py index 4e9312cc..dda8c431 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -1,22 +1,26 @@ # -*- 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 operator import attrgetter, itemgetter -from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader -from jinja2.exceptions import TemplateNotFound +from jinja2 import (Environment, FileSystemLoader, PrefixLoader, ChoiceLoader, + BaseLoader, TemplateNotFound) -from pelican.contents import Article, Page, Category, is_valid_content +from pelican.contents import Article, Page, Category, StaticContent, \ + is_valid_content from pelican.readers import read_file -from pelican.utils import copy, process_translations, open +from pelican.utils import copy, process_translations, mkdir_p from pelican import signals @@ -36,14 +40,17 @@ class Generator(object): # templates cache self._templates = {} - self._templates_path = os.path.expanduser( - os.path.join(self.theme, 'templates')) + 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', []) theme_path = os.path.dirname(os.path.abspath(__file__)) simple_loader = FileSystemLoader(os.path.join(theme_path, "themes", "simple", "templates")) self.env = Environment( + trim_blocks=True, loader=ChoiceLoader([ FileSystemLoader(self._templates_path), simple_loader, # implicit inheritance @@ -58,6 +65,8 @@ class Generator(object): custom_filters = self.settings.get('JINJA_FILTERS', {}) self.env.filters.update(custom_filters) + signals.generator_init.send(self) + def get_template(self, name): """Return the template by name. Use self.theme to get the templates to use, and return a list of @@ -76,8 +85,10 @@ class Generator(object): :param path: the path to search the file on :param exclude: the list of path to exclude + :param extensions: the list of allowed extensions (if False, all + extensions are allowed) """ - if not extensions: + if extensions is None: extensions = self.markup files = [] @@ -91,10 +102,17 @@ class Generator(object): for e in exclude: if e in dirs: dirs.remove(e) - files.extend([os.sep.join((root, f)) for f in temp_files - if True in [f.endswith(ext) for ext in extensions]]) + 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))) return files + def add_source_path(self, content): + location = os.path.relpath(os.path.abspath(content.source_path), + os.path.abspath(self.path)) + self.context['filenames'][location] = content + def _update_context(self, items): """Update the context with the given items from the currrent processor. @@ -102,10 +120,39 @@ 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 +class _FileLoader(BaseLoader): + + def __init__(self, path, basedir): + self.path = path + self.fullpath = os.path.join(basedir, path) + + def get_source(self, environment, template): + if template != self.path or not os.path.exists(self.fullpath): + raise TemplateNotFound(template) + 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): + + def generate_output(self, writer): + for source, dest in self.settings['TEMPLATE_PAGES'].items(): + self.env.loader.loaders.insert(0, _FileLoader(source, self.path)) + try: + template = self.env.get_template(source) + rurls = self.settings.get('RELATIVE_URLS') + writer.write_file(dest, template, self.context, rurls) + finally: + del self.env.loader.loaders[0] + + class ArticlesGenerator(Generator): """Generate blog articles""" @@ -116,6 +163,7 @@ class ArticlesGenerator(Generator): self.dates = {} self.tags = defaultdict(list) self.categories = defaultdict(list) + self.related_posts = [] self.authors = defaultdict(list) super(ArticlesGenerator, self).__init__(*args, **kwargs) self.drafts = [] @@ -124,54 +172,74 @@ class ArticlesGenerator(Generator): def generate_feeds(self, writer): """Generate the feeds from the current context, and output files.""" - if self.settings.get('FEED'): + if self.settings.get('FEED_ATOM'): writer.write_feed(self.articles, self.context, - self.settings['FEED']) + self.settings['FEED_ATOM']) if self.settings.get('FEED_RSS'): 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'): + all_articles = list(self.articles) + for article in self.articles: + all_articles.extend(article.translations) + all_articles.sort(key=attrgetter('date'), reverse=True) + + if self.settings.get('FEED_ALL_ATOM'): + writer.write_feed(all_articles, self.context, + self.settings['FEED_ALL_ATOM']) + + if self.settings.get('FEED_ALL_RSS'): + writer.write_feed(all_articles, self.context, + 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'): + if self.settings.get('CATEGORY_FEED_ATOM'): writer.write_feed(arts, self.context, - self.settings['CATEGORY_FEED'] % cat) + self.settings['CATEGORY_FEED_ATOM'] % cat) if self.settings.get('CATEGORY_FEED_RSS'): writer.write_feed(arts, self.context, self.settings['CATEGORY_FEED_RSS'] % cat, feed_type='rss') - if self.settings.get('TAG_FEED') or self.settings.get('TAG_FEED_RSS'): + 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'): + if self.settings.get('TAG_FEED_ATOM'): writer.write_feed(arts, self.context, - self.settings['TAG_FEED'] % tag) + self.settings['TAG_FEED_ATOM'] % tag) if self.settings.get('TAG_FEED_RSS'): writer.write_feed(arts, self.context, self.settings['TAG_FEED_RSS'] % tag, feed_type='rss') - if self.settings.get('TRANSLATION_FEED'): + 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) for lang, items in translations_feeds.items(): items.sort(key=attrgetter('date'), reverse=True) - writer.write_feed(items, self.context, - self.settings['TRANSLATION_FEED'] % lang) + if self.settings.get('TRANSLATION_FEED_ATOM'): + writer.write_feed(items, self.context, + self.settings['TRANSLATION_FEED_ATOM'] % lang) + if self.settings.get('TRANSLATION_FEED_RSS'): + writer.write_feed(items, self.context, + self.settings['TRANSLATION_FEED_RSS'] % lang, + feed_type='rss') def generate_articles(self, write): """Generate the articles.""" - article_template = self.get_template('article') for article in chain(self.translations, self.articles): - write(article.save_as, - article_template, self.context, article=article, - category=article.category) + write(article.save_as, self.get_template(article.template), + self.context, article=article, category=article.category) def generate_direct_templates(self, write): """Generate direct templates pages""" @@ -183,7 +251,7 @@ class ArticlesGenerator(Generator): save_as = self.settings.get("%s_SAVE_AS" % template.upper(), '%s.html' % template) if not save_as: - continue + continue write(save_as, self.get_template(template), self.context, blog=True, paginated=paginated, @@ -198,7 +266,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=u'tag/%s' % tag) + page_name=tag.page_name) def generate_categories(self, write): """Generate category pages.""" @@ -208,7 +276,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=u'category/%s' % cat) + page_name=cat.page_name) def generate_authors(self, write): """Generate Author pages.""" @@ -218,14 +286,14 @@ class ArticlesGenerator(Generator): write(aut.save_as, author_template, self.context, author=aut, articles=articles, dates=dates, paginated={'articles': articles, 'dates': dates}, - page_name=u'author/%s' % aut) + page_name=aut.page_name) def generate_drafts(self, write): """Generate drafts pages.""" - article_template = self.get_template('article') for article in self.drafts: - write('drafts/%s.html' % article.slug, article_template, - self.context, article=article, category=article.category) + write('drafts/%s.html' % article.slug, + self.get_template(article.template), self.context, + article=article, category=article.category) def generate_pages(self, writer): """Generate the pages on the disk""" @@ -237,7 +305,6 @@ class ArticlesGenerator(Generator): self.generate_articles(write) self.generate_direct_templates(write) - # and subfolders after that self.generate_tags(write) self.generate_categories(write) @@ -245,7 +312,7 @@ class ArticlesGenerator(Generator): self.generate_drafts(write) def generate_context(self): - """change the context""" + """Add the articles into the shared context""" article_path = os.path.normpath( # we have to remove trailing slashes os.path.join(self.path, self.settings['ARTICLE_DIR']) @@ -255,33 +322,42 @@ class ArticlesGenerator(Generator): article_path, exclude=self.settings['ARTICLE_EXCLUDES']): 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 if 'category' not in metadata: - if os.path.dirname(f) == article_path: # if the article is not in a subdirectory - category = self.settings['DEFAULT_CATEGORY'] + 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)) else: - category = os.path.basename(os.path.dirname(f))\ - .decode('utf-8') + # if the article is not in a subdirectory + category = self.settings['DEFAULT_CATEGORY'] if category != '': metadata['category'] = Category(category, self.settings) - if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']: + if 'date' not in metadata and self.settings.get('DEFAULT_DATE'): + if self.settings['DEFAULT_DATE'] == 'fs': metadata['date'] = datetime.datetime.fromtimestamp( - os.stat(f).st_ctime) + os.stat(f).st_ctime) + else: + metadata['date'] = datetime.datetime( + *self.settings['DEFAULT_DATE']) signals.article_generate_context.send(self, metadata=metadata) article = Article(content, metadata, settings=self.settings, - filename=f) + source_path=f, context=self.context) if not is_valid_content(article, f): continue + self.add_source_path(article) + if article.status == "published": if hasattr(article, 'tags'): for tag in article.tags: @@ -290,8 +366,8 @@ class ArticlesGenerator(Generator): 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) @@ -299,13 +375,15 @@ class ArticlesGenerator(Generator): for article in self.articles: # only main articles are listed in categories, not translations self.categories[article.category].append(article) - self.authors[article.author].append(article) + # ignore blank authors as well as undefined + 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) self.dates.sort(key=attrgetter('date'), - reverse=self.context['REVERSE_ARCHIVE_ORDER']) + reverse=self.context['NEWEST_FIRST_ARCHIVES']) # create tag cloud tag_cloud = defaultdict(int) @@ -316,7 +394,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') @@ -338,14 +416,15 @@ 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')) + 'tag_cloud', 'authors', 'related_posts')) + + signals.article_generator_finalized.send(self) def generate_output(self, writer): self.generate_feeds(writer) @@ -360,6 +439,7 @@ class PagesGenerator(Generator): self.hidden_pages = [] self.hidden_translations = [] super(PagesGenerator, self).__init__(*args, **kwargs) + signals.pages_generator_init.send(self) def generate_context(self): all_pages = [] @@ -368,24 +448,27 @@ class PagesGenerator(Generator): os.path.join(self.path, self.settings['PAGE_DIR']), exclude=self.settings['PAGE_EXCLUDES']): try: - content, metadata = read_file(f) - except Exception, e: - logger.error(u'Could not process %s\n%s' % (f, str(e))) + content, metadata = read_file(f, settings=self.settings) + 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) + source_path=f, context=self.context) if not is_valid_content(page, f): continue + + 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) @@ -395,7 +478,7 @@ class PagesGenerator(Generator): 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'), + writer.write_file(page.save_as, self.get_template(page.template), self.context, page=page, relative_urls=self.settings.get('RELATIVE_URLS')) @@ -412,53 +495,61 @@ class StaticGenerator(Generator): final_path, overwrite=True) def generate_context(self): + self.staticfiles = [] - if self.settings['WEBASSETS']: - from webassets import Environment as AssetsEnvironment - - # Define the assets environment that will be passed to the - # generators. The StaticGenerator must then be run first to have - # the assets in the output_path before generating the templates. - assets_url = self.settings['SITEURL'] + '/theme/' - assets_src = os.path.join(self.output_path, 'theme') - self.assets_env = AssetsEnvironment(assets_src, assets_url) - - if logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG": - self.assets_env.debug = True + # walk static paths + for static_path in self.settings['STATIC_PATHS']: + for f in self.get_files( + os.path.join(self.path, static_path), extensions=False): + f_rel = os.path.relpath(f, self.path) + # TODO remove this hardcoded 'static' subdirectory + sc = StaticContent(f_rel, os.path.join('static', f_rel), + settings=self.settings) + self.staticfiles.append(sc) + self.context['filenames'][f_rel] = sc + # same thing for FILES_TO_COPY + for src, dest in self.settings['FILES_TO_COPY']: + sc = StaticContent(src, dest, settings=self.settings) + self.staticfiles.append(sc) + self.context['filenames'][src] = sc def generate_output(self, writer): - - self._copy_paths(self.settings['STATIC_PATHS'], self.path, - 'static', self.output_path) self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme, 'theme', self.output_path, '.') - - # copy all the files needed - for source, destination in self.settings['FILES_TO_COPY']: - copy(source, self.path, self.output_path, destination, - overwrite=True) + # copy all StaticContent files + for sc in self.staticfiles: + mkdir_p(os.path.dirname(sc.save_as)) + shutil.copy(sc.source_path, sc.save_as) + logger.info('copying {} to {}'.format(sc.source_path, sc.save_as)) class PdfGenerator(Generator): """Generate PDFs on the output dir, for all articles and pages coming from rst""" def __init__(self, *args, **kwargs): + 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' self.pdfcreator = RstToPdf(breakside=0, - stylesheets=['twelvepoint']) + stylesheets=[pdf_style], + style_path=[pdf_style_path]) except ImportError: raise Exception("unable to find rst2pdf") - super(PdfGenerator, self).__init__(*args, **kwargs) 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: - self.pdfcreator.createPdf(text=f, output=output_pdf) - logger.info(u' [ok] writing %s' % output_pdf) + # 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(' [ok] writing %s' % output_pdf) def generate_context(self): pass @@ -466,14 +557,14 @@ 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: os.mkdir(pdf_path) except OSError: - logger.error("Couldn't create the pdf output folder in " + pdf_path) - pass + logger.error("Couldn't create the pdf output folder in " + + pdf_path) for article in self.context['articles']: self._create_pdf(article, pdf_path) @@ -481,49 +572,16 @@ 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'] -class LessCSSGenerator(Generator): - """Compile less css files.""" - - def _compile(self, less_file, source_dir, dest_dir): - base = os.path.relpath(less_file, source_dir) - target = os.path.splitext( - os.path.join(dest_dir, base))[0] + '.css' - target_dir = os.path.dirname(target) - - if not os.path.exists(target_dir): - try: - os.makedirs(target_dir) - except OSError: - logger.error("Couldn't create the less css output folder in " + - target_dir) - - subprocess.call([self._lessc, less_file, target]) - logger.info(u' [ok] compiled %s' % base) + def _create_source(self, obj, output_path): + output_path = os.path.splitext(obj.save_as)[0] + dest = os.path.join(output_path, output_path + self.output_extension) + copy('', obj.source_path, dest) def generate_output(self, writer=None): - logger.info(u' Compiling less css') - - # store out compiler here, so it won't be evaulted on each run of - # _compile - lg = self.settings['LESS_GENERATOR'] - self._lessc = lg if isinstance(lg, basestring) else 'lessc' - - # walk static paths - for static_path in self.settings['STATIC_PATHS']: - for f in self.get_files( - os.path.join(self.path, static_path), - extensions=['less']): - - self._compile(f, self.path, self.output_path) - - # walk theme static paths - theme_output_path = os.path.join(self.output_path, 'theme') - - for static_path in self.settings['THEME_STATIC_PATHS']: - theme_static_path = os.path.join(self.theme, static_path) - for f in self.get_files( - theme_static_path, - extensions=['less']): - - self._compile(f, theme_static_path, theme_output_path) + logger.info(' Generating source files...') + for object in chain(self.context['articles'], self.context['pages']): + self._create_source(object, self.output_path) diff --git a/pelican/log.py b/pelican/log.py index 9590d7f6..2ce11ea2 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,37 +27,38 @@ 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. + Convert a `logging.LogRecord' object into colored text, using ANSI escape sequences. """ ## colors: 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 = str(record.msg) + 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': + if not record.levelname or record.levelname == 'INFO': return record.msg else: return record.levelname + ': ' + record.msg 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/assets.py b/pelican/plugins/assets.py new file mode 100644 index 00000000..5708224c --- /dev/null +++ b/pelican/plugins/assets.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +""" +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) + + if 'ASSET_CONFIG' in generator.settings: + for item in generator.settings['ASSET_CONFIG']: + generator.env.assets_environment.config[item[0]] = item[1] + + 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 index f2ba1da7..cc139efc 100644 --- a/pelican/plugins/github_activity.py +++ b/pelican/plugins/github_activity.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function """ Copyright (c) Marco Milanesi diff --git a/pelican/plugins/global_license.py b/pelican/plugins/global_license.py index 463a93b3..9a0f5206 100644 --- a/pelican/plugins/global_license.py +++ b/pelican/plugins/global_license.py @@ -4,13 +4,14 @@ from pelican import signals License plugin for Pelican ========================== -Simply add license variable in article's context, which contain -the license text. +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: --------- -Add LICENSE to your settings file to define default license. +Define LICENSE in your settings file with the contents of your default license. """ diff --git a/pelican/plugins/gravatar.py b/pelican/plugins/gravatar.py index 4ab8ea9c..a4d11456 100644 --- a/pelican/plugins/gravatar.py +++ b/pelican/plugins/gravatar.py @@ -5,20 +5,22 @@ from pelican import signals Gravatar plugin for Pelican =========================== -Simply add author_gravatar variable in article's context, which contains -the gravatar url. +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 default author email. +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 +If one of them are defined, the author_gravatar variable is added to the article's context. """ diff --git a/pelican/plugins/gzip_cache.py b/pelican/plugins/gzip_cache.py new file mode 100644 index 00000000..9080629e --- /dev/null +++ b/pelican/plugins/gzip_cache.py @@ -0,0 +1,78 @@ +# 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 as 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 index d0a656f5..2b2bceb3 100644 --- a/pelican/plugins/html_rst_directive.py +++ b/pelican/plugins/html_rst_directive.py @@ -1,6 +1,7 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals from docutils import nodes from docutils.parsers.rst import directives, Directive -from pelican import log """ HTML tags for reStructuredText @@ -52,7 +53,7 @@ class RawHtml(Directive): has_content = True def run(self): - html = u' '.join(self.content) + html = ' '.join(self.content) node = nodes.raw('', html, format='html') return [node] diff --git a/pelican/plugins/initialized.py b/pelican/plugins/initialized.py index 5e4cf174..b4b5ab76 100644 --- a/pelican/plugins/initialized.py +++ b/pelican/plugins/initialized.py @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function from pelican import signals def test(sender): - print "%s initialized !!" % 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 new file mode 100644 index 00000000..0581b501 --- /dev/null +++ b/pelican/plugins/multi_part.py @@ -0,0 +1,59 @@ +# -*- 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 %} +
  1. + {{ part_article.title }} + +
  2. + {% else %} +
  3. + {{ part_article.title }} + +
  4. + {% 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 new file mode 100644 index 00000000..c2765c3c --- /dev/null +++ b/pelican/plugins/related_posts.py @@ -0,0 +1,52 @@ +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(list(zip(set(related_posts), list(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 new file mode 100644 index 00000000..8043baad --- /dev/null +++ b/pelican/plugins/sitemap.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +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 = """{0}/index.html +{0}/archives.html +{0}/tags.html +{0}/categories.html +""" + +XML_HEADER = """ + +""" + +XML_URL = """ + +{0}/{1} +{2} +{3} +{4} + +""" + +XML_FOOTER = """ + +""" + + +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): + # We use items for Py3k compat. .iteritems() otherwise + for k, v in pris.items(): + 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): + # .items() for py3k compat. + for k, v in chfreqs.items(): + 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 08ef4cf8..6fe8e894 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -1,4 +1,9 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import six + +import os +import re try: import docutils import docutils.core @@ -13,19 +18,24 @@ try: from markdown import Markdown except ImportError: Markdown = False # NOQA +try: + from asciidocapi import AsciiDocAPI + asciidoc = True +except ImportError: + asciidoc = False import re import cgi from HTMLParser import HTMLParser from pelican.contents import Category, Tag, Author -from pelican.utils import get_date, open +from pelican.utils import get_date, pelican_open _METADATA_PROCESSORS = { - 'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')], + '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, } @@ -66,6 +76,18 @@ def render_node_to_html(document, node): return visitor.astext() +class PelicanHTMLTranslator(HTMLTranslator): + + def visit_abbreviation(self, node): + attrs = {} + if node.hasattr('explanation'): + attrs['title'] = node['explanation'] + self.body.append(self.starttag(node, 'abbr', '', **attrs)) + + def depart_abbreviation(self, node): + self.body.append('') + + class RstReader(Reader): enabled = bool(docutils) file_extensions = ['rst'] @@ -90,19 +112,20 @@ class RstReader(Reader): output[name] = self.process_metadata(name, value) return output - def _get_publisher(self, filename): + def _get_publisher(self, source_path): extra_params = {'initial_header_level': '2'} pub = docutils.core.Publisher( - destination_class=docutils.io.StringOutput) + 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') @@ -117,16 +140,28 @@ class MarkdownReader(Reader): file_extensions = ['md', 'markdown', 'mkd'] extensions = ['codehilite', 'extra'] - def read(self, filename): + 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) + output[name] = self.process_metadata(name, summary) + else: + output[name] = self.process_metadata(name, value[0]) + return output + + def read(self, source_path): """Parse content and metadata of markdown files""" - with open(filename) as text: + + with pelican_open(source_path) as text: md = Markdown(extensions=set(self.extensions + ['meta'])) content = md.convert(text) - metadata = {} - for name, value in md.Meta.items(): - name = name.lower() - metadata[name] = self.process_metadata(name, value[0]) + metadata = self._parse_metadata(md.Meta) return content, metadata class HTMLReader(Reader): @@ -223,7 +258,7 @@ class HTMLReader(Reader): def read(self, filename): """Parse content and metadata of markdown files""" - with open(filename) as content: + with pelican_open(filename) as content: parser = self._HTMLParser(self.settings) parser.feed(content) parser.close() @@ -233,6 +268,37 @@ class HTMLReader(Reader): metadata[k] = self.process_metadata(k, parser.metadata[k]) return parser.body, metadata +class AsciiDocReader(Reader): + enabled = bool(asciidoc) + file_extensions = ['asc'] + default_options = ["--no-header-footer", "-a newline=\\n"] + + def read(self, source_path): + """Parse content and metadata of asciidoc files""" + from cStringIO import StringIO + text = StringIO(pelican_open(source_path)) + content = StringIO() + ad = AsciiDocAPI() + + options = self.settings.get('ASCIIDOC_OPTIONS', []) + if isinstance(options, (str, unicode)): + options = [m.strip() for m in options.split(',')] + options = self.default_options + options + for o in options: + ad.options(*o.split()) + + ad.execute(text, content, backend="html4") + content = content.getvalue() + + metadata = {} + for name, value in ad.asciidoc.document.attributes.items(): + name = name.lower() + metadata[name] = self.process_metadata(name, value) + if 'doctitle' in metadata: + metadata['title'] = metadata['doctitle'] + return content, metadata + + _EXTENSIONS = {} for cls in Reader.__subclasses__(): @@ -240,13 +306,14 @@ for cls in Reader.__subclasses__(): _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(path)) if not fmt: - fmt = filename.split('.')[-1] + fmt = ext[1:] if fmt not in _EXTENSIONS: - raise TypeError('Pelican does not know how to parse %s' % filename) + raise TypeError('Pelican does not know how to parse {}'.format(path)) reader = _EXTENSIONS[fmt](settings) settings_key = '%s_EXTENSIONS' % fmt.upper() @@ -257,12 +324,22 @@ def read_file(filename, fmt=None, settings=None): if not reader.enabled: raise ValueError("Missing dependencies for %s" % fmt) - content, metadata = reader.read(filename) + content, metadata = reader.read(path) # eventually filter the content with typogrify if asked so - if settings and settings['TYPOGRIFY']: - from typogrify import Typogrify - content = Typogrify.typogrify(content) - metadata['title'] = Typogrify.typogrify(metadata['title']) + if settings and settings.get('TYPOGRIFY'): + from typogrify.filters import typogrify + content = typogrify(content) + metadata['title'] = typogrify(metadata['title']) + + file_metadata = settings and settings.get('FILENAME_METADATA') + if file_metadata: + match = re.match(file_metadata, base) + 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 + metadata[k] = reader.process_metadata(k, v) return content, metadata diff --git a/pelican/rstdirectives.py b/pelican/rstdirectives.py index 9c821310..fb4a6c93 100644 --- a/pelican/rstdirectives.py +++ b/pelican/rstdirectives.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- -from docutils import nodes -from docutils.parsers.rst import directives, Directive +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 from pygments import highlight from pygments.lexers import get_lexer_by_name, TextLexer +import re INLINESTYLES = False DEFAULT = HtmlFormatter(noclasses=INLINESTYLES) @@ -31,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) @@ -94,3 +97,21 @@ class YouTube(Directive): nodes.raw('', '', format='html')] directives.register_directive('youtube', YouTube) + +_abbr_re = re.compile('\((.*)\)$') + + +class abbreviation(nodes.Inline, nodes.TextElement): + pass + + +def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): + text = utils.unescape(text) + m = _abbr_re.search(text) + if m is None: + return [abbreviation(text, text)], [] + abbr = text[:m.start()].strip() + expl = m.group(1) + return [abbreviation(abbr, abbr, explanation=expl)], [] + +roles.register_local_role('abbr', abbr_role) diff --git a/pelican/server.py b/pelican/server.py new file mode 100644 index 00000000..0f5a71d2 --- /dev/null +++ b/pelican/server.py @@ -0,0 +1,20 @@ +from __future__ import print_function +try: + import SimpleHTTPServer as srvmod +except ImportError: + import http.server as srvmod + +try: + import SocketServer as socketserver +except ImportError: + import socketserver + +PORT = 8000 + +Handler = srvmod.SimpleHTTPRequestHandler + +httpd = socketserver.TCPServer(("", PORT), Handler) + +print("serving at port", PORT) +httpd.serve_forever() + diff --git a/pelican/settings.py b/pelican/settings.py index 17efea58..8d6c1608 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import six + +import copy +import imp +import inspect import os import locale import logging @@ -21,18 +27,21 @@ _DEFAULT_CONFIG = {'PATH': '.', 'MARKUP': ('rst', 'md'), 'STATIC_PATHS': ['images', ], 'THEME_STATIC_PATHS': ['static', ], - 'FEED': 'feeds/all.atom.xml', - 'CATEGORY_FEED': 'feeds/%s.atom.xml', - 'TRANSLATION_FEED': 'feeds/all-%s.atom.xml', + 'FEED_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', - 'FALLBACK_ON_FS_DATE': True, 'WITH_FUTURE_DATES': True, 'CSS_FILE': 'main.css', - 'REVERSE_ARCHIVE_ORDER': False, + 'NEWEST_FIRST_ARCHIVES': True, 'REVERSE_CATEGORY_ORDER': False, 'DELETE_OUTPUT_DIRECTORY': False, 'ARTICLE_URL': '{slug}.html', @@ -47,13 +56,14 @@ _DEFAULT_CONFIG = {'PATH': '.', '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', + 'AUTHOR_URL': 'author/{slug}.html', + 'AUTHOR_SAVE_AS': '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', @@ -63,59 +73,83 @@ _DEFAULT_CONFIG = {'PATH': '.', '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, - 'LESS_GENERATOR': False, 'SUMMARY_MAX_LENGTH': 50, - 'WEBASSETS': False, 'PLUGINS': [], + 'TEMPLATE_PAGES': {}, + 'IGNORE_FILES': [] } -def read_settings(filename=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']: + 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(path), local_settings[p]))) + if p != 'THEME' or os.path.exists(absp): + local_settings[p] = absp else: - local_settings = _DEFAULT_CONFIG - configured_settings = configure_settings(local_settings, None, filename) - return configured_settings + local_settings = copy.deepcopy(_DEFAULT_CONFIG) + + if override: + local_settings.update(override) + + return configure_settings(local_settings) -def get_settings_from_file(filename, default_settings=None): - """Load a Python file into a dictionary. +def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG): """ - if default_settings == None: - default_settings = _DEFAULT_CONFIG - context = default_settings.copy() - if filename: - tempdict = {} - execfile(filename, tempdict) - for key in tempdict: - if key.isupper(): - context[key] = tempdict[key] + Load settings from a module, returning a dict. + """ + + context = copy.deepcopy(default_settings) + if module is not None: + context.update( + (k, v) for k, v in inspect.getmembers(module) if k.isupper()) return context -def configure_settings(settings, default_settings=None, filename=None): - """Provide optimizations, error checking, and warnings for loaded settings""" - if default_settings is None: - default_settings = _DEFAULT_CONFIG +def get_settings_from_file(path, default_settings=_DEFAULT_CONFIG): + """ + Load settings from a file path, returning a dict. - # Make the paths relative to the settings file - if filename: - for path in ['PATH', 'OUTPUT_PATH']: - if path in settings: - if settings[path] is not None and not isabs(settings[path]): - settings[path] = os.path.abspath(os.path.normpath( - os.path.join(os.path.dirname(filename), settings[path])) - ) + """ + + name = os.path.basename(path).rpartition('.')[0] + module = imp.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 + """ + 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 + if not os.path.isdir(settings['THEME']): + theme_path = os.sep.join([os.path.dirname( + os.path.abspath(__file__)), "themes/%s" % settings['THEME']]) + if os.path.exists(theme_path): + settings['THEME'] = theme_path + else: + raise Exception("Impossible to find the theme %s" + % settings['THEME']) # if locales is not a list, make it one locales = settings['LOCALE'] - if isinstance(locales, basestring): + if isinstance(locales, six.string_types): locales = [locales] # try to set the different locales, fallback on the default. @@ -124,8 +158,8 @@ def configure_settings(settings, default_settings=None, filename=None): for locale_ in locales: try: - locale.setlocale(locale.LC_ALL, locale_) - break # break if it is successfull + locale.setlocale(locale.LC_ALL, str(locale_)) + break # break if it is successful except locale.Error: pass else: @@ -142,10 +176,21 @@ def configure_settings(settings, default_settings=None, filename=None): settings['FEED_DOMAIN'] = settings['SITEURL'] # Warn if feeds are generated with both SITEURL & FEED_DOMAIN undefined - if (('FEED' in settings) or ('FEED_RSS' in settings)) and (not 'FEED_DOMAIN' in settings): - 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')") + feed_keys = ['FEED_ATOM', 'FEED_RSS', + 'FEED_ALL_ATOM', 'FEED_ALL_RSS', + 'CATEGORY_FEED_ATOM', 'CATEGORY_FEED_RSS', + 'TAG_FEED_ATOM', 'TAG_FEED_RSS', + 'TRANSLATION_FEED_ATOM', 'TRANSLATION_FEED_RSS', + ] + + 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") if not 'TIMEZONE' in settings: logger.warn("No timezone information specified in the settings. Assuming" @@ -153,12 +198,23 @@ def configure_settings(settings, default_settings=None, filename=None): "http://docs.notmyidea.org/alexis/pelican/settings.html#timezone " "for more information") - if 'WEBASSETS' in settings and settings['WEBASSETS'] is not False: - try: - from webassets.ext.jinja2 import AssetsExtension - settings['JINJA_EXTENSIONS'].append(AssetsExtension) - except ImportError: - logger.warn("You must install the webassets module to use WEBASSETS.") - settings['WEBASSETS'] = False + if 'LESS_GENERATOR' in settings: + logger.warn("The LESS_GENERATOR setting has been removed in favor " + "of the Webassets plugin") + + if 'OUTPUT_SOURCES_EXTENSION' in settings: + if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], six.string_types): + 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, six.string_types): + 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'] return settings diff --git a/pelican/signals.py b/pelican/signals.py index b1c35794..1367a117 100644 --- a/pelican/signals.py +++ b/pelican/signals.py @@ -1,5 +1,15 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function from blinker import signal initialized = signal('pelican_initialized') +finalized = signal('pelican_finalized') +article_generate_preread = signal('article_generate_preread') +generator_init = signal('generator_init') article_generate_context = signal('article_generate_context') article_generator_init = signal('article_generator_init') +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') +content_object_init = signal('content_object_init') diff --git a/pelican/themes/notmyidea/static/css/main.css b/pelican/themes/notmyidea/static/css/main.css index dce9e247..29cce82c 100644 --- a/pelican/themes/notmyidea/static/css/main.css +++ b/pelican/themes/notmyidea/static/css/main.css @@ -70,9 +70,6 @@ p {margin-bottom: 1.143em;} strong, b {font-weight: bold;} em, i {font-style: italic;} -::-moz-selection {background: #F6CF74; color: #fff;} -::selection {background: #F6CF74; color: #fff;} - /* Lists */ ul { list-style: outside disc; @@ -100,7 +97,7 @@ dl {margin: 0 0 1.5em 0;} dt {font-weight: bold;} dd {margin-left: 1.5em;} -pre{background-color: #000; padding: 10px; color: #fff; margin: 10px; overflow: auto;} +pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;} /* Quotes */ blockquote { @@ -144,8 +141,8 @@ aside, nav, article, figure { /***** Layout *****/ .body {clear: both; margin: 0 auto; width: 800px;} -img.right figure.right {float: right; margin: 0 0 2em 2em;} -img.left, figure.left {float: right; margin: 0 0 2em 2em;} +img.right, figure.right {float: right; margin: 0 0 2em 2em;} +img.left, figure.left {float: left; margin: 0 2em 2em 0;} /* Header @@ -163,7 +160,6 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} font-weight: bold; margin: 0 0 .6em .2em; text-decoration: none; - width: 427px; } #banner h1 a:hover, #banner h1 a:active { background: none; @@ -312,7 +308,11 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');} .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');} .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');} - .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.org');} + .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');} + .social a[href*='github.com'], + .social a[href*='git.io'] {background-image: url('../images/icons/github.png');} + .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');} + .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');} /* About diff --git a/pelican/themes/notmyidea/static/css/pygment.css b/pelican/themes/notmyidea/static/css/pygment.css index 594b0fa3..fdd056f6 100644 --- a/pelican/themes/notmyidea/static/css/pygment.css +++ b/pelican/themes/notmyidea/static/css/pygment.css @@ -1,5 +1,5 @@ .hll { -background-color:#FFFFCC; +background-color:#eee; } .c { color:#408090; diff --git a/pelican/themes/notmyidea/static/images/icons/delicious.png b/pelican/themes/notmyidea/static/images/icons/delicious.png index c6ce246a..3dccdd84 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/delicious.png and b/pelican/themes/notmyidea/static/images/icons/delicious.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/facebook.png b/pelican/themes/notmyidea/static/images/icons/facebook.png index a7914b49..74e7ad52 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/facebook.png and b/pelican/themes/notmyidea/static/images/icons/facebook.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/github.png b/pelican/themes/notmyidea/static/images/icons/github.png new file mode 100644 index 00000000..6c52b486 Binary files /dev/null and b/pelican/themes/notmyidea/static/images/icons/github.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/gitorious.png b/pelican/themes/notmyidea/static/images/icons/gitorious.png index 6485f5ec..3eeb3ece 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/gitorious.png and b/pelican/themes/notmyidea/static/images/icons/gitorious.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/gittip.png b/pelican/themes/notmyidea/static/images/icons/gittip.png new file mode 100644 index 00000000..af949625 Binary files /dev/null and b/pelican/themes/notmyidea/static/images/icons/gittip.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/google-plus.png b/pelican/themes/notmyidea/static/images/icons/google-plus.png new file mode 100644 index 00000000..3c6b7432 Binary files /dev/null and b/pelican/themes/notmyidea/static/images/icons/google-plus.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/lastfm.png b/pelican/themes/notmyidea/static/images/icons/lastfm.png index b09c7876..3a6c6262 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/lastfm.png and b/pelican/themes/notmyidea/static/images/icons/lastfm.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/linkedin.png b/pelican/themes/notmyidea/static/images/icons/linkedin.png index feb04962..d29c1201 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/linkedin.png and b/pelican/themes/notmyidea/static/images/icons/linkedin.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/rss.png b/pelican/themes/notmyidea/static/images/icons/rss.png index 7d4e85d9..7862c65a 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/rss.png and b/pelican/themes/notmyidea/static/images/icons/rss.png differ diff --git a/pelican/themes/notmyidea/static/images/icons/twitter.png b/pelican/themes/notmyidea/static/images/icons/twitter.png index d6119280..d0ef3cc1 100644 Binary files a/pelican/themes/notmyidea/static/images/icons/twitter.png and b/pelican/themes/notmyidea/static/images/icons/twitter.png differ diff --git a/pelican/themes/notmyidea/templates/analytics.html b/pelican/themes/notmyidea/templates/analytics.html index ba174fcc..4de2c86b 100644 --- a/pelican/themes/notmyidea/templates/analytics.html +++ b/pelican/themes/notmyidea/templates/analytics.html @@ -1,11 +1,12 @@ {% if GOOGLE_ANALYTICS %} - {% endif %} \ No newline at end of file diff --git a/pelican/themes/notmyidea/templates/article.html b/pelican/themes/notmyidea/templates/article.html index fc7e5893..737d3789 100644 --- a/pelican/themes/notmyidea/templates/article.html +++ b/pelican/themes/notmyidea/templates/article.html @@ -5,7 +5,7 @@

- {{ article.title}}

{% include 'twitter.html' %}
diff --git a/pelican/themes/notmyidea/templates/article_infos.html b/pelican/themes/notmyidea/templates/article_infos.html index a1993a09..4b86716d 100644 --- a/pelican/themes/notmyidea/templates/article_infos.html +++ b/pelican/themes/notmyidea/templates/article_infos.html @@ -10,5 +10,6 @@ {% endif %}

In {{ article.category }}. {% if PDF_PROCESSOR %}get the pdf{% endif %}

{% include 'taglist.html' %} -{% include 'translations.html' %} +{% import 'translations.html' as translations with context %} +{{ translations.translations_for(article) }} diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html index c9f2c0c8..f8b02662 100644 --- a/pelican/themes/notmyidea/templates/base.html +++ b/pelican/themes/notmyidea/templates/base.html @@ -4,9 +4,11 @@ {% block title %}{{ SITENAME }}{%endblock%} - - {% if FEED_RSS %} - + {% if FEED_ALL_ATOM %} + + {% endif %} + {% if FEED_ALL_RSS %} + {% endif %}

The theme is by Smashing Magazine, thanks!

diff --git a/pelican/themes/notmyidea/templates/page.html b/pelican/themes/notmyidea/templates/page.html index 9635fb8d..60409d5c 100644 --- a/pelican/themes/notmyidea/templates/page.html +++ b/pelican/themes/notmyidea/templates/page.html @@ -3,6 +3,8 @@ {% block content %}

{{ page.title }}

+ {% import 'translations.html' as translations with context %} + {{ translations.translations_for(page) }} {% if PDF_PROCESSOR %}get the pdf{% endif %} {{ page.content }} diff --git a/pelican/themes/notmyidea/templates/translations.html b/pelican/themes/notmyidea/templates/translations.html index 0079883e..ca03a2c9 100644 --- a/pelican/themes/notmyidea/templates/translations.html +++ b/pelican/themes/notmyidea/templates/translations.html @@ -1,6 +1,8 @@ +{% macro translations_for(article) %} {% if article.translations %} Translations: {% for translation in article.translations %} {{ translation.lang }} {% endfor %} {% endif %} +{% endmacro %} diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index 16c34266..86b14f62 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -5,6 +5,8 @@

{{ article.title }}

+ {% import 'translations.html' as translations with context %} + {{ translations.translations_for(article) }}
diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index ad2723fd..3ea2e8b9 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -4,6 +4,30 @@ {% block head %} {% block title %}{{ SITENAME }}{% endblock title %} + {% if FEED_ALL_ATOM %} + + {% endif %} + {% if FEED_ALL_RSS %} + + {% endif %} + {% if FEED_ATOM %} + + {% endif %} + {% if FEED_RSS %} + + {% endif %} + {% if CATEGORY_FEED_ATOM %} + + {% endif %} + {% if CATEGORY_FEED_RSS %} + + {% endif %} + {% if TAG_FEED_ATOM %} + + {% endif %} + {% if TAG_FEED_RSS %} + + {% endif %} {% endblock head %} @@ -29,7 +53,7 @@ {% endblock %}
- Proudly powered by Pelican, + Proudly powered by Pelican, which takes great advantage of Python.
diff --git a/pelican/themes/simple/templates/index.html b/pelican/themes/simple/templates/index.html index dfdb0b45..5bb94dfc 100644 --- a/pelican/themes/simple/templates/index.html +++ b/pelican/themes/simple/templates/index.html @@ -1,14 +1,14 @@ {% extends "base.html" %} -{% block content %} +{% block content %}
{% block content_title %}

All articles

{% endblock %}
    -{% for article in articles_page.object_list %} -
  1. -

    {{ article.title }}

    +{% for article in articles_page.object_list %} +
  2. +

    {{ article.title }}

    {{ article.locale_date }} {% if article.author %}
    By {{ article.author }}
    {% endif %} diff --git a/pelican/themes/simple/templates/page.html b/pelican/themes/simple/templates/page.html index 6308d588..3a0dc4a9 100644 --- a/pelican/themes/simple/templates/page.html +++ b/pelican/themes/simple/templates/page.html @@ -2,5 +2,8 @@ {% block title %}{{ page.title }}{%endblock%} {% block content %}

    {{ page.title }}

    + {% import 'translations.html' as translations with context %} + {{ translations.translations_for(page) }} + {{ page.content }} {% endblock %} diff --git a/pelican/themes/simple/templates/translations.html b/pelican/themes/simple/templates/translations.html new file mode 100644 index 00000000..db8c372d --- /dev/null +++ b/pelican/themes/simple/templates/translations.html @@ -0,0 +1,9 @@ +{% macro translations_for(article) %} +{% if article.translations %} +Translations: +{% for translation in article.translations %} +{{ translation.lang }} +{% endfor %} +{% endif %} +{% endmacro %} + diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index dab3c3a8..33041b0e 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -1,6 +1,14 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function import argparse +try: + # py3k import + from html.parser import HTMLParser +except ImportError: + # py2 import + from HTMLParser import HTMLParser # NOQA import os import subprocess import sys @@ -14,14 +22,14 @@ from pelican.utils import slugify def wp2fields(xml): """Opens a wordpress XML file, and yield pelican fields""" try: - from BeautifulSoup import BeautifulStoneSoup + from bs4 import BeautifulSoup except ImportError: error = ('Missing dependency ' - '"BeautifulSoup" required to import Wordpress XML files.') + '"BeautifulSoup4" and "lxml" required to import Wordpress XML files.') sys.exit(error) xmlfile = open(xml, encoding='utf-8').read() - soup = BeautifulStoneSoup(xmlfile) + soup = BeautifulSoup(xmlfile, "xml") items = soup.rss.channel.findAll('item') for item in items: @@ -29,7 +37,8 @@ def wp2fields(xml): if item.fetch('wp:status')[0].contents[0] == "publish": try: - title = item.title.contents[0] + # Use HTMLParser due to issues with BeautifulSoup 3 + title = HTMLParser().unescape(item.title.contents[0]) except IndexError: continue @@ -52,10 +61,10 @@ def wp2fields(xml): def dc2fields(file): """Opens a Dotclear export file, and yield pelican fields""" try: - from BeautifulSoup import BeautifulStoneSoup + from bs4 import BeautifulSoup except ImportError: error = ('Missing dependency ' - '"BeautifulSoup" required to import Dotclear files.') + '"BeautifulSoup4" and "lxml" required to import Dotclear files.') sys.exit(error) @@ -140,13 +149,27 @@ def dc2fields(file): if len(tag) > 1: if int(tag[:1]) == 1: newtag = tag.split('"')[1] - tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES ))) + tags.append( + BeautifulSoup( + newtag + , "xml" + ) + # bs4 always outputs UTF-8 + .decode('utf-8') + ) else: i=1 j=1 while(i <= int(tag[:1])): newtag = tag.split('"')[j].replace('\\','') - tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES ))) + tags.append( + BeautifulSoup( + newtag + , "xml" + ) + # bs4 always outputs UTF-8 + .decode('utf-8') + ) i=i+1 if j < int(tag[:1])*2: j=j+2 @@ -179,44 +202,53 @@ def feed2fields(file): yield (entry.title, entry.description, slug, date, author, [], tags, "html") -def build_header(title, date, author, categories, tags): +def build_header(title, date, author, categories, tags, slug): """Build a header from a list of fields""" header = '%s\n%s\n' % (title, '#' * len(title)) if date: header += ':date: %s\n' % date + if author: + header += ':author: %s\n' % author if categories: header += ':category: %s\n' % ', '.join(categories) if tags: header += ':tags: %s\n' % ', '.join(tags) + if slug: + header += ':slug: %s\n' % slug header += '\n' return header -def build_markdown_header(title, date, author, categories, tags): +def build_markdown_header(title, date, author, categories, tags, slug): """Build a header from a list of fields""" header = 'Title: %s\n' % title if date: header += 'Date: %s\n' % date + if author: + header += 'Author: %s\n' % author if categories: header += 'Category: %s\n' % ', '.join(categories) if tags: header += 'Tags: %s\n' % ', '.join(tags) + if slug: + header += 'Slug: %s\n' % slug header += '\n' return header -def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=False): +def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=False, disable_slugs=False): for title, content, filename, date, author, categories, tags, in_markup in fields: + slug = not disable_slugs and filename or None if (in_markup == "markdown") or (out_markup == "markdown") : ext = '.md' - header = build_markdown_header(title, date, author, categories, tags) + header = build_markdown_header(title, date, author, categories, tags, slug) else: out_markup = "rst" ext = '.rst' - header = build_header(title, date, author, categories, tags) + header = build_header(title, date, author, categories, tags, slug) filename = os.path.basename(filename) # option to put files in directories with categories names - if dircat and (len(categories) == 1): + if dircat and (len(categories) > 0): catname = slugify(categories[0]) out_filename = os.path.join(output_path, catname, filename+ext) if not os.path.isdir(os.path.join(output_path, catname)): @@ -232,8 +264,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals with open(html_filename, 'w', encoding='utf-8') as fp: # Replace newlines with paragraphs wrapped with

    so # HTML is valid before conversion - paragraphs = content.split('\n\n') - paragraphs = [u'

    {}

    '.format(p) for p in paragraphs] + paragraphs = content.splitlines() + paragraphs = ['

    {0}

    '.format(p) for p in paragraphs] new_content = ''.join(paragraphs) fp.write(new_content) @@ -253,7 +285,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals elif rc > 0: error = "Please, check your Pandoc installation." exit(error) - except OSError, e: + except OSError as e: error = "Pandoc execution failed: %s" % e exit(error) @@ -272,8 +304,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals def main(): parser = argparse.ArgumentParser( - description="Transform feed, Wordpress or Dotclear files to rst files." - "Be sure to have pandoc installed", + description="Transform feed, Wordpress or Dotclear files to reST (rst) " + "or Markdown (md) files. Be sure to have pandoc installed.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument(dest='input', help='The input file to read') @@ -292,6 +324,11 @@ def main(): parser.add_argument('--strip-raw', action='store_true', dest='strip_raw', help="Strip raw HTML code that can't be converted to " "markup such as flash embeds or iframes (wordpress import only)") + parser.add_argument('--disable-slugs', action='store_true', + dest='disable_slugs', + help='Disable storing slugs from imported posts within output. ' + 'With this disabled, your Pelican URLs may not be consistent ' + 'with your original posts.') args = parser.parse_args() @@ -322,4 +359,5 @@ def main(): fields2pelican(fields, args.markup, args.output, dircat=args.dircat or False, - strip_raw=args.strip_raw or False) + strip_raw=args.strip_raw or False, + disable_slugs=args.disable_slugs or False) diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py index f747d048..9f25f2fe 100755 --- a/pelican/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -1,19 +1,23 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import six import os import string import argparse +import sys +import codecs from pelican import __version__ -_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \ +_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") - CONF = { - 'pelican' : 'pelican', - 'pelicanopts' : '', + 'pelican': 'pelican', + 'pelicanopts': '', 'basedir': '.', 'ftp_host': 'localhost', 'ftp_user': 'anonymous', @@ -22,20 +26,41 @@ CONF = { 'ssh_port': 22, 'ssh_user': 'root', 'ssh_target_dir': '/var/www', - 'dropbox_dir' : '~/Dropbox/Public/', - 'default_pagination' : 10, + 'dropbox_dir': '~/Dropbox/Public/', + 'default_pagination': 10, 'siteurl': '', 'lang': 'en' } +def _input_compat(prompt): + if six.PY3: + r = input(prompt) + else: + r = raw_input(prompt).decode('utf-8') + return r -def get_template(name): +if six.PY3: + str_compat = str +else: + str_compat = unicode + +def decoding_strings(f): + def wrapper(*args, **kwargs): + out = f(*args, **kwargs) + if isinstance(out, six.string_types): + # todo: make encoding configurable? + return out.decode(sys.stdin.encoding) + return out + return wrapper + + +def get_template(name, as_encoding='utf-8'): template = os.path.join(_TEMPLATES_DIR, "{0}.in".format(name)) if not os.path.isfile(template): raise RuntimeError("Cannot open {0}".format(template)) - with open(template, 'r') as fd: + with codecs.open(template, 'r', as_encoding) as fd: line = fd.readline() while line: yield line @@ -43,14 +68,15 @@ def get_template(name): fd.close() -def ask(question, answer=str, default=None, l=None): - if answer == str: +@decoding_strings +def ask(question, answer=str_compat, default=None, l=None): + if answer == str_compat: r = '' while True: if default: - r = raw_input('> {0} [{1}] '.format(question, default)) + r = _input_compat('> {0} [{1}] '.format(question, default)) else: - r = raw_input('> {0} '.format(question, default)) + r = _input_compat('> {0} '.format(question, default)) r = r.strip() @@ -64,7 +90,7 @@ def ask(question, answer=str, default=None, l=None): if l and len(r) != l: print('You must enter a {0} letters long string'.format(l)) else: - break + break return r @@ -72,11 +98,11 @@ def ask(question, answer=str, default=None, l=None): r = None while True: if default is True: - r = raw_input('> {0} (Y/n) '.format(question)) + r = _input_compat('> {0} (Y/n) '.format(question)) elif default is False: - r = raw_input('> {0} (y/N) '.format(question)) + r = _input_compat('> {0} (y/N) '.format(question)) else: - r = raw_input('> {0} (y/n) '.format(question)) + r = _input_compat('> {0} (y/n) '.format(question)) r = r.strip().lower() @@ -96,9 +122,9 @@ def ask(question, answer=str, default=None, l=None): r = None while True: if default: - r = raw_input('> {0} [{1}] '.format(question, default)) + r = _input_compat('> {0} [{1}] '.format(question, default)) else: - r = raw_input('> {0} '.format(question)) + r = _input_compat('> {0} '.format(question)) r = r.strip() @@ -113,7 +139,7 @@ def ask(question, answer=str, default=None, l=None): print('You must enter an integer') return r else: - raise NotImplemented('Argument `answer` must be str, bool, or integer') + raise NotImplemented('Argument `answer` must be str_compat, bool, or integer') def main(): @@ -135,23 +161,25 @@ def main(): This script will help you create a new Pelican-based website. -Please answer the following questions so this script can generate the files needed by Pelican. +Please answer the following questions so this script can generate the files +needed by Pelican. '''.format(v=__version__)) - project = os.path.join(os.environ['VIRTUAL_ENV'], '.project') + project = os.path.join(os.environ.get('VIRTUAL_ENV', '.'), '.project') if os.path.isfile(project): CONF['basedir'] = open(project, 'r').read().rstrip("\n") - print('Using project associated with current virtual environment. Will save to:\n%s\n' % CONF['basedir']) + print('Using project associated with current virtual environment.' + 'Will save to:\n%s\n' % CONF['basedir']) else: - CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str, default=args.path)) + CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str_compat, default=args.path)) - CONF['sitename'] = ask('What will be the title of this web site?', answer=str, default=args.title) - CONF['author'] = ask('Who will be the author of this web site?', answer=str, default=args.author) - CONF['lang'] = ask('What will be the default language of this web site?', str, args.lang or CONF['lang'], 2) + CONF['sitename'] = ask('What will be the title of this web site?', answer=str_compat, default=args.title) + CONF['author'] = ask('Who will be the author of this web site?', answer=str_compat, default=args.author) + CONF['lang'] = ask('What will be the default language of this web site?', str_compat, args.lang or CONF['lang'], 2) if ask('Do you want to specify a URL prefix? e.g., http://example.com ', answer=bool, default=True): - CONF['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str, CONF['siteurl']) + CONF['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str_compat, CONF['siteurl']) CONF['with_pagination'] = ask('Do you want to enable article pagination?', bool, bool(CONF['default_pagination'])) @@ -161,57 +189,77 @@ Please answer the following questions so this script can generate the files need CONF['default_pagination'] = False mkfile = ask('Do you want to generate a Makefile to easily manage your website?', bool, True) + develop = ask('Do you want an auto-reload & simpleHTTP script to assist with theme and site development?', bool, True) if mkfile: if ask('Do you want to upload your website using FTP?', answer=bool, default=False): - CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str, CONF['ftp_host']) - CONF['ftp_user'] = ask('What is your username on that server?', str, CONF['ftp_user']) - CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ftp_target_dir']) + CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str_compat, CONF['ftp_host']) + CONF['ftp_user'] = ask('What is your username on that server?', str_compat, CONF['ftp_user']) + CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, CONF['ftp_target_dir']) if ask('Do you want to upload your website using SSH?', answer=bool, default=False): - CONF['ssh_host'] = ask('What is the hostname of your SSH server?', str, CONF['ssh_host']) + CONF['ssh_host'] = ask('What is the hostname of your SSH server?', str_compat, CONF['ssh_host']) CONF['ssh_port'] = ask('What is the port of your SSH server?', int, CONF['ssh_port']) - CONF['ssh_user'] = ask('What is your username on that server?', str, CONF['ssh_user']) - CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ssh_target_dir']) + CONF['ssh_user'] = ask('What is your username on that server?', str_compat, CONF['ssh_user']) + CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, CONF['ssh_target_dir']) if ask('Do you want to upload your website using Dropbox?', answer=bool, default=False): - CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str, CONF['dropbox_dir']) + CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str_compat, CONF['dropbox_dir']) try: os.makedirs(os.path.join(CONF['basedir'], 'content')) - except OSError, e: + except OSError as e: print('Error: {0}'.format(e)) try: os.makedirs(os.path.join(CONF['basedir'], 'output')) - except OSError, e: + except OSError as e: print('Error: {0}'.format(e)) try: - with open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w') as fd: + with codecs.open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w', 'utf-8') as fd: + conf_python = dict() + for key, value in CONF.items(): + conf_python[key] = repr(value) + for line in get_template('pelicanconf.py'): template = string.Template(line) - fd.write(template.safe_substitute(CONF)) + fd.write(template.safe_substitute(conf_python)) fd.close() - except OSError, e: + except OSError as e: print('Error: {0}'.format(e)) try: - with open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w') as fd: + with codecs.open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w', 'utf-8') as fd: for line in get_template('publishconf.py'): template = string.Template(line) fd.write(template.safe_substitute(CONF)) fd.close() - except OSError, e: + except OSError as e: print('Error: {0}'.format(e)) if mkfile: - try: - with open(os.path.join(CONF['basedir'], 'Makefile'), 'w') as fd: + with codecs.open(os.path.join(CONF['basedir'], 'Makefile'), 'w', 'utf-8') as fd: for line in get_template('Makefile'): template = string.Template(line) fd.write(template.safe_substitute(CONF)) fd.close() - except OSError, e: + except OSError as e: + print('Error: {0}'.format(e)) + + if develop: + conf_shell = dict() + for key, value in CONF.items(): + if isinstance(value, six.string_types) and ' ' in value: + value = '"' + value.replace('"', '\\"') + '"' + conf_shell[key] = value + try: + with codecs.open(os.path.join(CONF['basedir'], 'develop_server.sh'), 'w', 'utf-8') as fd: + for line in get_template('develop_server.sh'): + template = string.Template(line) + fd.write(template.safe_substitute(conf_shell)) + fd.close() + os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 493) # mode 0o755 + except OSError as e: print('Error: {0}'.format(e)) print('Done. Your new project is available at %s' % CONF['basedir']) diff --git a/pelican/tools/pelican_themes.py b/pelican/tools/pelican_themes.py index 6a021ecc..8d71535d 100755 --- a/pelican/tools/pelican_themes.py +++ b/pelican/tools/pelican_themes.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function + +import six import argparse import os @@ -28,7 +31,7 @@ _BUILTIN_THEMES = ['simple', 'notmyidea'] def err(msg, die=None): """Print an error message and exits if an exit code is given""" - sys.stderr.write(str(msg) + '\n') + sys.stderr.write(msg + '\n') if die: sys.exit((die if type(die) is int else 1)) @@ -180,7 +183,19 @@ def install(path, v=False, u=False): print("Copying `{p}' to `{t}' ...".format(p=path, t=theme_path)) try: shutil.copytree(path, theme_path) - except Exception, e: + + try: + if os.name == 'posix': + for root, dirs, files in os.walk(theme_path): + for d in dirs: + dname = os.path.join(root, d) + os.chmod(dname, 493) # 0o755 + for f in files: + fname = os.path.join(root, f) + os.chmod(fname, 420) # 0o644 + except OSError as e: + err("Cannot change permissions of files or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)), die=False) + except Exception as e: err("Cannot copy `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e))) @@ -200,7 +215,7 @@ def symlink(path, v=False): print("Linking `{p}' to `{t}' ...".format(p=path, t=theme_path)) try: os.symlink(path, theme_path) - except Exception, e: + except Exception as e: err("Cannot link `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e))) @@ -221,7 +236,7 @@ def clean(v=False): print('Removing {0}'.format(path)) try: os.remove(path) - except OSError, e: + except OSError as e: print('Error: cannot remove {0}'.format(path)) else: c+=1 diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in index 3a3fe74c..8dd91630 100644 --- a/pelican/tools/templates/Makefile.in +++ b/pelican/tools/templates/Makefile.in @@ -1,7 +1,7 @@ PELICAN=$pelican PELICANOPTS=$pelicanopts -BASEDIR=$$(PWD) +BASEDIR=$$(CURDIR) INPUTDIR=$$(BASEDIR)/content OUTPUTDIR=$$(BASEDIR)/output CONFFILE=$$(BASEDIR)/pelicanconf.py @@ -24,11 +24,15 @@ help: @echo 'Usage: ' @echo ' make html (re)generate the web site ' @echo ' make clean remove the generated files ' + @echo ' make regenerate regenerate files upon modification ' @echo ' make publish generate using production settings ' - @echo ' ftp_upload upload the web site via FTP ' + @echo ' make serve serve site at http://localhost:8000' + @echo ' make devserver start/restart develop_server.sh ' @echo ' ssh_upload upload the web site via SSH ' + @echo ' rsync_upload upload the web site via rsync+ssh ' @echo ' dropbox_upload upload the web site via Dropbox ' - @echo ' rsync_upload upload the web site via rsync/ssh ' + @echo ' ftp_upload upload the web site via FTP ' + @echo ' github upload the web site via gh-pages ' @echo ' ' @@ -45,19 +49,22 @@ regenerate: clean $$(PELICAN) -r $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS) serve: - cd $$(OUTPUTDIR) && python -m SimpleHTTPServer + cd $$(OUTPUTDIR) && python -m pelican.server + +devserver: + $$(BASEDIR)/develop_server.sh restart publish: $$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS) -dropbox_upload: publish - cp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR) - ssh_upload: publish scp -P $$(SSH_PORT) -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR) rsync_upload: publish - rsync -e "ssh -p $(SSH_PORT)" -P -rvz --delete $(OUTPUTDIR)/* $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) + rsync -e "ssh -p $(SSH_PORT)" -P -rvz --delete $(OUTPUTDIR) $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) + +dropbox_upload: publish + cp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR) ftp_upload: publish lftp ftp://$$(FTP_USER)@$$(FTP_HOST) -e "mirror -R $$(OUTPUTDIR) $$(FTP_TARGET_DIR) ; quit" @@ -66,4 +73,4 @@ github: publish ghp-import $$(OUTPUTDIR) git push origin gh-pages -.PHONY: html help clean regenerate serve publish ftp_upload ssh_upload rsync_upload dropbox_upload github +.PHONY: html help clean regenerate serve devserver publish ssh_upload rsync_upload dropbox_upload ftp_upload github diff --git a/pelican/tools/templates/develop_server.sh.in b/pelican/tools/templates/develop_server.sh.in new file mode 100755 index 00000000..1eda47c4 --- /dev/null +++ b/pelican/tools/templates/develop_server.sh.in @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +## +# This section should match your Makefile +## +PELICAN=$pelican +PELICANOPTS=$pelicanopts + +BASEDIR=$$(pwd) +INPUTDIR=$$BASEDIR/content +OUTPUTDIR=$$BASEDIR/output +CONFFILE=$$BASEDIR/pelicanconf.py + +### +# Don't change stuff below here unless you are sure +### + +SRV_PID=$$BASEDIR/srv.pid +PELICAN_PID=$$BASEDIR/pelican.pid + +function usage(){ + echo "usage: $$0 (stop) (start) (restart)" + echo "This starts pelican in debug and reload mode and then launches" + echo "A pelican.server to help site development. It doesn't read" + echo "your pelican options so you edit any paths in your Makefile" + echo "you will need to edit it as well" + exit 3 +} + +function shut_down(){ + if [[ -f $$SRV_PID ]]; then + PID=$$(cat $$SRV_PID) + PROCESS=$$(ps -p $$PID | tail -n 1 | awk '{print $$4}') + if [[ $$PROCESS != "" ]]; then + echo "Killing pelican.server" + kill $$PID + else + echo "Stale PID, deleting" + fi + rm $$SRV_PID + else + echo "pelican.server PIDFile not found" + fi + + if [[ -f $$PELICAN_PID ]]; then + PID=$$(cat $$PELICAN_PID) + PROCESS=$$(ps -p $$PID | tail -n 1 | awk '{print $$4}') + if [[ $$PROCESS != "" ]]; then + echo "Killing Pelican" + kill $$PID + else + echo "Stale PID, deleting" + fi + rm $$PELICAN_PID + else + echo "Pelican PIDFile not found" + fi +} + +function start_up(){ + echo "Starting up Pelican and pelican.server" + shift + $$PELICAN --debug --autoreload -r $$INPUTDIR -o $$OUTPUTDIR -s $$CONFFILE $$PELICANOPTS & + echo $$! > $$PELICAN_PID + cd $$OUTPUTDIR + python -m pelican.server & + echo $$! > $$SRV_PID + cd $$BASEDIR + sleep 1 && echo 'Pelican and pelican.server processes now running in background.' +} + +### +# MAIN +### +[[ $$# -ne 1 ]] && usage +if [[ $$1 == "stop" ]]; then + shut_down +elif [[ $$1 == "restart" ]]; then + shut_down + start_up +elif [[ $$1 == "start" ]]; then + start_up +else + usage +fi diff --git a/pelican/tools/templates/pelicanconf.py.in b/pelican/tools/templates/pelicanconf.py.in index 07e286cd..d59a7989 100644 --- a/pelican/tools/templates/pelicanconf.py.in +++ b/pelican/tools/templates/pelicanconf.py.in @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -AUTHOR = u"$author" -SITENAME = u"$sitename" +AUTHOR = $author +SITENAME = $sitename SITEURL = '' TIMEZONE = 'Europe/Paris' -DEFAULT_LANG = '$lang' +DEFAULT_LANG = $lang # Blogroll LINKS = (('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'), diff --git a/pelican/tools/templates/publishconf.py.in b/pelican/tools/templates/publishconf.py.in index 113a7318..a4516332 100644 --- a/pelican/tools/templates/publishconf.py.in +++ b/pelican/tools/templates/publishconf.py.in @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # +import sys +sys.path.append('.') from pelicanconf import * SITEURL = '$siteurl' diff --git a/pelican/utils.py b/pelican/utils.py index b811486d..9780c119 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -1,12 +1,20 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import six + import os import re import pytz import shutil +import traceback import logging -from collections import defaultdict +import errno +import locale +import fnmatch +from collections import defaultdict, Hashable +from functools import partial -from codecs import open as _open +from codecs import open from datetime import datetime from itertools import groupby from jinja2 import Markup @@ -15,6 +23,149 @@ from operator import attrgetter logger = logging.getLogger(__name__) +def strftime(date, date_format): + """ + Replacement for the builtin strftime(). + + This :func:`strftime()` is compatible to Python 2 and 3. In both cases, + input and output is always unicode. + + Still, Python 3's :func:`strftime()` seems to somehow "normalize" unicode + chars in the format string. So if e.g. your format string contains 'ø' or + 'ä', the result will be 'o' and 'a'. + + See here for an `extensive testcase `_. + + :param date: Any object that sports a :meth:`strftime()` method. + :param date_format: Format string, can always be unicode. + :returns: Unicode string with formatted date. + """ + # As tehkonst confirmed, above mentioned testcase runs correctly on + # Python 2 and 3 on Windows as well. Thanks. + if six.PY3: + # It could be so easy... *sigh* + return date.strftime(date_format) + # TODO Perhaps we should refactor again, so that the + # xmlcharrefreplace-regex-dance is always done, regardless + # of the Python version. + else: + # We must ensure that the format string is an encoded byte + # string, ASCII only WTF!!! + # But with "xmlcharrefreplace" our formatted date will produce + # *yuck* like this: + # "Øl trinken beim Besäufnis" + # --> "Øl trinken beim Besäufnis" + date_format = date_format.encode('ascii', + errors="xmlcharrefreplace") + result = date.strftime(date_format) + # strftime() returns an encoded byte string + # which we must decode into unicode. + lang_code, enc = locale.getlocale(locale.LC_ALL) + if enc: + result = result.decode(enc) + else: + result = unicode(result) + # Convert XML character references back to unicode characters. + if "&#" in result: + result = re.sub(r'&#(?P\d+);' + , lambda m: unichr(int(m.group('num'))) + , result + ) + return result + + + +#---------------------------------------------------------------------------- +# Stolen from Django: django.utils.encoding +# + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if not six.PY3: + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + +#---------------------------------------------------------------------------- + +class NoFilesError(Exception): + pass + + +class memoized(object): + '''Decorator. Caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned + (not reevaluated). + ''' + def __init__(self, func): + self.func = func + self.cache = {} + def __call__(self, *args): + if not isinstance(args, Hashable): + # uncacheable. a list, for instance. + # better to not cache than blow up. + return self.func(*args) + if args in self.cache: + return self.cache[args] + else: + value = self.func(*args) + self.cache[args] = value + return value + def __repr__(self): + '''Return the function's docstring.''' + return self.func.__doc__ + def __get__(self, obj, objtype): + '''Support instance methods.''' + return partial(self.__call__, obj) + + +def deprecated_attribute(old, new, since=None, remove=None, doc=None): + """Attribute deprecation decorator for gentle upgrades + + For example: + + class MyClass (object): + @deprecated_attribute( + old='abc', new='xyz', since=(3, 2, 0), remove=(4, 1, 3)) + def abc(): return None + + def __init__(self): + xyz = 5 + + Note that the decorator needs a dummy method to attach to, but the + content of the dummy method is ignored. + """ + def _warn(): + version = '.'.join(six.text_type(x) for x in since) + message = ['{} has been deprecated since {}'.format(old, version)] + if remove: + version = '.'.join(six.text_type(x) for x in remove) + message.append( + ' and will be removed by version {}'.format(version)) + message.append('. Use {} instead.'.format(new)) + logger.warning(''.join(message)) + logger.debug(''.join( + six.text_type(x) for x in traceback.format_stack())) + + def fget(self): + _warn() + return getattr(self, new) + + def fset(self, value): + _warn() + setattr(self, new, value) + + def decorator(dummy): + return property(fget=fget, fset=fset, doc=doc) + + return decorator + def get_date(string): """Return a datetime object from a string. @@ -34,12 +185,13 @@ def get_date(string): raise ValueError("'%s' is not a valid date" % string) -class open(object): +class pelican_open(object): """Open a file and return it's content""" def __init__(self, filename): self.filename = filename + def __enter__(self): - return _open(self.filename, encoding='utf-8').read() + return open(self.filename, encoding='utf-8').read() def __exit__(self, exc_type, exc_value, traceback): pass @@ -51,12 +203,24 @@ def slugify(value): Took from django sources. """ + # TODO Maybe steal again from current Django 1.5dev value = Markup(value).striptags() - if type(value) == unicode: - import unicodedata - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) - return re.sub('[-\s]+', '-', value) + # value must be unicode per se + import unicodedata + from unidecode import unidecode + # unidecode returns str in Py2 and 3, so in Py2 we have to make + # it unicode again + value = unidecode(value) + if isinstance(value, six.binary_type): + value = value.decode('ascii') + # still unicode + value = unicodedata.normalize('NFKD', value) + value = re.sub('[^\w\s-]', '', value).strip().lower() + value = re.sub('[-\s]+', '-', value) + # we want only ASCII chars + value = value.encode('ascii', 'ignore') + # but Pelican should generally use only unicode + return value.decode('ascii') def copy(path, source, destination, destination_path=None, overwrite=False): @@ -86,16 +250,32 @@ def copy(path, source, destination, destination_path=None, overwrite=False): if overwrite: shutil.rmtree(destination_) shutil.copytree(source_, destination_) - logger.info('replacement of %s with %s' % (source_, destination_)) + logger.info('replacement of %s with %s' % (source_, + destination_)) elif os.path.isfile(source_): + dest_dir = os.path.dirname(destination_) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) shutil.copy(source_, destination_) logger.info('copying %s to %s' % (source_, destination_)) - + else: + logger.warning('skipped copy %s to %s' % (source_, destination_)) def clean_output_dir(path): """Remove all the files from the output directory""" + if not os.path.exists(path): + logger.debug("Directory already removed: %s" % path) + return + + if not os.path.isdir(path): + try: + os.remove(path) + except Exception as e: + logger.error("Unable to delete file %s; %e" % path, e) + return + # remove all the existing content from the output folder for filename in os.listdir(path): file = os.path.join(path, filename) @@ -103,21 +283,25 @@ def clean_output_dir(path): try: shutil.rmtree(file) logger.debug("Deleted directory %s" % file) - except Exception, e: + except Exception as e: logger.error("Unable to delete directory %s; %e" % file, e) elif os.path.isfile(file) or os.path.islink(file): try: os.remove(file) logger.debug("Deleted file/link %s" % file) - except Exception, e: + except Exception as e: logger.error("Unable to delete file %s; %e" % file, e) else: logger.error("Unable to delete %s, file type unknown" % file) -def get_relative_path(filename): - """Return the relative path to the given filename""" - return '../' * filename.count('/') + '.' +def get_relative_path(path): + """Return the relative path from the given path to the root path.""" + nslashes = path.count('/') + if nslashes == 0: + return '.' + else: + return '/'.join(['..'] * nslashes) def truncate_html_words(s, num, end_text='...'): @@ -131,7 +315,7 @@ def truncate_html_words(s, num, end_text='...'): """ length = int(num) if length <= 0: - return u'' + return '' html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input') @@ -205,34 +389,31 @@ def process_translations(content_list): for slug, items in grouped_by_slugs: items = list(items) # find items with default language - default_lang_items = filter(attrgetter('in_default_lang'), items) + default_lang_items = list(filter(attrgetter('in_default_lang'), items)) len_ = len(default_lang_items) if len_ > 1: - logger.warning(u'there are %s variants of "%s"' % (len_, slug)) + logger.warning('there are %s variants of "%s"' % (len_, slug)) for x in default_lang_items: - logger.warning(' %s' % x.filename) + logger.warning(' {}'.format(x.source_path)) elif len_ == 0: default_lang_items = items[:1] if not slug: - msg = 'empty slug for %r. ' % default_lang_items[0].filename\ - + 'You can fix this by adding a title or a slug to your '\ - + 'content' - logger.warning(msg) + logger.warning(( + 'empty slug for {!r}. ' + 'You can fix this by adding a title or a slug to your ' + 'content' + ).format(default_lang_items[0].source_path)) index.extend(default_lang_items) - translations.extend(filter( - lambda x: x not in default_lang_items, - items - )) + translations.extend([x for x in items if x not in default_lang_items]) for a in items: - a.translations = filter(lambda x: x != a, items) + a.translations = [x for x in items if x != a] return index, translations LAST_MTIME = 0 - -def files_changed(path, extensions): +def files_changed(path, extensions, ignores=[]): """Return True if the files have changed since the last check""" def file_times(path): @@ -240,28 +421,32 @@ def files_changed(path, extensions): for root, dirs, files in os.walk(path): dirs[:] = [x for x in dirs if x[0] != '.'] for f in files: - if any(f.endswith(ext) for ext in extensions): + if any(f.endswith(ext) for ext in extensions) \ + and not any(fnmatch.fnmatch(f, ignore) for ignore in ignores): yield os.stat(os.path.join(root, f)).st_mtime global LAST_MTIME - mtime = max(file_times(path)) - if mtime > LAST_MTIME: - LAST_MTIME = mtime - return True + try: + mtime = max(file_times(path)) + if mtime > LAST_MTIME: + LAST_MTIME = mtime + return True + except ValueError: + raise NoFilesError("No files with the given extension(s) found.") return False FILENAMES_MTIMES = defaultdict(int) -def file_changed(filename): - mtime = os.stat(filename).st_mtime - if FILENAMES_MTIMES[filename] == 0: - FILENAMES_MTIMES[filename] = mtime +def file_changed(path): + mtime = os.stat(path).st_mtime + if FILENAMES_MTIMES[path] == 0: + FILENAMES_MTIMES[path] = mtime return False else: - if mtime > FILENAMES_MTIMES[filename]: - FILENAMES_MTIMES[filename] = mtime + if mtime > FILENAMES_MTIMES[path]: + FILENAMES_MTIMES[path] = mtime return True return False @@ -276,3 +461,11 @@ def set_date_tzinfo(d, tz_name=None): return tz.localize(d) else: return d + + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST or not os.path.isdir(path): + raise diff --git a/pelican/writers.py b/pelican/writers.py index 75971ee9..429507a0 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import with_statement +from __future__ import with_statement, unicode_literals, print_function +import six import os -import re import locale import logging from codecs import open -from functools import partial from feedgenerator import Atom1Feed, Rss201rev2Feed from jinja2 import Markup from pelican.paginator import Paginator @@ -41,45 +40,45 @@ class Writer(object): link='%s/%s' % (self.site_url, item.url), unique_id='tag:%s,%s:%s' % (self.site_url.replace('http://', ''), item.date.date(), item.url), - description=item.content, + description=item.get_content(self.site_url), categories=item.tags if hasattr(item, 'tags') else None, - author_name=getattr(item, 'author', 'John Doe'), + author_name=getattr(item, 'author', ''), pubdate=set_date_tzinfo(item.date, self.settings.get('TIMEZONE', None))) - def write_feed(self, elements, context, filename=None, feed_type='atom'): + def write_feed(self, elements, context, path=None, feed_type='atom'): """Generate a feed with the list of articles provided - Return the feed. If no output_path or filename is specified, just + Return the feed. If no path or output_path is specified, just return the feed object. :param elements: the articles to put on the feed. :param context: the context to get the feed metadata. - :param filename: the filename to output. + :param path: the path to output. :param feed_type: the feed type to use (atom or rss) """ old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, str('C')) try: - self.site_url = context.get('SITEURL', get_relative_path(filename)) + self.site_url = context.get('SITEURL', get_relative_path(path)) self.feed_domain = context.get('FEED_DOMAIN') - self.feed_url = '%s/%s' % (self.feed_domain, filename) + self.feed_url = '{}/{}'.format(self.feed_domain, path) feed = self._create_new_feed(feed_type, context) max_items = len(elements) if self.settings['FEED_MAX_ITEMS']: max_items = min(self.settings['FEED_MAX_ITEMS'], max_items) - for i in xrange(max_items): + for i in range(max_items): self._add_item_to_the_feed(feed, elements[i]) - if filename: - complete_path = os.path.join(self.output_path, filename) + if path: + complete_path = os.path.join(self.output_path, path) try: os.makedirs(os.path.dirname(complete_path)) except Exception: pass - fp = open(complete_path, 'w') + fp = open(complete_path, 'w', encoding='utf-8' if six.PY3 else None) feed.write(fp, 'utf-8') logger.info('writing %s' % complete_path) @@ -110,34 +109,34 @@ class Writer(object): def _write_file(template, localcontext, output_path, name): """Render the template write the file.""" old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + locale.setlocale(locale.LC_ALL, str('C')) try: output = template.render(localcontext) finally: locale.setlocale(locale.LC_ALL, old_locale) - filename = os.sep.join((output_path, name)) + path = os.path.join(output_path, name) try: - os.makedirs(os.path.dirname(filename)) + os.makedirs(os.path.dirname(path)) except Exception: pass - with open(filename, 'w', encoding='utf-8') as f: + with open(path, 'w', encoding='utf-8') as f: f.write(output) - logger.info(u'writing %s' % filename) + logger.info('writing {}'.format(path)) localcontext = context.copy() if relative_urls: - localcontext['SITEURL'] = get_relative_path(name) + relative_path = get_relative_path(name) + context['localsiteurl'] = relative_path + localcontext['SITEURL'] = relative_path localcontext.update(kwargs) - if relative_urls: - self.update_context_contents(name, localcontext) # check paginated paginated = paginated or {} if paginated: # pagination needed, init paginators paginators = {} - for key in paginated.iterkeys(): + for key in paginated.keys(): object_list = paginated[key] if self.settings.get('DEFAULT_PAGINATION'): @@ -148,81 +147,23 @@ class Writer(object): paginators[key] = Paginator(object_list, len(object_list)) # generated pages, and write - for page_num in range(paginators.values()[0].num_pages): + name_root, ext = os.path.splitext(name) + for page_num in range(list(paginators.values())[0].num_pages): paginated_localcontext = localcontext.copy() - paginated_name = name - for key in paginators.iterkeys(): + for key in paginators.keys(): paginator = paginators[key] page = paginator.page(page_num + 1) paginated_localcontext.update( {'%s_paginator' % key: paginator, '%s_page' % key: page}) if page_num > 0: - ext = '.' + paginated_name.rsplit('.')[-1] - paginated_name = paginated_name.replace(ext, - '%s%s' % (page_num + 1, ext)) + paginated_name = '%s%s%s' % ( + name_root, page_num + 1, ext) + else: + paginated_name = name _write_file(template, paginated_localcontext, self.output_path, paginated_name) else: # no pagination _write_file(template, localcontext, self.output_path, name) - - def update_context_contents(self, name, context): - """Recursively run the context to find elements (articles, pages, etc) - whose content getter needs to be modified in order to deal with - relative paths. - - :param name: name of the file to output. - :param context: dict that will be passed to the templates, which need - to be updated. - """ - def _update_content(name, input): - """Change all the relatives paths of the input content to relatives - paths suitable fot the ouput content - - :param name: path of the output. - :param input: input resource that will be passed to the templates. - """ - content = input._content - - hrefs = re.compile(r""" - (?P<\s*[^\>]* # match tag with src and href attr - (?:href|src)\s*=\s* - ) - (?P["\']) # require value to be quoted - (?![#?]) # don't match fragment or query URLs - (?![a-z]+:) # don't match protocol URLS - (?P.*?) # the url value - \2""", re.X) - - def replacer(m): - relative_path = m.group('path') - dest_path = os.path.normpath( - os.sep.join((get_relative_path(name), "static", - relative_path))) - - return m.group('markup') + m.group('quote') + dest_path \ - + m.group('quote') - - return hrefs.sub(replacer, content) - - if context is None: - return - if hasattr(context, 'values'): - context = context.values() - - for item in context: - # run recursively on iterables - if hasattr(item, '__iter__'): - self.update_context_contents(name, item) - - # if it is a content, patch it - elif hasattr(item, '_content'): - relative_path = get_relative_path(name) - - paths = self.reminder.setdefault(item, []) - if relative_path not in paths: - paths.append(relative_path) - setattr(item, "_get_content", - partial(_update_content, name, item)) diff --git a/samples/content/2012-11-30_filename-metadata.rst b/samples/content/2012-11-30_filename-metadata.rst new file mode 100644 index 00000000..b048103d --- /dev/null +++ b/samples/content/2012-11-30_filename-metadata.rst @@ -0,0 +1,4 @@ +FILENAME_METADATA example +######################### + +Some cool stuff! diff --git a/samples/content/another_super_article.rst b/samples/content/another_super_article.rst index 5ec1e2b8..e6e0a92c 100644 --- a/samples/content/another_super_article.rst +++ b/samples/content/another_super_article.rst @@ -14,7 +14,7 @@ Why not ? After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH ! -.. image:: pictures/Sushi.jpg +.. image:: |filename|/pictures/Sushi.jpg :height: 450 px :width: 600 px :alt: alternate text diff --git a/samples/content/cat1/markdown-article.md b/samples/content/cat1/markdown-article.md index 3bf56dc0..5307b47a 100644 --- a/samples/content/cat1/markdown-article.md +++ b/samples/content/cat1/markdown-article.md @@ -2,3 +2,6 @@ Title: A markdown powered article Date: 2011-04-20 You're mutually oblivious. + +[a root-relative link to unbelievable](|filename|/unbelievable.rst) +[a file-relative link to unbelievable](|filename|../unbelievable.rst) diff --git a/samples/content/pages/jinja2_template.html b/samples/content/pages/jinja2_template.html new file mode 100644 index 00000000..1b0dc4e4 --- /dev/null +++ b/samples/content/pages/jinja2_template.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block content %} + +Some text + +{% endblock %} diff --git a/samples/content/pages/test_page.rst b/samples/content/pages/test_page.rst index 06f91c10..2285f17b 100644 --- a/samples/content/pages/test_page.rst +++ b/samples/content/pages/test_page.rst @@ -5,7 +5,7 @@ This is a test page Just an image. -.. image:: pictures/Fat_Cat.jpg +.. image:: |filename|/pictures/Fat_Cat.jpg :height: 450 px :width: 600 px :alt: alternate text diff --git a/samples/content/super_article.rst b/samples/content/super_article.rst index 1dfd8e34..76e57683 100644 --- a/samples/content/super_article.rst +++ b/samples/content/super_article.rst @@ -16,12 +16,12 @@ This is a simple title And here comes the cool stuff_. -.. image:: pictures/Sushi.jpg +.. image:: |filename|/pictures/Sushi.jpg :height: 450 px :width: 600 px :alt: alternate text -.. image:: pictures/Sushi_Macro.jpg +.. image:: |filename|/pictures/Sushi_Macro.jpg :height: 450 px :width: 600 px :alt: alternate text diff --git a/samples/content/unbelievable.rst b/samples/content/unbelievable.rst index 11443e9a..20cb9dc7 100644 --- a/samples/content/unbelievable.rst +++ b/samples/content/unbelievable.rst @@ -4,3 +4,6 @@ Unbelievable ! :date: 2010-10-15 20:30 Or completely awesome. Depends the needs. + +`a root-relative link to markdown-article <|filename|/cat1/markdown-article.md>`_ +`a file-relative link to markdown-article <|filename|cat1/markdown-article.md>`_ diff --git a/samples/pelican.conf.py b/samples/pelican.conf.py index bffe57f6..714a418c 100755 --- a/samples/pelican.conf.py +++ b/samples/pelican.conf.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -AUTHOR = u'Alexis Métaireau' -SITENAME = u"Alexis' log" +from __future__ import unicode_literals + +AUTHOR = 'Alexis Métaireau' +SITENAME = "Alexis' log" SITEURL = 'http://blog.notmyidea.org' TIMEZONE = "Europe/Paris" @@ -8,17 +10,18 @@ GITHUB_URL = 'http://github.com/ametaireau/' DISQUS_SITENAME = "blog-notmyidea" PDF_GENERATOR = False REVERSE_CATEGORY_ORDER = True -LOCALE = "" +LOCALE = "C" DEFAULT_PAGINATION = 4 +DEFAULT_DATE = (2012, 3, 2, 14, 1, 1) -FEED_RSS = 'feeds/all.rss.xml' +FEED_ALL_RSS = 'feeds/all.rss.xml' CATEGORY_FEED_RSS = 'feeds/%s.rss.xml' LINKS = (('Biologeek', 'http://biologeek.org'), ('Filyb', "http://filyb.info/"), ('Libert-fr', "http://www.libert-fr.com"), ('N1k0', "http://prendreuncafe.com/blog/"), - (u'Tarek Ziadé', "http://ziade.org/blog"), + ('Tarek Ziadé', "http://ziade.org/blog"), ('Zubin Mithra', "http://zubin71.wordpress.com/"),) SOCIAL = (('twitter', 'http://twitter.com/ametaireau'), @@ -34,6 +37,9 @@ STATIC_PATHS = ["pictures", ] # A list of files to copy from the source to the destination FILES_TO_COPY = (('extra/robots.txt', 'robots.txt'),) +# custom page generated with a jinja2 template +TEMPLATE_PAGES = {'pages/jinja2_template.html': 'jinja2_template.html'} + # foobar will not be used, because it's not in caps. All configuration keys # have to be in caps foobar = "barbaz" diff --git a/setup.py b/setup.py index 1603746e..4c791008 100755 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ #!/usr/bin/env python from setuptools import setup -requires = ['feedgenerator', 'jinja2 >= 2.4', 'pygments', 'docutils', 'pytz', 'blinker'] +requires = ['feedgenerator>=1.5', 'jinja2 >= 2.6', 'pygments', 'docutils', 'pytz', + 'blinker', 'unidecode', 'six'] try: - import argparse + import argparse # NOQA except ImportError: requires.append('argparse') @@ -17,25 +18,35 @@ entry_points = { ] } + +README = open('README.rst').read() +CHANGELOG = open('docs/changelog.rst').read() + + setup( - name = "pelican", - version = "3.0", - url = 'http://pelican.notmyidea.org/', - author = 'Alexis Metaireau', - author_email = 'alexis@notmyidea.org', - description = "A tool to generate a static blog from reStructuredText or Markdown input files.", - long_description=open('README.rst').read(), - packages = ['pelican', 'pelican.tools', 'pelican.plugins'], - include_package_data = True, - install_requires = requires, - entry_points = entry_points, - classifiers = ['Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], + name="pelican", + version="3.2", + url='http://getpelican.com/', + author='Alexis Metaireau', + author_email='authors@getpelican.com', + description="A tool to generate a static blog from reStructuredText or " + "Markdown input files.", + long_description=README + '\n' + CHANGELOG, + packages=['pelican', 'pelican.tools', 'pelican.plugins'], + include_package_data=True, + install_requires=requires, + entry_points=entry_points, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + test_suite='tests', ) diff --git a/tests/TestPages/hidden_page_with_template.rst b/tests/TestPages/hidden_page_with_template.rst new file mode 100644 index 00000000..36104a09 --- /dev/null +++ b/tests/TestPages/hidden_page_with_template.rst @@ -0,0 +1,11 @@ +This is a test hidden page with a custom template +################################################# + +:status: hidden +:template: custom + +The quick brown fox jumped over the lazy dog's back. + +This page is hidden + +This page has a custom template to be called when rendered diff --git a/tests/TestPages/page_with_template.rst b/tests/TestPages/page_with_template.rst new file mode 100644 index 00000000..9388dc2f --- /dev/null +++ b/tests/TestPages/page_with_template.rst @@ -0,0 +1,8 @@ +This is a test page with a preset template +########################################## + +:template: custom + +The quick brown fox jumped over the lazy dog's back. + +This article has a custom template to be called when rendered diff --git a/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst b/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst new file mode 100644 index 00000000..43f05a15 --- /dev/null +++ b/tests/content/2012-11-29_rst_w_filename_meta#foo-bar.rst @@ -0,0 +1,6 @@ + +Rst with filename metadata +########################## + +:category: yeah +:author: Alexis Métaireau diff --git a/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md b/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md new file mode 100644 index 00000000..cdccfc8a --- /dev/null +++ b/tests/content/2012-11-30_md_w_filename_meta#foo-bar.md @@ -0,0 +1,6 @@ +category: yeah +author: Alexis Métaireau + +Markdown with filename metadata +=============================== + diff --git a/tests/content/TestCategory/article_with_category.rst b/tests/content/TestCategory/article_with_category.rst index 7ed6e6d1..69ffa98b 100644 --- a/tests/content/TestCategory/article_with_category.rst +++ b/tests/content/TestCategory/article_with_category.rst @@ -2,5 +2,6 @@ This is an article with category ! ################################## :category: yeah +:date: 1970-01-01 This article should be in 'yeah' category. diff --git a/tests/content/article.rst b/tests/content/article.rst index 1707ab03..7109c29b 100644 --- a/tests/content/article.rst +++ b/tests/content/article.rst @@ -2,3 +2,5 @@ Article title ############# This is some content. With some stuff to "typogrify". + +Now with added support for :abbr:`TLA (three letter acronym)`. diff --git a/tests/content/article_with_asc_extension.asc b/tests/content/article_with_asc_extension.asc new file mode 100644 index 00000000..9ce2166c --- /dev/null +++ b/tests/content/article_with_asc_extension.asc @@ -0,0 +1,12 @@ +Test AsciiDoc File Header +========================= +:Author: Author O. Article +:Email: +:Date: 2011-09-15 09:05 +:Category: Blog +:Tags: Linux, Python, Pelican + +Used for pelican test +--------------------- + +The quick brown fox jumped over the lazy dog's back. diff --git a/tests/content/article_with_asc_options.asc b/tests/content/article_with_asc_options.asc new file mode 100644 index 00000000..bafb3a4a --- /dev/null +++ b/tests/content/article_with_asc_options.asc @@ -0,0 +1,9 @@ +Test AsciiDoc File Header +========================= + +Used for pelican test +--------------------- + +version {revision} + +The quick brown fox jumped over the lazy dog's back. diff --git a/tests/content/article_with_markdown_and_summary_metadata_multi.md b/tests/content/article_with_markdown_and_summary_metadata_multi.md new file mode 100644 index 00000000..b6ef666c --- /dev/null +++ b/tests/content/article_with_markdown_and_summary_metadata_multi.md @@ -0,0 +1,7 @@ +Title: Article with markdown and summary metadata multi +Date: 2012-10-31 +Summary: + A multi-line summary should be supported + as well as **inline markup**. + +This is some content. diff --git a/tests/content/article_with_markdown_and_summary_metadata_single.md b/tests/content/article_with_markdown_and_summary_metadata_single.md new file mode 100644 index 00000000..a7d6f09b --- /dev/null +++ b/tests/content/article_with_markdown_and_summary_metadata_single.md @@ -0,0 +1,5 @@ +Title: Article with markdown and summary metadata single +Date: 2012-10-30 +Summary: A single-line summary should be supported as well as **inline markup**. + +This is some content. diff --git a/tests/content/article_with_markdown_extension.markdown b/tests/content/article_with_markdown_extension.markdown new file mode 100644 index 00000000..94e92871 --- /dev/null +++ b/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/tests/content/article_with_markdown_markup_extensions.md new file mode 100644 index 00000000..6cf56403 --- /dev/null +++ b/tests/content/article_with_markdown_markup_extensions.md @@ -0,0 +1,8 @@ +Title: Test Markdown extensions + +[TOC] + +## Level1 + +### Level2 + diff --git a/tests/content/article_with_md_extension.md b/tests/content/article_with_md_extension.md index 11aa22a2..1f111796 100644 --- a/tests/content/article_with_md_extension.md +++ b/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/tests/content/article_with_template.rst b/tests/content/article_with_template.rst new file mode 100644 index 00000000..eb55738c --- /dev/null +++ b/tests/content/article_with_template.rst @@ -0,0 +1,8 @@ +Article with template +##################### + +:template: custom + +This article has a custom template to be called when rendered + +This is some content. With some stuff to "typogrify". diff --git a/tests/content/wordpressexport.xml b/tests/content/wordpressexport.xml index d3e86cba..0d68f180 100644 --- a/tests/content/wordpressexport.xml +++ b/tests/content/wordpressexport.xml @@ -112,10 +112,10 @@ A normal post - http://thisisa.test/?p=173 + http://thisisa.test/?p=174 Thu, 01 Jan 1970 00:00:00 +0000 bob - http://thisisa.test/?p=173 + http://thisisa.test/?p=174 - 173 + 174 2012-02-16 15:52:55 0000-00-00 00:00:00 open @@ -574,5 +574,59 @@ Bottom line: don't mess up with birds]]> + + A normal post with some <html> entities in the title. You can't miss them. + http://thisisa.test/?p=175 + Thu, 01 Jan 1970 00:00:00 +0000 + bob + http://thisisa.test/?p=175 + + +
  3. 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.
  4. +
  5. 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.
  6. + + +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 + html-entity-test + publish + 0 + 0 + post + + 0 + + + _edit_last + + +
    diff --git a/tests/default_conf.py b/tests/default_conf.py index acb7d9da..09548fdd 100644 --- a/tests/default_conf.py +++ b/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'), diff --git a/tests/output/basic/a-markdown-powered-article.html b/tests/output/basic/a-markdown-powered-article.html index 32a863d6..80a12212 100644 --- a/tests/output/basic/a-markdown-powered-article.html +++ b/tests/output/basic/a-markdown-powered-article.html @@ -4,9 +4,8 @@ A markdown powered article - - - + + @@ -20,70 +19,49 @@ - - -
    -
    -
    - - -
    +
- - - \ No newline at end of file diff --git a/tests/output/basic/archives.html b/tests/output/basic/archives.html index 840dfa02..cb06dfa1 100644 --- a/tests/output/basic/archives.html +++ b/tests/output/basic/archives.html @@ -4,9 +4,8 @@ A Pelican Blog - - - + + @@ -20,75 +19,50 @@ - - -
+

Archives for A Pelican Blog

- -
Fri 15 October 2010
-
Unbelievable !
- -
Wed 20 October 2010
-
Oh yeah !
- -
Thu 02 December 2010
-
This is a super article !
- -
Thu 17 February 2011
-
Article 1
- -
Thu 17 February 2011
-
Article 2
- -
Thu 17 February 2011
-
Article 3
- -
Wed 20 April 2011
-
A markdown powered article
- +
Fri 30 November 2012
+
FILENAME_METADATA example
Wed 29 February 2012
-
Second article
- +
Second article
+
Wed 20 April 2011
+
A markdown powered article
+
Thu 17 February 2011
+
Article 1
+
Thu 17 February 2011
+
Article 2
+
Thu 17 February 2011
+
Article 3
+
Thu 02 December 2010
+
This is a super article !
+
Wed 20 October 2010
+
Oh yeah !
+
Fri 15 October 2010
+
Unbelievable !
-
- - -
+
- - - \ No newline at end of file diff --git a/tests/output/basic/article-1.html b/tests/output/basic/article-1.html index c1199371..b372d6ca 100644 --- a/tests/output/basic/article-1.html +++ b/tests/output/basic/article-1.html @@ -4,9 +4,8 @@ Article 1 - - - + + @@ -20,71 +19,48 @@ - - -
- + + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/article-2.html b/tests/output/basic/article-2.html index 62dd0368..1a18cb47 100644 --- a/tests/output/basic/article-2.html +++ b/tests/output/basic/article-2.html @@ -4,9 +4,8 @@ Article 2 - - - + + @@ -20,71 +19,48 @@ - - -
- + + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/article-3.html b/tests/output/basic/article-3.html index 9fd6df0a..6b83b062 100644 --- a/tests/output/basic/article-3.html +++ b/tests/output/basic/article-3.html @@ -4,9 +4,8 @@ Article 3 - - - + + @@ -20,71 +19,48 @@ - - -
- + + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/author/alexis-metaireau.html b/tests/output/basic/author/alexis-metaireau.html index ab68482d..e6f65a11 100644 --- a/tests/output/basic/author/alexis-metaireau.html +++ b/tests/output/basic/author/alexis-metaireau.html @@ -3,100 +3,67 @@ A Pelican Blog - Alexis Métaireau - - - - + + + + + + - - - - - +

Other articles


    - - - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/author/bruno.html b/tests/output/basic/author/bruno.html deleted file mode 100644 index 1e2dc655..00000000 --- a/tests/output/basic/author/bruno.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - A Pelican Blog - bruno - - - - - - - - - - - - - - - - - - - - - - - - -
-

Other articles

-
-
    - - - - - - - - - - -
  1. - - - - - - - - -
  2. - - - - - - - - -
  3. - - - - - - - - -
  4. - - - - - - - - -
  5. - - - - - -
-
- - - - -
- - -
- - - - - - - - \ No newline at end of file diff --git a/tests/output/basic/categories.html b/tests/output/basic/categories.html index 5ffb220d..f945d04a 100644 --- a/tests/output/basic/categories.html +++ b/tests/output/basic/categories.html @@ -4,9 +4,8 @@ A Pelican Blog - - - + + @@ -20,55 +19,32 @@ - - - - - - - + + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/category/cat1.html b/tests/output/basic/category/cat1.html index e92fd0df..b8ae397e 100644 --- a/tests/output/basic/category/cat1.html +++ b/tests/output/basic/category/cat1.html @@ -3,89 +3,60 @@ A Pelican Blog - cat1 - - - - + + + + + + - - - - - +

Other articles


    - - - - - - + - - - -
  1. + +
  2. - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/category/content.html b/tests/output/basic/category/content.html deleted file mode 100644 index 0eec912c..00000000 --- a/tests/output/basic/category/content.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - A Pelican Blog - content - - - - - - - - - - - - - - - - - - - - - - - - -
-

Other articles

-
-
    - - - - - - - - - - -
  1. - - - - - -
-
- - - - -
- - -
- - - - - - - - \ No newline at end of file diff --git a/tests/output/basic/category/misc.html b/tests/output/basic/category/misc.html new file mode 100644 index 00000000..2d62b852 --- /dev/null +++ b/tests/output/basic/category/misc.html @@ -0,0 +1,114 @@ + + + + A Pelican Blog - misc + + + + + + + + + + + + + + + + + + +
+

Other articles

+
+
    + + + +
  1. + + + +
  2. +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tests/output/basic/category/yeah.html b/tests/output/basic/category/yeah.html index ccb531f1..37dfde63 100644 --- a/tests/output/basic/category/yeah.html +++ b/tests/output/basic/category/yeah.html @@ -3,111 +3,74 @@ A Pelican Blog - yeah - - - - + + + + + + - - - - - + + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/drafts/a-draft-article.html b/tests/output/basic/drafts/a-draft-article.html deleted file mode 100644 index 59b6223f..00000000 --- a/tests/output/basic/drafts/a-draft-article.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - A draft article - - - - - - - - - - - - - - - - - -
-
-

A draft article

-
-
- - Fri 02 March 2012 - - - -
- By bruno -
- -

In content.

- - - -
-

This is a draft article, it should live under the /drafts/ folder and not be -listed anywhere else.

- -
- - -
-
- -
- - -
- - - - - - - - \ No newline at end of file diff --git a/tests/output/basic/feeds/all-en.atom.xml b/tests/output/basic/feeds/all-en.atom.xml index c9fe8270..54c87905 100644 --- a/tests/output/basic/feeds/all-en.atom.xml +++ b/tests/output/basic/feeds/all-en.atom.xml @@ -1,25 +1,30 @@ -A Pelican Blog../.2012-02-29T00:00:00ZSecond article2012-02-29T00:00:00Zbruno.././second-article.html<p>This is some article, in english</p> -A markdown powered article2011-04-20T00:00:00Zbruno.././a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00Zbruno.././article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00Zbruno.././article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00Zbruno.././article-3.html<p>Article 3</p> -This is a super article !2010-12-02T10:14:00ZAlexis Métaireau.././this-is-a-super-article.html<p>Some content here !</p> +A Pelican Blog/2012-11-30T00:00:00ZFILENAME_METADATA example2012-11-30T00:00:00Ztag:,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Second article2012-02-29T00:00:00Ztag:,2012-02-29:second-article.html<p>This is some article, in english</p> +A markdown powered article2011-04-20T00:00:00Ztag:,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="/unbelievable.html">a root-relative link to unbelievable</a> +<a href="/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00Ztag:,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00Ztag:,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00Ztag:,2011-02-17:article-3.html<p>Article 3</p> +This is a super article !2010-12-02T10:14:00ZAlexis Métaireautag:,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Oh yeah !2010-10-20T10:14:00ZAlexis Métaireau.././oh-yeah.html<div class="section" id="why-not"> +Oh yeah !2010-10-20T10:14:00ZAlexis Métaireautag:,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Unbelievable !2010-10-15T20:30:00Zbruno.././unbelievable.html<p>Or completely awesome. Depends the needs.</p> +Unbelievable !2010-10-15T20:30:00Ztag:,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> \ No newline at end of file diff --git a/tests/output/basic/feeds/all-fr.atom.xml b/tests/output/basic/feeds/all-fr.atom.xml index 606e5186..98b9a681 100644 --- a/tests/output/basic/feeds/all-fr.atom.xml +++ b/tests/output/basic/feeds/all-fr.atom.xml @@ -1,4 +1,3 @@ -A Pelican Blog../.2012-03-02T14:01:01ZTrop bien !2012-03-02T14:01:01Zbruno.././oh-yeah-fr.html<p>Et voila du contenu en français</p> -Deuxième article2012-02-29T00:00:00Zbruno.././second-article-fr.html<p>Ceci est un article, en français.</p> +A Pelican Blog/2012-02-29T00:00:00ZDeuxième article2012-02-29T00:00:00Ztag:,2012-02-29:second-article-fr.html<p>Ceci est un article, en français.</p> \ No newline at end of file diff --git a/tests/output/basic/feeds/all.atom.xml b/tests/output/basic/feeds/all.atom.xml index 3bb7d2fd..3081adc6 100644 --- a/tests/output/basic/feeds/all.atom.xml +++ b/tests/output/basic/feeds/all.atom.xml @@ -1,25 +1,31 @@ -A Pelican Blog../.2012-02-29T00:00:00ZSecond article2012-02-29T00:00:00Zbruno.././second-article.html<p>This is some article, in english</p> -A markdown powered article2011-04-20T00:00:00Zbruno.././a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00Zbruno.././article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00Zbruno.././article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00Zbruno.././article-3.html<p>Article 3</p> -This is a super article !2010-12-02T10:14:00ZAlexis Métaireau.././this-is-a-super-article.html<p>Some content here !</p> +A Pelican Blog/2012-11-30T00:00:00ZFILENAME_METADATA example2012-11-30T00:00:00Ztag:,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Second article2012-02-29T00:00:00Ztag:,2012-02-29:second-article.html<p>This is some article, in english</p> +Deuxième article2012-02-29T00:00:00Ztag:,2012-02-29:second-article-fr.html<p>Ceci est un article, en français.</p> +A markdown powered article2011-04-20T00:00:00Ztag:,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="/unbelievable.html">a root-relative link to unbelievable</a> +<a href="/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00Ztag:,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00Ztag:,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00Ztag:,2011-02-17:article-3.html<p>Article 3</p> +This is a super article !2010-12-02T10:14:00ZAlexis Métaireautag:,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Oh yeah !2010-10-20T10:14:00ZAlexis Métaireau.././oh-yeah.html<div class="section" id="why-not"> +Oh yeah !2010-10-20T10:14:00ZAlexis Métaireautag:,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Unbelievable !2010-10-15T20:30:00Zbruno.././unbelievable.html<p>Or completely awesome. Depends the needs.</p> +Unbelievable !2010-10-15T20:30:00Ztag:,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> \ No newline at end of file diff --git a/tests/output/basic/feeds/bar.atom.xml b/tests/output/basic/feeds/bar.atom.xml index 6ce45518..2c988122 100644 --- a/tests/output/basic/feeds/bar.atom.xml +++ b/tests/output/basic/feeds/bar.atom.xml @@ -1,8 +1,8 @@ -A Pelican Blog../.2010-10-20T10:14:00ZOh yeah !2010-10-20T10:14:00ZAlexis Métaireau.././oh-yeah.html<div class="section" id="why-not"> +A Pelican Blog/2010-10-20T10:14:00ZOh yeah !2010-10-20T10:14:00ZAlexis Métaireautag:,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> \ No newline at end of file diff --git a/tests/output/basic/feeds/cat1.atom.xml b/tests/output/basic/feeds/cat1.atom.xml index f66c2e73..2fa534aa 100644 --- a/tests/output/basic/feeds/cat1.atom.xml +++ b/tests/output/basic/feeds/cat1.atom.xml @@ -1,5 +1,7 @@ -A Pelican Blog../.2011-04-20T00:00:00ZA markdown powered article2011-04-20T00:00:00Zbruno.././a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00Zbruno.././article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00Zbruno.././article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00Zbruno.././article-3.html<p>Article 3</p> +A Pelican Blog/2011-04-20T00:00:00ZA markdown powered article2011-04-20T00:00:00Ztag:,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="/unbelievable.html">a root-relative link to unbelievable</a> +<a href="/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00Ztag:,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00Ztag:,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00Ztag:,2011-02-17:article-3.html<p>Article 3</p> \ No newline at end of file diff --git a/tests/output/basic/feeds/content.atom.xml b/tests/output/basic/feeds/content.atom.xml deleted file mode 100644 index 0cf53aa7..00000000 --- a/tests/output/basic/feeds/content.atom.xml +++ /dev/null @@ -1,4 +0,0 @@ - -A Pelican Blog../.2012-02-29T00:00:00ZSecond article2012-02-29T00:00:00Zbruno.././second-article.html<p>This is some article, in english</p> -Unbelievable !2010-10-15T20:30:00Zbruno.././unbelievable.html<p>Or completely awesome. Depends the needs.</p> - \ No newline at end of file diff --git a/tests/output/basic/feeds/misc.atom.xml b/tests/output/basic/feeds/misc.atom.xml new file mode 100644 index 00000000..e71bd151 --- /dev/null +++ b/tests/output/basic/feeds/misc.atom.xml @@ -0,0 +1,7 @@ + +A Pelican Blog/2012-11-30T00:00:00ZFILENAME_METADATA example2012-11-30T00:00:00Ztag:,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Second article2012-02-29T00:00:00Ztag:,2012-02-29:second-article.html<p>This is some article, in english</p> +Unbelievable !2010-10-15T20:30:00Ztag:,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> + \ No newline at end of file diff --git a/tests/output/basic/feeds/yeah.atom.xml b/tests/output/basic/feeds/yeah.atom.xml index 7fd8e9f2..78d1a2d0 100644 --- a/tests/output/basic/feeds/yeah.atom.xml +++ b/tests/output/basic/feeds/yeah.atom.xml @@ -1,10 +1,10 @@ -A Pelican Blog../.2010-12-02T10:14:00ZThis is a super article !2010-12-02T10:14:00ZAlexis Métaireau.././this-is-a-super-article.html<p>Some content here !</p> +A Pelican Blog/2010-12-02T10:14:00ZThis is a super article !2010-12-02T10:14:00ZAlexis Métaireautag:,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="|filename|/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() diff --git a/tests/output/basic/filename_metadata-example.html b/tests/output/basic/filename_metadata-example.html new file mode 100644 index 00000000..b7ae95a7 --- /dev/null +++ b/tests/output/basic/filename_metadata-example.html @@ -0,0 +1,66 @@ + + + + FILENAME_METADATA example + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/tests/output/basic/index.html b/tests/output/basic/index.html index 8a2091b9..488d1b08 100644 --- a/tests/output/basic/index.html +++ b/tests/output/basic/index.html @@ -4,9 +4,8 @@ A Pelican Blog - - - + + @@ -20,78 +19,67 @@ - - - - - - -
-

Other articles

-
-
    - - - - - - - - - - -
  1. + +
  2. - - - - + - - - -
  3. + +
  4. - - - - + - - - -
  5. + +
  6. - - - - + - - - -
  7. + +
  8. - - - - + - - - -
  9. + +
  10. - - - - + - - - -
  11. + +
  12. -

    Oh yeah !

    +

    Oh yeah !

    @@ -274,43 +203,28 @@ as well as inline markup. Wed 20 October 2010 - -
    +
    By Alexis Métaireau
    - -

    In bar.

    +

    In bar.

    tags: ohbaryeah

    - - -Translations: - - fr - - - -
    +

    Why not ?

    After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !

    -alternate text +alternate text
    read more - -
    +
  13. - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/oh-yeah-fr.html b/tests/output/basic/oh-yeah-fr.html deleted file mode 100644 index 55eec103..00000000 --- a/tests/output/basic/oh-yeah-fr.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - Trop bien ! - - - - - - - - - - - - - - - - - -
-
-

Trop bien !

-
-
- - Fri 02 March 2012 - - - -
- By bruno -
- -

In content.

- - - -Translations: - - en - - -
-

Et voila du contenu en français

- -
- - -
-
- -
- - -
- - - - - - - - \ No newline at end of file diff --git a/tests/output/basic/oh-yeah.html b/tests/output/basic/oh-yeah.html index 4b650e7d..ab090b58 100644 --- a/tests/output/basic/oh-yeah.html +++ b/tests/output/basic/oh-yeah.html @@ -4,9 +4,8 @@ Oh yeah ! - - - + + @@ -20,81 +19,56 @@ - - -
-
-

Oh yeah !

-
-

Why not ?

After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !

-alternate text +alternate text
-
- - -
+ + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/pages/this-is-a-test-hidden-page.html b/tests/output/basic/pages/this-is-a-test-hidden-page.html new file mode 100644 index 00000000..655ae272 --- /dev/null +++ b/tests/output/basic/pages/this-is-a-test-hidden-page.html @@ -0,0 +1,52 @@ + + + + This is a test hidden page + + + + + + + + + + + + + + + +
+

This is a test hidden page

+ +

This is great for things like error(404) pages +Anyone can see this page but it's not linked to anywhere!

+ +
+
+
+ + + + + \ No newline at end of file diff --git a/tests/output/basic/pages/this-is-a-test-page.html b/tests/output/basic/pages/this-is-a-test-page.html index 0162232c..43e2c2d4 100644 --- a/tests/output/basic/pages/this-is-a-test-page.html +++ b/tests/output/basic/pages/this-is-a-test-page.html @@ -3,68 +3,50 @@ This is a test page - - - - + + + + + + -

This is a test page

- -

Just an image.

-alternate text + +

Just an image.

+alternate text
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/second-article-fr.html b/tests/output/basic/second-article-fr.html index 704971d2..aa30292e 100644 --- a/tests/output/basic/second-article-fr.html +++ b/tests/output/basic/second-article-fr.html @@ -4,9 +4,8 @@ Deuxième article - - - + + @@ -20,76 +19,50 @@ - - -
- +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/second-article.html b/tests/output/basic/second-article.html index 94043446..7319c87f 100644 --- a/tests/output/basic/second-article.html +++ b/tests/output/basic/second-article.html @@ -4,9 +4,8 @@ Second article - - - + + @@ -20,76 +19,50 @@ - - -
- +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/tag/bar.html b/tests/output/basic/tag/bar.html index 4afb4bfd..06b012cb 100644 --- a/tests/output/basic/tag/bar.html +++ b/tests/output/basic/tag/bar.html @@ -3,95 +3,61 @@ A Pelican Blog - bar - - - - + + + + + + - - - - - +

Other articles


    - - - - - - + - - - -
  1. + +
  2. - - - - + - - - -
  3. + +
  4. - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/tag/baz.html b/tests/output/basic/tag/baz.html index b8df58e3..f6c56d4b 100644 --- a/tests/output/basic/tag/baz.html +++ b/tests/output/basic/tag/baz.html @@ -3,95 +3,61 @@ A Pelican Blog - baz - - - - + + + + + + - - - - - +

Other articles


    - - - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/tag/foo.html b/tests/output/basic/tag/foo.html index 20cf293a..4ee92b7d 100644 --- a/tests/output/basic/tag/foo.html +++ b/tests/output/basic/tag/foo.html @@ -3,95 +3,61 @@ A Pelican Blog - foo - - - - + + + + + + - - - - - +

Other articles


    - - - - - - + - - - -
  1. + +
  2. - - - - + - - - -
-
- - - - -
- - -
+ + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/tag/foobar.html b/tests/output/basic/tag/foobar.html index 0a5eeb3b..11f21b6d 100644 --- a/tests/output/basic/tag/foobar.html +++ b/tests/output/basic/tag/foobar.html @@ -3,111 +3,74 @@ A Pelican Blog - foobar - - - - + + + + + + - - - - - + + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/tag/oh.html b/tests/output/basic/tag/oh.html index 563c0f2e..d88192a0 100644 --- a/tests/output/basic/tag/oh.html +++ b/tests/output/basic/tag/oh.html @@ -3,110 +3,68 @@ A Pelican Blog - oh - - - - + + + + + + - - - - - + + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/tag/yeah.html b/tests/output/basic/tag/yeah.html index 4b18b7e3..8533dbdc 100644 --- a/tests/output/basic/tag/yeah.html +++ b/tests/output/basic/tag/yeah.html @@ -3,110 +3,68 @@ A Pelican Blog - yeah - - - - + + + + + + - - - - - + + +
+
- - - \ No newline at end of file diff --git a/tests/output/basic/theme/css/main.css b/tests/output/basic/theme/css/main.css index 28c98b99..29cce82c 100644 --- a/tests/output/basic/theme/css/main.css +++ b/tests/output/basic/theme/css/main.css @@ -70,9 +70,6 @@ p {margin-bottom: 1.143em;} strong, b {font-weight: bold;} em, i {font-style: italic;} -::-moz-selection {background: #F6CF74; color: #fff;} -::selection {background: #F6CF74; color: #fff;} - /* Lists */ ul { list-style: outside disc; @@ -100,7 +97,7 @@ dl {margin: 0 0 1.5em 0;} dt {font-weight: bold;} dd {margin-left: 1.5em;} -pre{background-color: #000; padding: 10px; color: #fff; margin: 10px; overflow: auto;} +pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;} /* Quotes */ blockquote { @@ -111,6 +108,13 @@ cite {} q {} +div.note { + float: right; + margin: 5px; + font-size: 85%; + max-width: 300px; +} + /* Tables */ table {margin: .5em auto 1.5em auto; width: 98%;} @@ -137,8 +141,8 @@ aside, nav, article, figure { /***** Layout *****/ .body {clear: both; margin: 0 auto; width: 800px;} -img.right figure.right {float: right; margin: 0 0 2em 2em;} -img.left, figure.left {float: right; margin: 0 0 2em 2em;} +img.right, figure.right {float: right; margin: 0 0 2em 2em;} +img.left, figure.left {float: left; margin: 0 2em 2em 0;} /* Header @@ -156,7 +160,6 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} font-weight: bold; margin: 0 0 .6em .2em; text-decoration: none; - width: 427px; } #banner h1 a:hover, #banner h1 a:active { background: none; @@ -305,6 +308,11 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');} .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');} .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');} + .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');} + .social a[href*='github.com'], + .social a[href*='git.io'] {background-image: url('../images/icons/github.png');} + .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');} + .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');} /* About diff --git a/tests/output/basic/theme/css/pygment.css b/tests/output/basic/theme/css/pygment.css index 594b0fa3..fdd056f6 100644 --- a/tests/output/basic/theme/css/pygment.css +++ b/tests/output/basic/theme/css/pygment.css @@ -1,5 +1,5 @@ .hll { -background-color:#FFFFCC; +background-color:#eee; } .c { color:#408090; diff --git a/tests/output/basic/theme/css/wide.css b/tests/output/basic/theme/css/wide.css index 3376f4c7..88fd59ce 100644 --- a/tests/output/basic/theme/css/wide.css +++ b/tests/output/basic/theme/css/wide.css @@ -4,13 +4,17 @@ body { font:1.3em/1.3 "Hoefler Text","Georgia",Georgia,serif,sans-serif; } -.body, #banner nav, #banner nav ul, #about, #featured, #content{ - width: inherit; +.post-info{ + display: none; } #banner nav { + display: none; -moz-border-radius: 0px; - margin-bottom: 0px; + margin-bottom: 20px; + overflow: hidden; + font-size: 1em; + background: #F5F4EF; } #banner nav ul{ @@ -19,10 +23,11 @@ body { #banner nav li{ float: right; + color: #000; } -#banner nav li:first-child a { - -moz-border-radius: 0px; +#banner nav li a { + color: #000; } #banner h1 { diff --git a/tests/output/basic/theme/images/icons/delicious.png b/tests/output/basic/theme/images/icons/delicious.png index c6ce246a..3dccdd84 100644 Binary files a/tests/output/basic/theme/images/icons/delicious.png and b/tests/output/basic/theme/images/icons/delicious.png differ diff --git a/tests/output/basic/theme/images/icons/facebook.png b/tests/output/basic/theme/images/icons/facebook.png new file mode 100644 index 00000000..74e7ad52 Binary files /dev/null and b/tests/output/basic/theme/images/icons/facebook.png differ diff --git a/tests/output/basic/theme/images/icons/github.png b/tests/output/basic/theme/images/icons/github.png new file mode 100644 index 00000000..6c52b486 Binary files /dev/null and b/tests/output/basic/theme/images/icons/github.png differ diff --git a/tests/output/basic/theme/images/icons/gitorious.png b/tests/output/basic/theme/images/icons/gitorious.png new file mode 100644 index 00000000..3eeb3ece Binary files /dev/null and b/tests/output/basic/theme/images/icons/gitorious.png differ diff --git a/tests/output/basic/theme/images/icons/gittip.png b/tests/output/basic/theme/images/icons/gittip.png new file mode 100644 index 00000000..af949625 Binary files /dev/null and b/tests/output/basic/theme/images/icons/gittip.png differ diff --git a/tests/output/basic/theme/images/icons/google-plus.png b/tests/output/basic/theme/images/icons/google-plus.png new file mode 100644 index 00000000..3c6b7432 Binary files /dev/null and b/tests/output/basic/theme/images/icons/google-plus.png differ diff --git a/tests/output/basic/theme/images/icons/lastfm.png b/tests/output/basic/theme/images/icons/lastfm.png index b09c7876..3a6c6262 100644 Binary files a/tests/output/basic/theme/images/icons/lastfm.png and b/tests/output/basic/theme/images/icons/lastfm.png differ diff --git a/tests/output/basic/theme/images/icons/linkedin.png b/tests/output/basic/theme/images/icons/linkedin.png index feb04962..d29c1201 100644 Binary files a/tests/output/basic/theme/images/icons/linkedin.png and b/tests/output/basic/theme/images/icons/linkedin.png differ diff --git a/tests/output/basic/theme/images/icons/rss.png b/tests/output/basic/theme/images/icons/rss.png index 7d4e85d9..7862c65a 100644 Binary files a/tests/output/basic/theme/images/icons/rss.png and b/tests/output/basic/theme/images/icons/rss.png differ diff --git a/tests/output/basic/theme/images/icons/twitter.png b/tests/output/basic/theme/images/icons/twitter.png index d6119280..d0ef3cc1 100644 Binary files a/tests/output/basic/theme/images/icons/twitter.png and b/tests/output/basic/theme/images/icons/twitter.png differ diff --git a/tests/output/basic/this-is-a-super-article.html b/tests/output/basic/this-is-a-super-article.html index cb12da2d..0188c07e 100644 --- a/tests/output/basic/this-is-a-super-article.html +++ b/tests/output/basic/this-is-a-super-article.html @@ -4,9 +4,8 @@ This is a super article ! - - - + + @@ -20,54 +19,41 @@ - - -
- + + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/basic/unbelievable.html b/tests/output/basic/unbelievable.html index 4c6e6b07..36a1703c 100644 --- a/tests/output/basic/unbelievable.html +++ b/tests/output/basic/unbelievable.html @@ -4,9 +4,8 @@ Unbelievable ! - - - + + @@ -20,71 +19,50 @@ - - -
- + + +
-
- - -
+ - - - \ No newline at end of file diff --git a/tests/output/custom/a-markdown-powered-article.html b/tests/output/custom/a-markdown-powered-article.html index c001278a..49c0d422 100644 --- a/tests/output/custom/a-markdown-powered-article.html +++ b/tests/output/custom/a-markdown-powered-article.html @@ -4,11 +4,9 @@ A markdown powered article - + + - - - @@ -22,127 +20,90 @@ - - Fork me on GitHub - - - -
-
-

A markdown powered article

-
-

You're mutually oblivious.

+

a root-relative link to unbelievable +a file-relative link to unbelievable

+
-

Comments !

-
- -
- - -
+

Comments !

+
+ + + +
-
- -
+ - - -
+ - - - - \ No newline at end of file diff --git a/tests/output/custom/archives.html b/tests/output/custom/archives.html index e9393667..632387a4 100644 --- a/tests/output/custom/archives.html +++ b/tests/output/custom/archives.html @@ -4,11 +4,9 @@ Alexis' log - + + - - - @@ -22,119 +20,76 @@ - - Fork me on GitHub - - - -
+

Archives for Alexis' log

- -
Fri 15 October 2010
-
Unbelievable !
- -
Wed 20 October 2010
-
Oh yeah !
- -
Thu 02 December 2010
-
This is a super article !
- -
Thu 17 February 2011
-
Article 1
- -
Thu 17 February 2011
-
Article 2
- -
Thu 17 February 2011
-
Article 3
- -
Wed 20 April 2011
-
A markdown powered article
- +
Fri 30 November 2012
+
FILENAME_METADATA example
Wed 29 February 2012
-
Second article
- +
Second article
+
Wed 20 April 2011
+
A markdown powered article
+
Thu 17 February 2011
+
Article 1
+
Thu 17 February 2011
+
Article 2
+
Thu 17 February 2011
+
Article 3
+
Thu 02 December 2010
+
This is a super article !
+
Wed 20 October 2010
+
Oh yeah !
+
Fri 15 October 2010
+
Unbelievable !
-
- -
+ - - -
+
- - - - \ No newline at end of file diff --git a/tests/output/custom/article-1.html b/tests/output/custom/article-1.html index 4bdb8f16..a07b669b 100644 --- a/tests/output/custom/article-1.html +++ b/tests/output/custom/article-1.html @@ -4,11 +4,9 @@ Article 1 - + + - - - @@ -22,128 +20,89 @@ - - Fork me on GitHub - - - -
-
-

Article 1

-
- -

Article 1

- -
- +
-

Comments !

-
- -
- - -
+

Comments !

+
+ + + +
-
- -
+ - - -
+ - - - - \ No newline at end of file diff --git a/tests/output/custom/article-2.html b/tests/output/custom/article-2.html index 6a0bb442..fd95b709 100644 --- a/tests/output/custom/article-2.html +++ b/tests/output/custom/article-2.html @@ -4,11 +4,9 @@ Article 2 - + + - - - @@ -22,128 +20,89 @@ - - Fork me on GitHub - - - -
-
-

Article 2

-
- -

Article 2

- -
- +
-

Comments !

-
- -
- - -
+

Comments !

+
+ + + +
-
- -
+ - - -
+ - - - - \ No newline at end of file diff --git a/tests/output/custom/article-3.html b/tests/output/custom/article-3.html index 8410b4f9..4bd5fd09 100644 --- a/tests/output/custom/article-3.html +++ b/tests/output/custom/article-3.html @@ -4,11 +4,9 @@ Article 3 - + + - - - @@ -22,128 +20,89 @@ - - Fork me on GitHub - - - -
-
-

Article 3

-
- -

Article 3

- -
- +
-

Comments !

-
- -
- - -
+

Comments !

+
+ + + +
-
- -
+ - - -
+ - - - - \ No newline at end of file diff --git a/tests/output/custom/author/alexis-metaireau.html b/tests/output/custom/author/alexis-metaireau.html index 1a373e5d..8bb0dd1c 100644 --- a/tests/output/custom/author/alexis-metaireau.html +++ b/tests/output/custom/author/alexis-metaireau.html @@ -3,97 +3,67 @@ Alexis' log - Alexis Métaireau - - + + + - - - + + + - - Fork me on GitHub - - - - - - +

Other articles


    - - - - - - + - - - -
  1. + +
  2. - - - - + - - - -
  3. + +
  4. - - - - + - - - -
-
- - - - -
- -
+ +

+ Page 1 / 3 + » +

+
+
+ - - -
+ - - - - \ No newline at end of file diff --git a/tests/output/custom/author/alexis-metaireau2.html b/tests/output/custom/author/alexis-metaireau2.html index 43ded360..59926d8c 100644 --- a/tests/output/custom/author/alexis-metaireau2.html +++ b/tests/output/custom/author/alexis-metaireau2.html @@ -3,65 +3,71 @@ Alexis' log - Alexis Métaireau - - + + + - - - + + + - - Fork me on GitHub - - - - - - -
+ +
    - -
  1. +
  2. + + + +
  3. - - - - + - - - -
  4. + +
  5. - - - - + - - - -
  6. + +
  7. - - - - - - - - -
  8. - - - -

    - - - « - - - Page 2 / 2 - -

    - - - -
-
- - - - -
- -
+ +

+ « + Page 2 / 3 + » +

+
+
+ - - -
+
- - - - \ No newline at end of file diff --git a/tests/output/custom/author/alexis-metaireau3.html b/tests/output/custom/author/alexis-metaireau3.html new file mode 100644 index 00000000..5f2ecbb1 --- /dev/null +++ b/tests/output/custom/author/alexis-metaireau3.html @@ -0,0 +1,115 @@ + + + + Alexis' log - Alexis Métaireau + + + + + + + + + + + + + + + +Fork me on GitHub + + + + + + +
+
    +
  1. +
+

+ « + Page 3 / 3 +

+
+
+
+

blogroll

+ +
+ +
+ + + + + + \ No newline at end of file diff --git a/tests/output/custom/categories.html b/tests/output/custom/categories.html index a1a44e5b..64da26b4 100644 --- a/tests/output/custom/categories.html +++ b/tests/output/custom/categories.html @@ -4,11 +4,9 @@ Alexis' log - + + - - - @@ -22,99 +20,58 @@ - - Fork me on GitHub - - - - - - - - - - - + - - - -
  • + +
  • - - - - + - - - -
  • + +
  • - - - -

    - - Page 1 / 1 - -

    - - - - - - - - - -
    - -
    + +

    + Page 1 / 1 +

    +
    +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/category/content.html b/tests/output/custom/category/content.html deleted file mode 100644 index 16651436..00000000 --- a/tests/output/custom/category/content.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - Alexis' log - content - - - - - - - - - - - - - - - - - - - -Fork me on GitHub - - - - - - - - - - - - -
    -

    Other articles

    -
    -
      - - - - - - - - - - -
    1. - - - -

      - - Page 1 / 1 - -

      - - - -
    -
    - - - - -
    - -
    -

    blogroll

    - -
    - - - - -
    - - - - - - - - - - \ No newline at end of file diff --git a/tests/output/custom/category/misc.html b/tests/output/custom/category/misc.html new file mode 100644 index 00000000..33acbd70 --- /dev/null +++ b/tests/output/custom/category/misc.html @@ -0,0 +1,161 @@ + + + + Alexis' log - misc + + + + + + + + + + + + + + + +Fork me on GitHub + + + + + + +
    +

    Other articles

    +
    +
      + + + +
    1. + + + +
    2. +
    +

    + Page 1 / 1 +

    +
    +
    +
    +

    blogroll

    + +
    + +
    + + + + + + \ No newline at end of file diff --git a/tests/output/custom/category/yeah.html b/tests/output/custom/category/yeah.html index 3c9af4e2..c39e1a86 100644 --- a/tests/output/custom/category/yeah.html +++ b/tests/output/custom/category/yeah.html @@ -3,75 +3,52 @@ Alexis' log - yeah - - + + + - - - + + + - - Fork me on GitHub - - - - - -
    +

    + Page 1 / 1 +

    + + + +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/drafts/a-draft-article.html b/tests/output/custom/drafts/a-draft-article.html index 99f5ad68..840b0dfc 100644 --- a/tests/output/custom/drafts/a-draft-article.html +++ b/tests/output/custom/drafts/a-draft-article.html @@ -3,148 +3,107 @@ A draft article - - + + + - - - + + + - - Fork me on GitHub - - - -
    -
    -

    A draft article

    -
    -

    This is a draft article, it should live under the /drafts/ folder and not be listed anywhere else.

    -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
  • -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/feeds/all-en.atom.xml b/tests/output/custom/feeds/all-en.atom.xml index 7356cb17..5c8f9241 100644 --- a/tests/output/custom/feeds/all-en.atom.xml +++ b/tests/output/custom/feeds/all-en.atom.xml @@ -1,25 +1,30 @@ -Alexis' loghttp://blog.notmyidea.org2012-02-29T00:00:00+01:00Second article2012-02-29T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> -A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-3.html<p>Article 3</p> -This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> +Alexis' loghttp://blog.notmyidea.org/2012-11-30T00:00:00+01:00FILENAME_METADATA example2012-11-30T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Second article2012-02-29T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-02-29:second-article.html<p>This is some article, in english</p> +A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireautag:blog.notmyidea.org,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a> +<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-3.html<p>Article 3</p> +This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireautag:blog.notmyidea.org,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> +Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> +Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> \ No newline at end of file diff --git a/tests/output/custom/feeds/all-fr.atom.xml b/tests/output/custom/feeds/all-fr.atom.xml index 27949d80..5d58742c 100644 --- a/tests/output/custom/feeds/all-fr.atom.xml +++ b/tests/output/custom/feeds/all-fr.atom.xml @@ -1,4 +1,4 @@ -Alexis' loghttp://blog.notmyidea.org2012-03-02T14:01:01+01:00Trop bien !2012-03-02T14:01:01+01:00Alexis Métaireauhttp://blog.notmyidea.org/oh-yeah-fr.html<p>Et voila du contenu en français</p> -Deuxième article2012-02-29T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/second-article-fr.html<p>Ceci est un article, en français.</p> +Alexis' loghttp://blog.notmyidea.org/2012-03-02T14:01:01+01:00Trop bien !2012-03-02T14:01:01+01:00Alexis Métaireautag:blog.notmyidea.org,2012-03-02:oh-yeah-fr.html<p>Et voila du contenu en français</p> +Deuxième article2012-02-29T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-02-29:second-article-fr.html<p>Ceci est un article, en français.</p> \ No newline at end of file diff --git a/tests/output/custom/feeds/all.atom.xml b/tests/output/custom/feeds/all.atom.xml index ef6dbf52..ae5fe30c 100644 --- a/tests/output/custom/feeds/all.atom.xml +++ b/tests/output/custom/feeds/all.atom.xml @@ -1,25 +1,32 @@ -Alexis' loghttp://blog.notmyidea.org2012-02-29T00:00:00+01:00Second article2012-02-29T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> -A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-3.html<p>Article 3</p> -This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> +Alexis' loghttp://blog.notmyidea.org/2012-11-30T00:00:00+01:00FILENAME_METADATA example2012-11-30T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Trop bien !2012-03-02T14:01:01+01:00Alexis Métaireautag:blog.notmyidea.org,2012-03-02:oh-yeah-fr.html<p>Et voila du contenu en français</p> +Second article2012-02-29T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-02-29:second-article.html<p>This is some article, in english</p> +Deuxième article2012-02-29T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-02-29:second-article-fr.html<p>Ceci est un article, en français.</p> +A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireautag:blog.notmyidea.org,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a> +<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-3.html<p>Article 3</p> +This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireautag:blog.notmyidea.org,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> +Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> +Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> \ No newline at end of file diff --git a/tests/output/custom/feeds/all.rss.xml b/tests/output/custom/feeds/all.rss.xml index a3f7eff9..0ce2c837 100644 --- a/tests/output/custom/feeds/all.rss.xml +++ b/tests/output/custom/feeds/all.rss.xml @@ -1,25 +1,32 @@ -Alexis' loghttp://blog.notmyidea.orgWed, 29 Feb 2012 00:00:00 +0100Second articlehttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> -Alexis MétaireauWed, 29 Feb 2012 00:00:00 +0100http://blog.notmyidea.org/second-article.htmlfoobarbazA markdown powered articlehttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>Alexis MétaireauWed, 20 Apr 2011 00:00:00 +0200http://blog.notmyidea.org/a-markdown-powered-article.htmlArticle 1http://blog.notmyidea.org/article-1.html<p>Article 1</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-1.htmlArticle 2http://blog.notmyidea.org/article-2.html<p>Article 2</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-2.htmlArticle 3http://blog.notmyidea.org/article-3.html<p>Article 3</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-3.htmlThis is a super article !http://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> +Alexis' loghttp://blog.notmyidea.org/Fri, 30 Nov 2012 00:00:00 +0100FILENAME_METADATA examplehttp://blog.notmyidea.org/filename_metadata-example.html<p>Some cool stuff!</p> +Alexis MétaireauFri, 30 Nov 2012 00:00:00 +0100tag:blog.notmyidea.org,2012-11-30:filename_metadata-example.htmlTrop bien !http://blog.notmyidea.org/oh-yeah-fr.html<p>Et voila du contenu en français</p> +Alexis MétaireauFri, 02 Mar 2012 14:01:01 +0100tag:blog.notmyidea.org,2012-03-02:oh-yeah-fr.htmlSecond articlehttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> +Alexis MétaireauWed, 29 Feb 2012 00:00:00 +0100tag:blog.notmyidea.org,2012-02-29:second-article.htmlfoobarbazDeuxième articlehttp://blog.notmyidea.org/second-article-fr.html<p>Ceci est un article, en français.</p> +Alexis MétaireauWed, 29 Feb 2012 00:00:00 +0100tag:blog.notmyidea.org,2012-02-29:second-article-fr.htmlfoobarbazA markdown powered articlehttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a> +<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p>Alexis MétaireauWed, 20 Apr 2011 00:00:00 +0200tag:blog.notmyidea.org,2011-04-20:a-markdown-powered-article.htmlArticle 1http://blog.notmyidea.org/article-1.html<p>Article 1</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-1.htmlArticle 2http://blog.notmyidea.org/article-2.html<p>Article 2</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-2.htmlArticle 3http://blog.notmyidea.org/article-3.html<p>Article 3</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-3.htmlThis is a super article !http://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Alexis MétaireauThu, 02 Dec 2010 10:14:00 +0100http://blog.notmyidea.org/this-is-a-super-article.htmlfoobarfoobarOh yeah !http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> +Alexis MétaireauThu, 02 Dec 2010 10:14:00 +0100tag:blog.notmyidea.org,2010-12-02:this-is-a-super-article.htmlfoobarfoobarOh yeah !http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Alexis MétaireauWed, 20 Oct 2010 10:14:00 +0200http://blog.notmyidea.org/oh-yeah.htmlohbaryeahUnbelievable !http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> -Alexis MétaireauFri, 15 Oct 2010 20:30:00 +0200http://blog.notmyidea.org/unbelievable.html \ No newline at end of file +Alexis MétaireauWed, 20 Oct 2010 10:14:00 +0200tag:blog.notmyidea.org,2010-10-20:oh-yeah.htmlohbaryeahUnbelievable !http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> +Alexis MétaireauFri, 15 Oct 2010 20:30:00 +0200tag:blog.notmyidea.org,2010-10-15:unbelievable.html \ No newline at end of file diff --git a/tests/output/custom/feeds/bar.atom.xml b/tests/output/custom/feeds/bar.atom.xml index 84ac9cda..9945f938 100644 --- a/tests/output/custom/feeds/bar.atom.xml +++ b/tests/output/custom/feeds/bar.atom.xml @@ -1,8 +1,8 @@ -Alexis' loghttp://blog.notmyidea.org2010-10-20T10:14:00+02:00Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> +Alexis' loghttp://blog.notmyidea.org/2010-10-20T10:14:00+02:00Oh yeah !2010-10-20T10:14:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-20:oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> \ No newline at end of file diff --git a/tests/output/custom/feeds/bar.rss.xml b/tests/output/custom/feeds/bar.rss.xml index bb5cb2b4..a7495c36 100644 --- a/tests/output/custom/feeds/bar.rss.xml +++ b/tests/output/custom/feeds/bar.rss.xml @@ -1,8 +1,8 @@ -Alexis' loghttp://blog.notmyidea.orgWed, 20 Oct 2010 10:14:00 +0200Oh yeah !http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> +Alexis' loghttp://blog.notmyidea.org/Wed, 20 Oct 2010 10:14:00 +0200Oh yeah !http://blog.notmyidea.org/oh-yeah.html<div class="section" id="why-not"> <h2>Why not ?</h2> <p>After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> </div> -Alexis MétaireauWed, 20 Oct 2010 10:14:00 +0200http://blog.notmyidea.org/oh-yeah.htmlohbaryeah \ No newline at end of file +Alexis MétaireauWed, 20 Oct 2010 10:14:00 +0200tag:blog.notmyidea.org,2010-10-20:oh-yeah.htmlohbaryeah \ No newline at end of file diff --git a/tests/output/custom/feeds/cat1.atom.xml b/tests/output/custom/feeds/cat1.atom.xml index e0f01780..5454ce4d 100644 --- a/tests/output/custom/feeds/cat1.atom.xml +++ b/tests/output/custom/feeds/cat1.atom.xml @@ -1,5 +1,7 @@ -Alexis' loghttp://blog.notmyidea.org2011-04-20T00:00:00+02:00A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>Article 12011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-1.html<p>Article 1</p> -Article 22011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-2.html<p>Article 2</p> -Article 32011-02-17T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/article-3.html<p>Article 3</p> +Alexis' loghttp://blog.notmyidea.org/2011-04-20T00:00:00+02:00A markdown powered article2011-04-20T00:00:00+02:00Alexis Métaireautag:blog.notmyidea.org,2011-04-20:a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a> +<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p>Article 12011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-1.html<p>Article 1</p> +Article 22011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-2.html<p>Article 2</p> +Article 32011-02-17T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2011-02-17:article-3.html<p>Article 3</p> \ No newline at end of file diff --git a/tests/output/custom/feeds/cat1.rss.xml b/tests/output/custom/feeds/cat1.rss.xml index 0043b2fb..349c3fe6 100644 --- a/tests/output/custom/feeds/cat1.rss.xml +++ b/tests/output/custom/feeds/cat1.rss.xml @@ -1,5 +1,7 @@ -Alexis' loghttp://blog.notmyidea.orgWed, 20 Apr 2011 00:00:00 +0200A markdown powered articlehttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p>Alexis MétaireauWed, 20 Apr 2011 00:00:00 +0200http://blog.notmyidea.org/a-markdown-powered-article.htmlArticle 1http://blog.notmyidea.org/article-1.html<p>Article 1</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-1.htmlArticle 2http://blog.notmyidea.org/article-2.html<p>Article 2</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-2.htmlArticle 3http://blog.notmyidea.org/article-3.html<p>Article 3</p> -Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100http://blog.notmyidea.org/article-3.html \ No newline at end of file +Alexis' loghttp://blog.notmyidea.org/Wed, 20 Apr 2011 00:00:00 +0200A markdown powered articlehttp://blog.notmyidea.org/a-markdown-powered-article.html<p>You're mutually oblivious.</p> +<p><a href="http://blog.notmyidea.org/unbelievable.html">a root-relative link to unbelievable</a> +<a href="http://blog.notmyidea.org/unbelievable.html">a file-relative link to unbelievable</a></p>Alexis MétaireauWed, 20 Apr 2011 00:00:00 +0200tag:blog.notmyidea.org,2011-04-20:a-markdown-powered-article.htmlArticle 1http://blog.notmyidea.org/article-1.html<p>Article 1</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-1.htmlArticle 2http://blog.notmyidea.org/article-2.html<p>Article 2</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-2.htmlArticle 3http://blog.notmyidea.org/article-3.html<p>Article 3</p> +Alexis MétaireauThu, 17 Feb 2011 00:00:00 +0100tag:blog.notmyidea.org,2011-02-17:article-3.html \ No newline at end of file diff --git a/tests/output/custom/feeds/content.atom.xml b/tests/output/custom/feeds/content.atom.xml deleted file mode 100644 index c141a0aa..00000000 --- a/tests/output/custom/feeds/content.atom.xml +++ /dev/null @@ -1,4 +0,0 @@ - -Alexis' loghttp://blog.notmyidea.org2012-02-29T00:00:00+01:00Second article2012-02-29T00:00:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> -Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireauhttp://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> - \ No newline at end of file diff --git a/tests/output/custom/feeds/content.rss.xml b/tests/output/custom/feeds/content.rss.xml deleted file mode 100644 index 9f36c97e..00000000 --- a/tests/output/custom/feeds/content.rss.xml +++ /dev/null @@ -1,4 +0,0 @@ - -Alexis' loghttp://blog.notmyidea.orgWed, 29 Feb 2012 00:00:00 +0100Second articlehttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> -Alexis MétaireauWed, 29 Feb 2012 00:00:00 +0100http://blog.notmyidea.org/second-article.htmlfoobarbazUnbelievable !http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> -Alexis MétaireauFri, 15 Oct 2010 20:30:00 +0200http://blog.notmyidea.org/unbelievable.html \ No newline at end of file diff --git a/tests/output/custom/feeds/misc.atom.xml b/tests/output/custom/feeds/misc.atom.xml new file mode 100644 index 00000000..45c996f3 --- /dev/null +++ b/tests/output/custom/feeds/misc.atom.xml @@ -0,0 +1,7 @@ + +Alexis' loghttp://blog.notmyidea.org/2012-11-30T00:00:00+01:00FILENAME_METADATA example2012-11-30T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-11-30:filename_metadata-example.html<p>Some cool stuff!</p> +Second article2012-02-29T00:00:00+01:00Alexis Métaireautag:blog.notmyidea.org,2012-02-29:second-article.html<p>This is some article, in english</p> +Unbelievable !2010-10-15T20:30:00+02:00Alexis Métaireautag:blog.notmyidea.org,2010-10-15:unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> + \ No newline at end of file diff --git a/tests/output/custom/feeds/misc.rss.xml b/tests/output/custom/feeds/misc.rss.xml new file mode 100644 index 00000000..a0fa290d --- /dev/null +++ b/tests/output/custom/feeds/misc.rss.xml @@ -0,0 +1,7 @@ + +Alexis' loghttp://blog.notmyidea.org/Fri, 30 Nov 2012 00:00:00 +0100FILENAME_METADATA examplehttp://blog.notmyidea.org/filename_metadata-example.html<p>Some cool stuff!</p> +Alexis MétaireauFri, 30 Nov 2012 00:00:00 +0100tag:blog.notmyidea.org,2012-11-30:filename_metadata-example.htmlSecond articlehttp://blog.notmyidea.org/second-article.html<p>This is some article, in english</p> +Alexis MétaireauWed, 29 Feb 2012 00:00:00 +0100tag:blog.notmyidea.org,2012-02-29:second-article.htmlfoobarbazUnbelievable !http://blog.notmyidea.org/unbelievable.html<p>Or completely awesome. Depends the needs.</p> +<p><a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a root-relative link to markdown-article</a> +<a class="reference external" href="http://blog.notmyidea.org/a-markdown-powered-article.html">a file-relative link to markdown-article</a></p> +Alexis MétaireauFri, 15 Oct 2010 20:30:00 +0200tag:blog.notmyidea.org,2010-10-15:unbelievable.html \ No newline at end of file diff --git a/tests/output/custom/feeds/yeah.atom.xml b/tests/output/custom/feeds/yeah.atom.xml index 4c6eed49..d2307359 100644 --- a/tests/output/custom/feeds/yeah.atom.xml +++ b/tests/output/custom/feeds/yeah.atom.xml @@ -1,10 +1,10 @@ -Alexis' loghttp://blog.notmyidea.org2010-12-02T10:14:00+01:00This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireauhttp://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> +Alexis' loghttp://blog.notmyidea.org/2010-12-02T10:14:00+01:00This is a super article !2010-12-02T10:14:00+01:00Alexis Métaireautag:blog.notmyidea.org,2010-12-02:this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() diff --git a/tests/output/custom/feeds/yeah.rss.xml b/tests/output/custom/feeds/yeah.rss.xml index c4f5512e..a54e48e4 100644 --- a/tests/output/custom/feeds/yeah.rss.xml +++ b/tests/output/custom/feeds/yeah.rss.xml @@ -1,14 +1,14 @@ -Alexis' loghttp://blog.notmyidea.orgThu, 02 Dec 2010 10:14:00 +0100This is a super article !http://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> +Alexis' loghttp://blog.notmyidea.org/Thu, 02 Dec 2010 10:14:00 +0100This is a super article !http://blog.notmyidea.org/this-is-a-super-article.html<p>Some content here !</p> <div class="section" id="this-is-a-simple-title"> <h2>This is a simple title</h2> <p>And here comes the cool <a class="reference external" href="http://books.couchdb.org/relax/design-documents/views">stuff</a>.</p> -<img alt="alternate text" src="pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> -<img alt="alternate text" src="pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi.jpg" style="width: 600px; height: 450px;" /> +<img alt="alternate text" src="http://blog.notmyidea.org/static/pictures/Sushi_Macro.jpg" style="width: 600px; height: 450px;" /> <pre class="literal-block"> &gt;&gt;&gt; from ipdb import set_trace &gt;&gt;&gt; set_trace() </pre> <p>→ And now try with some utf8 hell: ééé</p> </div> -Alexis MétaireauThu, 02 Dec 2010 10:14:00 +0100http://blog.notmyidea.org/this-is-a-super-article.htmlfoobarfoobar \ No newline at end of file +Alexis MétaireauThu, 02 Dec 2010 10:14:00 +0100tag:blog.notmyidea.org,2010-12-02:this-is-a-super-article.htmlfoobarfoobar \ No newline at end of file diff --git a/tests/output/custom/filename_metadata-example.html b/tests/output/custom/filename_metadata-example.html new file mode 100644 index 00000000..8f6353cb --- /dev/null +++ b/tests/output/custom/filename_metadata-example.html @@ -0,0 +1,116 @@ + + + + FILENAME_METADATA example + + + + + + + + + + + + + + + +Fork me on GitHub + + +
    +
    +
    +

    + FILENAME_METADATA example

    +
    + +
    +

    Some cool stuff!

    + +
    +
    +

    Comments !

    +
    + +
    + +
    +
    +
    +
    +

    blogroll

    + +
    + +
    + + + + + + \ No newline at end of file diff --git a/tests/output/custom/index.html b/tests/output/custom/index.html index 466a4db4..cc905735 100644 --- a/tests/output/custom/index.html +++ b/tests/output/custom/index.html @@ -4,11 +4,9 @@ Alexis' log - + + - - - @@ -22,84 +20,76 @@ - - Fork me on GitHub - - - - - - - -
    -

    Other articles

    -
    -
      - - - - - - + read more +

      There are comments.

      + + - - - -
    1. + +
    2. - - - - + - - - -
    -
    - - - - -
    - -
    + +

    + Page 1 / 3 + » +

    +
    +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/index2.html b/tests/output/custom/index2.html index 9262d717..f0d2f948 100644 --- a/tests/output/custom/index2.html +++ b/tests/output/custom/index2.html @@ -4,11 +4,9 @@ Alexis' log - + + - - - @@ -22,46 +20,29 @@ - - Fork me on GitHub - - - - - - -
    + +
      - -
    1. +
    2. + - -

      Article 3

      + +
    3. - - - - + - - - -
    4. + +
    5. - - - - + - - - -
    6. + +
    7. - - - - - - - - -
    8. - - - -

      - - - « - - - Page 2 / 2 - -

      - - - -
    -
    - - - - -
    - -
    + +

    + « + Page 2 / 3 + » +

    +
    +
    + - - -
    +
    - - - - \ No newline at end of file diff --git a/tests/output/custom/index3.html b/tests/output/custom/index3.html new file mode 100644 index 00000000..60f8633d --- /dev/null +++ b/tests/output/custom/index3.html @@ -0,0 +1,115 @@ + + + + Alexis' log + + + + + + + + + + + + + + + +Fork me on GitHub + + + + + + +
    +
      +
    1. +
    +

    + « + Page 3 / 3 +

    +
    +
    +
    +

    blogroll

    + +
    + +
    + + + + + + \ No newline at end of file diff --git a/tests/output/custom/jinja2_template.html b/tests/output/custom/jinja2_template.html new file mode 100644 index 00000000..2def2d4e --- /dev/null +++ b/tests/output/custom/jinja2_template.html @@ -0,0 +1,82 @@ + + + + Alexis' log + + + + + + + + + + + + + + + +Fork me on GitHub + + + +Some text + +
    +
    +

    blogroll

    + +
    + +
    + + + + + + \ No newline at end of file diff --git a/tests/output/custom/oh-yeah-fr.html b/tests/output/custom/oh-yeah-fr.html index e692105b..0c1fdcdb 100644 --- a/tests/output/custom/oh-yeah-fr.html +++ b/tests/output/custom/oh-yeah-fr.html @@ -4,11 +4,9 @@ Trop bien ! - + + - - - @@ -22,133 +20,91 @@ - - Fork me on GitHub - - - -
    -
    -

    Trop bien !

    -
    - -

    Et voila du contenu en français

    - -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/oh-yeah.html b/tests/output/custom/oh-yeah.html index 6ffaad13..cb30a7d6 100644 --- a/tests/output/custom/oh-yeah.html +++ b/tests/output/custom/oh-yeah.html @@ -4,11 +4,9 @@ Oh yeah ! - + + - - - @@ -22,138 +20,96 @@ - - Fork me on GitHub - - - -
    -
    -

    Oh yeah !

    -
    -

    Why not ?

    After all, why not ? It's pretty simple to do it, and it will allow me to write my blogposts in rst ! YEAH !

    -alternate text +alternate text
    -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/pages/this-is-a-test-hidden-page.html b/tests/output/custom/pages/this-is-a-test-hidden-page.html new file mode 100644 index 00000000..5c3b1da1 --- /dev/null +++ b/tests/output/custom/pages/this-is-a-test-hidden-page.html @@ -0,0 +1,87 @@ + + + + This is a test hidden page + + + + + + + + + + + + + + + +Fork me on GitHub + + + +
    +

    This is a test hidden page

    + +

    This is great for things like error(404) pages +Anyone can see this page but it's not linked to anywhere!

    + +
    +
    +
    +

    blogroll

    + +
    + +
    + + + + + + \ No newline at end of file diff --git a/tests/output/custom/pages/this-is-a-test-page.html b/tests/output/custom/pages/this-is-a-test-page.html index 27d6ec69..5df98726 100644 --- a/tests/output/custom/pages/this-is-a-test-page.html +++ b/tests/output/custom/pages/this-is-a-test-page.html @@ -3,114 +3,77 @@ This is a test page - - + + + - - - + + + - - Fork me on GitHub - -

    This is a test page

    - -

    Just an image.

    + +

    Just an image.

    alternate text
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/second-article-fr.html b/tests/output/custom/second-article-fr.html index b3b12af7..95ad3bd7 100644 --- a/tests/output/custom/second-article-fr.html +++ b/tests/output/custom/second-article-fr.html @@ -4,11 +4,9 @@ Deuxième article - + + - - - @@ -22,133 +20,91 @@ - - Fork me on GitHub - - - -
    -
    -

    Deuxième article

    -
    - -

    Ceci est un article, en français.

    - -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/second-article.html b/tests/output/custom/second-article.html index 4b31dc69..9c2b6de3 100644 --- a/tests/output/custom/second-article.html +++ b/tests/output/custom/second-article.html @@ -4,11 +4,9 @@ Second article - + + - - - @@ -22,133 +20,91 @@ - - Fork me on GitHub - - - -
    -
    -

    Second article

    -
    - -

    This is some article, in english

    - -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/bar.html b/tests/output/custom/tag/bar.html index bf468bf2..85bc5a07 100644 --- a/tests/output/custom/tag/bar.html +++ b/tests/output/custom/tag/bar.html @@ -3,103 +3,68 @@ Alexis' log - bar - - + + + - - - + + + - - Fork me on GitHub - - - - - - +

    Other articles


      - - - - - - + - - - -
    1. + +
    2. - - - - + - - - -
    3. + +
    4. - - - - + - - - -
    -
    - - - - -
    - -
    + +

    + Page 1 / 1 +

    +
    +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/baz.html b/tests/output/custom/tag/baz.html index 34bcdbc3..3064aa34 100644 --- a/tests/output/custom/tag/baz.html +++ b/tests/output/custom/tag/baz.html @@ -3,103 +3,68 @@ Alexis' log - baz - - + + + - - - + + + - - Fork me on GitHub - - - - - - +

    Other articles


      - - - - - - + - - - -
    -
    - - - - -
    - -
    + +

    + Page 1 / 1 +

    +
    +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/foo.html b/tests/output/custom/tag/foo.html index c8f088f1..c6e99849 100644 --- a/tests/output/custom/tag/foo.html +++ b/tests/output/custom/tag/foo.html @@ -3,103 +3,68 @@ Alexis' log - foo - - + + + - - - + + + - - Fork me on GitHub - - - - - - +

    Other articles


      - - - - - - + - - - -
    1. + +
    2. - - - - + - - - -
    -
    - - - - -
    - -
    + +

    + Page 1 / 1 +

    +
    +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/foobar.html b/tests/output/custom/tag/foobar.html index 682a9b7d..b7a257e4 100644 --- a/tests/output/custom/tag/foobar.html +++ b/tests/output/custom/tag/foobar.html @@ -3,75 +3,52 @@ Alexis' log - foobar - - + + + - - - + + + - - Fork me on GitHub - - - - - - + + +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/oh.html b/tests/output/custom/tag/oh.html index 9e8239a4..451b1031 100644 --- a/tests/output/custom/tag/oh.html +++ b/tests/output/custom/tag/oh.html @@ -3,162 +3,100 @@ Alexis' log - oh - - + + + - - - + + + - - Fork me on GitHub - - - - - - + + +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/tag/yeah.html b/tests/output/custom/tag/yeah.html index 675a53cb..151eb64c 100644 --- a/tests/output/custom/tag/yeah.html +++ b/tests/output/custom/tag/yeah.html @@ -3,162 +3,100 @@ Alexis' log - yeah - - + + + - - - + + + - - Fork me on GitHub - - - - - - + + +
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/theme/css/main.css b/tests/output/custom/theme/css/main.css index 28c98b99..29cce82c 100644 --- a/tests/output/custom/theme/css/main.css +++ b/tests/output/custom/theme/css/main.css @@ -70,9 +70,6 @@ p {margin-bottom: 1.143em;} strong, b {font-weight: bold;} em, i {font-style: italic;} -::-moz-selection {background: #F6CF74; color: #fff;} -::selection {background: #F6CF74; color: #fff;} - /* Lists */ ul { list-style: outside disc; @@ -100,7 +97,7 @@ dl {margin: 0 0 1.5em 0;} dt {font-weight: bold;} dd {margin-left: 1.5em;} -pre{background-color: #000; padding: 10px; color: #fff; margin: 10px; overflow: auto;} +pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;} /* Quotes */ blockquote { @@ -111,6 +108,13 @@ cite {} q {} +div.note { + float: right; + margin: 5px; + font-size: 85%; + max-width: 300px; +} + /* Tables */ table {margin: .5em auto 1.5em auto; width: 98%;} @@ -137,8 +141,8 @@ aside, nav, article, figure { /***** Layout *****/ .body {clear: both; margin: 0 auto; width: 800px;} -img.right figure.right {float: right; margin: 0 0 2em 2em;} -img.left, figure.left {float: right; margin: 0 0 2em 2em;} +img.right, figure.right {float: right; margin: 0 0 2em 2em;} +img.left, figure.left {float: left; margin: 0 2em 2em 0;} /* Header @@ -156,7 +160,6 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} font-weight: bold; margin: 0 0 .6em .2em; text-decoration: none; - width: 427px; } #banner h1 a:hover, #banner h1 a:active { background: none; @@ -305,6 +308,11 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;} .social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');} .social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');} .social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');} + .social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');} + .social a[href*='github.com'], + .social a[href*='git.io'] {background-image: url('../images/icons/github.png');} + .social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');} + .social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');} /* About diff --git a/tests/output/custom/theme/css/pygment.css b/tests/output/custom/theme/css/pygment.css index 594b0fa3..fdd056f6 100644 --- a/tests/output/custom/theme/css/pygment.css +++ b/tests/output/custom/theme/css/pygment.css @@ -1,5 +1,5 @@ .hll { -background-color:#FFFFCC; +background-color:#eee; } .c { color:#408090; diff --git a/tests/output/custom/theme/css/wide.css b/tests/output/custom/theme/css/wide.css index 3376f4c7..88fd59ce 100644 --- a/tests/output/custom/theme/css/wide.css +++ b/tests/output/custom/theme/css/wide.css @@ -4,13 +4,17 @@ body { font:1.3em/1.3 "Hoefler Text","Georgia",Georgia,serif,sans-serif; } -.body, #banner nav, #banner nav ul, #about, #featured, #content{ - width: inherit; +.post-info{ + display: none; } #banner nav { + display: none; -moz-border-radius: 0px; - margin-bottom: 0px; + margin-bottom: 20px; + overflow: hidden; + font-size: 1em; + background: #F5F4EF; } #banner nav ul{ @@ -19,10 +23,11 @@ body { #banner nav li{ float: right; + color: #000; } -#banner nav li:first-child a { - -moz-border-radius: 0px; +#banner nav li a { + color: #000; } #banner h1 { diff --git a/tests/output/custom/theme/images/icons/delicious.png b/tests/output/custom/theme/images/icons/delicious.png index c6ce246a..3dccdd84 100644 Binary files a/tests/output/custom/theme/images/icons/delicious.png and b/tests/output/custom/theme/images/icons/delicious.png differ diff --git a/tests/output/custom/theme/images/icons/facebook.png b/tests/output/custom/theme/images/icons/facebook.png new file mode 100644 index 00000000..74e7ad52 Binary files /dev/null and b/tests/output/custom/theme/images/icons/facebook.png differ diff --git a/tests/output/custom/theme/images/icons/github.png b/tests/output/custom/theme/images/icons/github.png new file mode 100644 index 00000000..6c52b486 Binary files /dev/null and b/tests/output/custom/theme/images/icons/github.png differ diff --git a/tests/output/custom/theme/images/icons/gitorious.png b/tests/output/custom/theme/images/icons/gitorious.png new file mode 100644 index 00000000..3eeb3ece Binary files /dev/null and b/tests/output/custom/theme/images/icons/gitorious.png differ diff --git a/tests/output/custom/theme/images/icons/gittip.png b/tests/output/custom/theme/images/icons/gittip.png new file mode 100644 index 00000000..af949625 Binary files /dev/null and b/tests/output/custom/theme/images/icons/gittip.png differ diff --git a/tests/output/custom/theme/images/icons/google-plus.png b/tests/output/custom/theme/images/icons/google-plus.png new file mode 100644 index 00000000..3c6b7432 Binary files /dev/null and b/tests/output/custom/theme/images/icons/google-plus.png differ diff --git a/tests/output/custom/theme/images/icons/lastfm.png b/tests/output/custom/theme/images/icons/lastfm.png index b09c7876..3a6c6262 100644 Binary files a/tests/output/custom/theme/images/icons/lastfm.png and b/tests/output/custom/theme/images/icons/lastfm.png differ diff --git a/tests/output/custom/theme/images/icons/linkedin.png b/tests/output/custom/theme/images/icons/linkedin.png index feb04962..d29c1201 100644 Binary files a/tests/output/custom/theme/images/icons/linkedin.png and b/tests/output/custom/theme/images/icons/linkedin.png differ diff --git a/tests/output/custom/theme/images/icons/rss.png b/tests/output/custom/theme/images/icons/rss.png index 7d4e85d9..7862c65a 100644 Binary files a/tests/output/custom/theme/images/icons/rss.png and b/tests/output/custom/theme/images/icons/rss.png differ diff --git a/tests/output/custom/theme/images/icons/twitter.png b/tests/output/custom/theme/images/icons/twitter.png index d6119280..d0ef3cc1 100644 Binary files a/tests/output/custom/theme/images/icons/twitter.png and b/tests/output/custom/theme/images/icons/twitter.png differ diff --git a/tests/output/custom/this-is-a-super-article.html b/tests/output/custom/this-is-a-super-article.html index 2fd6b306..98d22351 100644 --- a/tests/output/custom/this-is-a-super-article.html +++ b/tests/output/custom/this-is-a-super-article.html @@ -4,11 +4,9 @@ This is a super article ! - + + - - - @@ -22,60 +20,44 @@ - - Fork me on GitHub - - - -
    -
    -

    This is a super article !

    -
    -

    Some content here !

    This is a simple title

    And here comes the cool stuff.

    -alternate text -alternate text +alternate text +alternate text
     >>> from ipdb import set_trace
     >>> set_trace()
    @@ -83,78 +65,55 @@
     

    → And now try with some utf8 hell: ééé

    -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/output/custom/unbelievable.html b/tests/output/custom/unbelievable.html index b7730421..691f28f6 100644 --- a/tests/output/custom/unbelievable.html +++ b/tests/output/custom/unbelievable.html @@ -4,11 +4,9 @@ Unbelievable ! - + + - - - @@ -22,128 +20,91 @@ - - Fork me on GitHub - - - -
    -
    -

    Unbelievable !

    -
    - -

    Or completely awesome. Depends the needs.

    - -
    - +
    -

    Comments !

    -
    - -
    - - -
    +

    Comments !

    +
    + + + +
    -
    - -
    + - - -
    + - - - - \ No newline at end of file diff --git a/tests/support.py b/tests/support.py index 994cd509..209cc665 100644 --- a/tests/support.py +++ b/tests/support.py @@ -1,14 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function __all__ = [ - 'temporary_folder', 'get_article', 'unittest', ] import os -import subprocess import re +import subprocess import sys -import cStringIO +from six import StringIO +import logging +from logging.handlers import BufferingHandler from functools import wraps from contextlib import contextmanager @@ -16,6 +19,7 @@ from tempfile import mkdtemp from shutil import rmtree from pelican.contents import Article +from pelican.settings import _DEFAULT_CONFIG try: import unittest2 as unittest @@ -99,7 +103,7 @@ def mute(returns_output=False): def wrapper(*args, **kwargs): saved_stdout = sys.stdout - sys.stdout = cStringIO.StringIO() + sys.stdout = StringIO() try: out = func(*args, **kwargs) @@ -115,7 +119,6 @@ def mute(returns_output=False): return decorator - def get_article(title, slug, content, lang, extra_metadata=None): metadata = {'slug': slug, 'title': title, 'lang': lang} if extra_metadata is not None: @@ -123,19 +126,74 @@ def get_article(title, slug, content, lang, extra_metadata=None): return Article(content, metadata=metadata) -def skipIfNoExecutable(executable, valid_exit_code=1): - """Tries to run an executable to make sure it's in the path, Skips the tests - if not found. +def skipIfNoExecutable(executable): + """Skip test if `executable` is not found + + Tries to run `executable` with subprocess to make sure it's in the path, + and skips the tests if not found (if subprocess raises a `OSError`). """ - # calling with no params the command should exit with 1 with open(os.devnull, 'w') as fnull: try: res = subprocess.call(executable, stdout=fnull, stderr=fnull) except OSError: res = None - if res != valid_exit_code: - return unittest.skip('{0} compiler not found'.format(executable)) + if res is None: + return unittest.skip('{0} executable not found'.format(executable)) return lambda func: func + + +def module_exists(module_name): + """Test if a module is importable.""" + + try: + __import__(module_name) + except ImportError: + return False + else: + return True + + +def get_settings(): + settings = _DEFAULT_CONFIG.copy() + settings['DIRECT_TEMPLATES'] = ['archives'] + settings['filenames'] = {} + return settings + + +class LogCountHandler(BufferingHandler): + """ + Capturing and counting logged messages. + """ + + def __init__(self, capacity=1000): + logging.handlers.BufferingHandler.__init__(self, capacity) + + def count_logs(self, msg=None, level=None): + return len([l for l in self.buffer + if (msg is None or re.match(msg, l.getMessage())) + and (level is None or l.levelno == level) + ]) + + +class LoggedTestCase(unittest.TestCase): + """A test case that captures log messages + """ + + def setUp(self): + super(LoggedTestCase, self).setUp() + self._logcount_handler = LogCountHandler() + logging.getLogger().addHandler(self._logcount_handler) + + def tearDown(self): + logging.getLogger().removeHandler(self._logcount_handler) + super(LoggedTestCase, self).tearDown() + + def assertLogCountEqual(self, count=None, msg=None, **kwargs): + actual = self._logcount_handler.count_logs(msg=msg, **kwargs) + self.assertEqual( + actual, count, + msg='expected {} occurrences of {!r}, but found {}'.format( + count, msg, actual)) diff --git a/tests/test_contents.py b/tests/test_contents.py index e7c9ad01..9bb77ec3 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals from .support import unittest -from pelican.contents import Page +from pelican.contents import Page, Article, URLWrapper from pelican.settings import _DEFAULT_CONFIG from pelican.utils import truncate_html_words - +from pelican.signals import content_object_init from jinja2.utils import generate_lorem_ipsum # generate one paragraph, enclosed with

    @@ -19,6 +20,9 @@ class TestPage(unittest.TestCase): super(TestPage, self).setUp() self.page_kwargs = { 'content': TEST_CONTENT, + 'context': { + 'localsiteurl': '', + }, 'metadata': { 'summary': TEST_SUMMARY, 'title': 'foo bar', @@ -32,7 +36,8 @@ class TestPage(unittest.TestCase): """ metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', } - page = Page(TEST_CONTENT, metadata=metadata) + page = Page(TEST_CONTENT, metadata=metadata, + context={'localsiteurl': ''}) for key, value in metadata.items(): self.assertTrue(hasattr(page, key)) self.assertEqual(value, getattr(page, key)) @@ -40,8 +45,11 @@ class TestPage(unittest.TestCase): def test_mandatory_properties(self): """If the title is not set, must throw an exception.""" - self.assertRaises(AttributeError, Page, 'content') - page = Page(**self.page_kwargs) + page = Page('content') + with self.assertRaises(NameError) as cm: + page.check_properties() + + page = Page('content', metadata={'title': 'foobar'}) page.check_properties() def test_summary_from_metadata(self): @@ -92,6 +100,16 @@ class TestPage(unittest.TestCase): page = Page(**self.page_kwargs) self.assertEqual(page.save_as, "pages/foo-bar-fr.html") + def test_metadata_url_format(self): + """Arbitrary metadata should be passed through url_format() + """ + page = Page(**self.page_kwargs) + self.assertIn('summary', page.url_format.keys()) + page.metadata['directory'] = 'test-dir' + page.settings = _DEFAULT_CONFIG.copy() + page.settings['PAGE_SAVE_AS'] = '{directory}/{slug}' + self.assertEqual(page.save_as, 'test-dir/foo-bar') + def test_datetime(self): """If DATETIME is set to a tuple, it should be used to override LOCALE """ @@ -106,8 +124,8 @@ class TestPage(unittest.TestCase): page = Page(**page_kwargs) self.assertEqual(page.locale_date, - unicode(dt.strftime(_DEFAULT_CONFIG['DEFAULT_DATE_FORMAT']), - 'utf-8')) + dt.strftime(_DEFAULT_CONFIG['DEFAULT_DATE_FORMAT'])) + page_kwargs['settings'] = dict([(x, _DEFAULT_CONFIG[x]) for x in _DEFAULT_CONFIG]) @@ -124,7 +142,7 @@ class TestPage(unittest.TestCase): import locale as locale_module try: page = Page(**page_kwargs) - self.assertEqual(page.locale_date, u'2015-09-13(\u65e5)') + self.assertEqual(page.locale_date, '2015-09-13(\u65e5)') except locale_module.Error: # The constructor of ``Page`` will try to set the locale to # ``ja_JP.utf8``. But this attempt will failed when there is no @@ -135,6 +153,17 @@ class TestPage(unittest.TestCase): # will simply skip this test. unittest.skip("There is no locale %s in this system." % locale) + def test_template(self): + """ + Pages default to page, metadata overwrites + """ + default_page = Page(**self.page_kwargs) + self.assertEqual('page', default_page.template) + page_kwargs = self._copy_page_kwargs() + page_kwargs['metadata']['template'] = 'custom' + custom_page = Page(**page_kwargs) + self.assertEqual('custom', custom_page.template) + def _copy_page_kwargs(self): # make a deep copy of page_kwargs page_kwargs = dict([(key, self.page_kwargs[key]) for key in @@ -146,3 +175,54 @@ class TestPage(unittest.TestCase): for subkey in page_kwargs[key]]) return page_kwargs + + def test_signal(self): + """If a title is given, it should be used to generate the slug.""" + + def receiver_test_function(sender,instance): + pass + + content_object_init.connect(receiver_test_function ,sender=Page) + page = Page(**self.page_kwargs) + self.assertTrue(content_object_init.has_receivers_for(Page)) + + +class TestArticle(TestPage): + def test_template(self): + """ + Articles default to article, metadata overwrites + """ + default_article = Article(**self.page_kwargs) + self.assertEqual('article', default_article.template) + article_kwargs = self._copy_page_kwargs() + article_kwargs['metadata']['template'] = 'custom' + custom_article = Article(**article_kwargs) + self.assertEqual('custom', custom_article.template) + + +class TestURLWrapper(unittest.TestCase): + def test_comparisons(self): + """URLWrappers are sorted by name + """ + wrapper_a = URLWrapper(name='first', settings={}) + wrapper_b = URLWrapper(name='last', settings={}) + self.assertFalse(wrapper_a > wrapper_b) + self.assertFalse(wrapper_a >= wrapper_b) + self.assertFalse(wrapper_a == wrapper_b) + self.assertTrue(wrapper_a != wrapper_b) + self.assertTrue(wrapper_a <= wrapper_b) + self.assertTrue(wrapper_a < wrapper_b) + wrapper_b.name = 'first' + self.assertFalse(wrapper_a > wrapper_b) + self.assertTrue(wrapper_a >= wrapper_b) + self.assertTrue(wrapper_a == wrapper_b) + self.assertFalse(wrapper_a != wrapper_b) + self.assertTrue(wrapper_a <= wrapper_b) + self.assertFalse(wrapper_a < wrapper_b) + wrapper_a.name = 'last' + self.assertTrue(wrapper_a > wrapper_b) + self.assertTrue(wrapper_a >= wrapper_b) + self.assertFalse(wrapper_a == wrapper_b) + self.assertTrue(wrapper_a != wrapper_b) + self.assertFalse(wrapper_a <= wrapper_b) + self.assertFalse(wrapper_a < wrapper_b) diff --git a/tests/test_generators.py b/tests/test_generators.py index 61f31696..54fe7e61 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,66 +1,120 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function from mock import MagicMock import os -import re -from pelican.generators import ArticlesGenerator, LessCSSGenerator, PagesGenerator +from codecs import open +from tempfile import mkdtemp +from shutil import rmtree + +from pelican.generators import ArticlesGenerator, PagesGenerator, \ + TemplatePagesGenerator +from pelican.writers import Writer from pelican.settings import _DEFAULT_CONFIG -from .support import unittest, temporary_folder, skipIfNoExecutable +from .support import unittest, get_settings CUR_DIR = os.path.dirname(__file__) class TestArticlesGenerator(unittest.TestCase): - def test_generate_feeds(self): + def setUp(self): + super(TestArticlesGenerator, self).setUp() + self.generator = None - generator = ArticlesGenerator(None, {'FEED': _DEFAULT_CONFIG['FEED']}, - None, _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + def get_populated_generator(self): + """ + We only need to pull all the test articles once, but read from it + for each test. + """ + if self.generator is None: + settings = get_settings() + settings['ARTICLE_DIR'] = 'content' + settings['DEFAULT_CATEGORY'] = 'Default' + settings['DEFAULT_DATE'] = (1970, 1, 1) + self.generator = ArticlesGenerator(settings.copy(), settings, + CUR_DIR, settings['THEME'], None, + settings['MARKUP']) + self.generator.generate_context() + return self.generator + + def distill_articles(self, articles): + distilled = [] + for page in articles: + distilled.append([ + page.title, + page.status, + page.category.name, + page.template + ] + ) + return distilled + + def test_generate_feeds(self): + settings = get_settings() + generator = ArticlesGenerator(settings, + {'FEED_ALL_ATOM': settings['FEED_ALL_ATOM']}, None, + settings['THEME'], None, settings['MARKUP']) writer = MagicMock() generator.generate_feeds(writer) - writer.write_feed.assert_called_with([], None, 'feeds/all.atom.xml') + writer.write_feed.assert_called_with([], settings, 'feeds/all.atom.xml') - generator = ArticlesGenerator(None, {'FEED': None}, None, - _DEFAULT_CONFIG['THEME'], None, None) + generator = ArticlesGenerator(settings, {'FEED_ALL_ATOM': None}, None, + settings['THEME'], None, None) writer = MagicMock() generator.generate_feeds(writer) self.assertFalse(writer.write_feed.called) def test_generate_context(self): + generator = self.get_populated_generator() + articles = self.distill_articles(generator.articles) + articles_expected = [ + ['Article title', 'published', 'Default', 'article'], + ['Article with markdown and summary metadata single', 'published', 'Default', 'article'], + ['Article with markdown and summary metadata multi', 'published', 'Default', 'article'], + ['Article with template', 'published', 'Default', 'custom'], + ['Test md File', 'published', 'test', 'article'], + ['Rst with filename metadata', 'published', 'yeah', 'article'], + ['Test Markdown extensions', 'published', 'Default', 'article'], + ['This is a super article !', 'published', 'Yeah', 'article'], + ['This is an article with category !', 'published', 'yeah', 'article'], + ['This is an article without category !', 'published', 'Default', 'article'], + ['This is an article without category !', 'published', 'TestCategory', 'article'], + ['This is a super article !', 'published', 'yeah', 'article'] + ] + self.assertItemsEqual(articles_expected, articles) + + def test_generate_categories(self): + + generator = self.get_populated_generator() + categories = [cat.name for cat, _ in generator.categories] + categories_expected = ['Default', 'TestCategory', 'Yeah', 'test', 'yeah'] + self.assertEquals(categories, categories_expected) + + def test_do_not_use_folder_as_category(self): + settings = _DEFAULT_CONFIG.copy() settings['ARTICLE_DIR'] = 'content' settings['DEFAULT_CATEGORY'] = 'Default' - generator = ArticlesGenerator(settings.copy(), settings, CUR_DIR, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + settings['DEFAULT_DATE'] = (1970, 1, 1) + settings['USE_FOLDER_AS_CATEGORY'] = False + settings['filenames'] = {} + generator = ArticlesGenerator(settings.copy(), settings, + CUR_DIR, _DEFAULT_CONFIG['THEME'], None, + _DEFAULT_CONFIG['MARKUP']) generator.generate_context() - for article in generator.articles: - relfilepath = os.path.relpath(article.filename, CUR_DIR) - if relfilepath == os.path.join("TestCategory", - "article_with_category.rst"): - self.assertEquals(article.category.name, 'yeah') - elif relfilepath == os.path.join("TestCategory", - "article_without_category.rst"): - self.assertEquals(article.category.name, 'TestCategory') - elif relfilepath == "article_without_category.rst": - self.assertEquals(article.category.name, 'Default') categories = [cat.name for cat, _ in generator.categories] - # assert that the categories are ordered as expected - self.assertEquals( - categories, ['Default', 'TestCategory', 'Yeah', 'test', - 'yeah']) + self.assertEquals(categories, ['Default', 'Yeah', 'test', 'yeah']) def test_direct_templates_save_as_default(self): - settings = _DEFAULT_CONFIG.copy() - settings['DIRECT_TEMPLATES'] = ['archives'] - generator = ArticlesGenerator(settings.copy(), settings, None, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + settings = get_settings() + generator = ArticlesGenerator(settings, settings, None, + settings['THEME'], None, + settings['MARKUP']) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives.html", @@ -69,12 +123,12 @@ class TestArticlesGenerator(unittest.TestCase): def test_direct_templates_save_as_modified(self): - settings = _DEFAULT_CONFIG.copy() + settings = get_settings() settings['DIRECT_TEMPLATES'] = ['archives'] settings['ARCHIVES_SAVE_AS'] = 'archives/index.html' generator = ArticlesGenerator(settings, settings, None, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + settings['THEME'], None, + settings['MARKUP']) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_with("archives/index.html", @@ -83,16 +137,26 @@ class TestArticlesGenerator(unittest.TestCase): def test_direct_templates_save_as_false(self): - settings = _DEFAULT_CONFIG.copy() + settings = get_settings() settings['DIRECT_TEMPLATES'] = ['archives'] settings['ARCHIVES_SAVE_AS'] = 'archives/index.html' generator = ArticlesGenerator(settings, settings, None, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + settings['THEME'], None, + settings['MARKUP']) write = MagicMock() generator.generate_direct_templates(write) write.assert_called_count == 0 + def test_per_article_template(self): + """ + Custom template articles get the field but standard/unset are None + """ + generator = self.get_populated_generator() + articles = self.distill_articles(generator.articles) + custom_template = ['Article with template', 'published', 'Default', 'custom'] + standard_template = ['This is a super article !', 'published', 'Yeah', 'article'] + self.assertIn(custom_template, articles) + self.assertIn(standard_template, articles) class TestPageGenerator(unittest.TestCase): """ @@ -107,77 +171,78 @@ class TestPageGenerator(unittest.TestCase): for page in pages: distilled.append([ page.title, - page.status + page.status, + page.template ] ) return distilled def test_generate_context(self): - settings = _DEFAULT_CONFIG.copy() - + settings = get_settings() settings['PAGE_DIR'] = 'TestPages' + settings['DEFAULT_DATE'] = (1970, 1, 1) + generator = PagesGenerator(settings.copy(), settings, CUR_DIR, - _DEFAULT_CONFIG['THEME'], None, - _DEFAULT_CONFIG['MARKUP']) + settings['THEME'], None, + settings['MARKUP']) generator.generate_context() pages = self.distill_pages(generator.pages) hidden_pages = self.distill_pages(generator.hidden_pages) pages_expected = [ - [u'This is a test page', 'published'], - [u'This is a markdown test page', 'published'] + ['This is a test page', 'published', 'page'], + ['This is a markdown test page', 'published', 'page'], + ['This is a test page with a preset template', 'published', 'custom'] ] hidden_pages_expected = [ - [u'This is a test hidden page', 'hidden'], - [u'This is a markdown test hidden page', 'hidden'] + ['This is a test hidden page', 'hidden', 'page'], + ['This is a markdown test hidden page', 'hidden', 'page'], + ['This is a test hidden page with a custom template', 'hidden', 'custom'] ] self.assertItemsEqual(pages_expected,pages) self.assertItemsEqual(hidden_pages_expected,hidden_pages) -class TestLessCSSGenerator(unittest.TestCase): +class TestTemplatePagesGenerator(unittest.TestCase): - LESS_CONTENT = """ - @color: #4D926F; + TEMPLATE_CONTENT = "foo: {{ foo }}" - #header { - color: @color; - } - h2 { - color: @color; - } - """ + def setUp(self): + self.temp_content = mkdtemp() + self.temp_output = mkdtemp() - @skipIfNoExecutable('lessc') - def test_less_compiler(self): + def tearDown(self): + rmtree(self.temp_content) + rmtree(self.temp_output) - settings = _DEFAULT_CONFIG.copy() + def test_generate_output(self): + + settings = get_settings() settings['STATIC_PATHS'] = ['static'] - settings['LESS_GENERATOR'] = True + settings['TEMPLATE_PAGES'] = { + 'template/source.html': 'generated/file.html' + } - # we'll nest here for py < 2.7 compat - with temporary_folder() as temp_content: - with temporary_folder() as temp_output: - generator = LessCSSGenerator(None, settings, temp_content, - _DEFAULT_CONFIG['THEME'], temp_output, None) + generator = TemplatePagesGenerator({'foo': 'bar'}, settings, + self.temp_content, '', self.temp_output, None) - # create a dummy less file - less_dir = os.path.join(temp_content, 'static', 'css') - less_filename = os.path.join(less_dir, 'test.less') + # create a dummy template file + template_dir = os.path.join(self.temp_content, 'template') + template_path = os.path.join(template_dir, 'source.html') + os.makedirs(template_dir) + with open(template_path, 'w') as template_file: + template_file.write(self.TEMPLATE_CONTENT) - less_output = os.path.join(temp_output, 'static', 'css', - 'test.css') + writer = Writer(self.temp_output, settings=settings) + generator.generate_output(writer) - os.makedirs(less_dir) - with open(less_filename, 'w') as less_file: - less_file.write(self.LESS_CONTENT) + output_path = os.path.join( + self.temp_output, 'generated', 'file.html') - generator.generate_output() + # output file has been generated + self.assertTrue(os.path.exists(output_path)) - # we have the file ? - self.assertTrue(os.path.exists(less_output)) - - # was it compiled ? - self.assertIsNotNone(re.search(r'^\s+color:\s*#4D926F;$', - open(less_output).read(), re.MULTILINE | re.IGNORECASE)) + # output content is correct + with open(output_path, 'r') as output_file: + self.assertEquals(output_file.read(), 'foo: bar') diff --git a/tests/test_importer.py b/tests/test_importer.py index 5504b12e..a79cd493 100644 --- a/tests/test_importer.py +++ b/tests/test_importer.py @@ -1,27 +1,27 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function import os from pelican.tools.pelican_import import wp2fields, fields2pelican -from .support import unittest, temporary_folder, mute +from .support import unittest, temporary_folder, mute, skipIfNoExecutable CUR_DIR = os.path.dirname(__file__) WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml') -PANDOC = os.system('pandoc --version') == 0 try: - import BeautifulSoup + from bs4 import BeautifulSoup except ImportError: BeautifulSoup = False # NOQA +@skipIfNoExecutable(['pandoc', '--version']) +@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') class TestWordpressXmlImporter(unittest.TestCase): def setUp(self): self.posts = wp2fields(WORDPRESS_XML_SAMPLE) - @unittest.skipUnless(PANDOC and BeautifulSoup, - 'Needs Pandoc and BeautifulSoup') def test_ignore_empty_posts(self): posts = list(self.posts) @@ -29,8 +29,6 @@ class TestWordpressXmlImporter(unittest.TestCase): for title, content, fname, date, author, categ, tags, format in posts: self.assertTrue(title.strip()) - @unittest.skipUnless(PANDOC and BeautifulSoup, - 'Needs Pandoc and BeautifulSoup') def test_can_toggle_raw_html_code_parsing(self): posts = list(self.posts) @@ -50,3 +48,13 @@ class TestWordpressXmlImporter(unittest.TestCase): rst_files = (r(f) for f in silent_f2p(posts, 'rst', temp, strip_raw=True)) self.assertFalse(any(' entities in the title. You can't miss them.") + self.assertTrue('&' not in title) diff --git a/tests/test_pelican.py b/tests/test_pelican.py index c317e5b3..49e20b0a 100644 --- a/tests/test_pelican.py +++ b/tests/test_pelican.py @@ -1,15 +1,16 @@ -try: - import unittest2 as unittest -except ImportError: - import unittest # NOQA +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function import os from filecmp import dircmp - -from .support import temporary_folder +from tempfile import mkdtemp +from shutil import rmtree +import locale +import logging from pelican import Pelican from pelican.settings import read_settings +from .support import LoggedTestCase CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) SAMPLES_PATH = os.path.abspath(os.sep.join((CURRENT_DIR, "..", "samples"))) @@ -19,31 +20,73 @@ INPUT_PATH = os.path.join(SAMPLES_PATH, "content") SAMPLE_CONFIG = os.path.join(SAMPLES_PATH, "pelican.conf.py") -class TestPelican(unittest.TestCase): +def recursiveDiff(dcmp): + diff = { + 'diff_files': [os.sep.join((dcmp.right, f)) + for f in dcmp.diff_files], + 'left_only': [os.sep.join((dcmp.right, f)) + for f in dcmp.left_only], + 'right_only': [os.sep.join((dcmp.right, f)) + for f in dcmp.right_only], + } + for sub_dcmp in dcmp.subdirs.values(): + for k, v in recursiveDiff(sub_dcmp).items(): + diff[k] += v + return diff + + +class TestPelican(LoggedTestCase): # general functional testing for pelican. Basically, this test case tries # to run pelican in different situations and see how it behaves - @unittest.skip("Test failing") + def setUp(self): + super(TestPelican, self).setUp() + self.temp_path = mkdtemp() + self.old_locale = locale.setlocale(locale.LC_ALL) + locale.setlocale(locale.LC_ALL, str('C')) + + def tearDown(self): + rmtree(self.temp_path) + locale.setlocale(locale.LC_ALL, self.old_locale) + super(TestPelican, self).tearDown() + + def assertFilesEqual(self, diff): + msg = "some generated files differ from the expected functional " \ + "tests output.\n" \ + "This is probably because the HTML generated files " \ + "changed. If these changes are normal, please refer " \ + "to docs/contribute.rst to update the expected " \ + "output of the functional tests." + + self.assertEqual(diff['left_only'], [], msg=msg) + self.assertEqual(diff['right_only'], [], msg=msg) + self.assertEqual(diff['diff_files'], [], msg=msg) + def test_basic_generation_works(self): # when running pelican without settings, it should pick up the default - # ones and generate the output without raising any exception / issuing - # any warning. - with temporary_folder() as temp_path: - pelican = Pelican(path=INPUT_PATH, output_path=temp_path) - pelican.run() - diff = dircmp(temp_path, os.sep.join((OUTPUT_PATH, "basic"))) - self.assertEqual(diff.left_only, []) - self.assertEqual(diff.right_only, []) - self.assertEqual(diff.diff_files, []) + # ones and generate correct output without raising any exception + settings = read_settings(path=None, override={ + 'PATH': INPUT_PATH, + 'OUTPUT_PATH': self.temp_path, + 'LOCALE': locale.normalize('en_US'), + }) + pelican = Pelican(settings=settings) + pelican.run() + dcmp = dircmp(self.temp_path, os.sep.join((OUTPUT_PATH, "basic"))) + self.assertFilesEqual(recursiveDiff(dcmp)) + self.assertLogCountEqual( + count=10, + msg="Unable to find.*skipping url replacement", + level=logging.WARNING) - @unittest.skip("Test failing") def test_custom_generation_works(self): # the same thing with a specified set of settings should work - with temporary_folder() as temp_path: - pelican = Pelican(path=INPUT_PATH, output_path=temp_path, - settings=read_settings(SAMPLE_CONFIG)) - pelican.run() - diff = dircmp(temp_path, os.sep.join((OUTPUT_PATH, "custom"))) - self.assertEqual(diff.left_only, []) - self.assertEqual(diff.right_only, []) - self.assertEqual(diff.diff_files, []) + settings = read_settings(path=SAMPLE_CONFIG, override={ + 'PATH': INPUT_PATH, + 'OUTPUT_PATH': self.temp_path, + 'LOCALE': locale.normalize('en_US'), + }) + pelican = Pelican(settings=settings) + pelican.run() + dcmp = dircmp(self.temp_path, os.sep.join((OUTPUT_PATH, "custom"))) + self.assertFilesEqual(recursiveDiff(dcmp)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 00000000..0a5be921 --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +'''Core plugins unit tests''' + +import os +import tempfile + +from pelican.plugins import gzip_cache + +from .support import unittest, temporary_folder + +class TestGzipCache(unittest.TestCase): + '''Unit tests for the gzip cache plugin''' + + def test_should_compress(self): + '''Test that some filetypes should compress and others shouldn't.''' + self.assertTrue(gzip_cache.should_compress('foo.html')) + self.assertTrue(gzip_cache.should_compress('bar.css')) + self.assertTrue(gzip_cache.should_compress('baz.js')) + self.assertTrue(gzip_cache.should_compress('foo.txt')) + + self.assertFalse(gzip_cache.should_compress('foo.gz')) + self.assertFalse(gzip_cache.should_compress('bar.png')) + self.assertFalse(gzip_cache.should_compress('baz.mp3')) + self.assertFalse(gzip_cache.should_compress('foo.mov')) + + def test_creates_gzip_file(self): + '''Test that a file matching the input filename with a .gz extension is + created.''' + # The plugin walks over the output content after the finalized signal + # so it is safe to assume that the file exists (otherwise walk would + # not report it). Therefore, create a dummy file to use. + with temporary_folder() as tempdir: + (_, a_html_filename) = tempfile.mkstemp(suffix='.html', dir=tempdir) + gzip_cache.create_gzip_file(a_html_filename) + self.assertTrue(os.path.exists(a_html_filename + '.gz')) + diff --git a/tests/test_readers.py b/tests/test_readers.py index b3e30bfc..8cee4c1a 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -1,4 +1,5 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function import datetime import os @@ -10,7 +11,7 @@ CUR_DIR = os.path.dirname(__file__) CONTENT_PATH = os.path.join(CUR_DIR, 'content') -def _filename(*args): +def _path(*args): return os.path.join(CONTENT_PATH, *args) @@ -18,14 +19,14 @@ class RstReaderTest(unittest.TestCase): def test_article_with_metadata(self): reader = readers.RstReader({}) - content, metadata = reader.read(_filename('article_with_metadata.rst')) + content, metadata = reader.read(_path('article_with_metadata.rst')) expected = { 'category': 'yeah', - 'author': u'Alexis Métaireau', + 'author': 'Alexis Métaireau', 'title': 'This is a super article !', - 'summary': u'

    Multi-line metadata should be'\ - u' supported\nas well as inline'\ - u' markup.

    \n', + 'summary': '

    Multi-line metadata should be'\ + ' supported\nas well as inline'\ + ' markup.

    \n', 'date': datetime.datetime(2010, 12, 2, 10, 14), 'tags': ['foo', 'bar', 'foobar'], 'custom_field': 'http://notmyidea.org', @@ -34,10 +35,54 @@ class RstReaderTest(unittest.TestCase): for key, value in expected.items(): self.assertEquals(value, metadata[key], key) + def test_article_with_filename_metadata(self): + content, metadata = readers.read_file( + _path('2012-11-29_rst_w_filename_meta#foo-bar.rst'), + settings={}) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + 'title': 'Rst with filename metadata', + } + for key, value in metadata.items(): + self.assertEquals(value, expected[key], key) + + content, metadata = readers.read_file( + _path('2012-11-29_rst_w_filename_meta#foo-bar.rst'), + settings={ + 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2}).*' + }) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + 'title': 'Rst with filename metadata', + 'date': datetime.datetime(2012, 11, 29), + } + for key, value in metadata.items(): + self.assertEquals(value, expected[key], key) + + content, metadata = readers.read_file( + _path('2012-11-29_rst_w_filename_meta#foo-bar.rst'), + settings={ + 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2})_' \ + '_(?P.*)' \ + '#(?P.*)-(?P.*)' + }) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + 'title': 'Rst with filename metadata', + 'date': datetime.datetime(2012, 11, 29), + 'slug': 'article_with_filename_metadata', + 'mymeta': 'foo', + } + for key, value in metadata.items(): + self.assertEquals(value, expected[key], key) + def test_article_metadata_key_lowercase(self): """Keys of metadata should be lowercase.""" reader = readers.RstReader({}) - content, metadata = reader.read(_filename('article_with_uppercase_metadata.rst')) + content, metadata = reader.read(_path('article_with_uppercase_metadata.rst')) self.assertIn('category', metadata, "Key should be lowercase.") self.assertEquals('Yeah', metadata.get('category'), "Value keeps cases.") @@ -45,18 +90,22 @@ class RstReaderTest(unittest.TestCase): def test_typogrify(self): # if nothing is specified in the settings, the content should be # unmodified - content, _ = readers.read_file(_filename('article.rst')) + content, _ = readers.read_file(_path('article.rst')) expected = "

    This is some content. With some stuff to "\ - ""typogrify".

    \n" + ""typogrify".

    \n

    Now with added "\ + 'support for '\ + 'TLA.

    \n' self.assertEqual(content, expected) try: # otherwise, typogrify should be applied - content, _ = readers.read_file(_filename('article.rst'), + content, _ = readers.read_file(_path('article.rst'), settings={'TYPOGRIFY': True}) expected = "

    This is some content. With some stuff to "\ - "“typogrify”.

    \n" + "“typogrify”.

    \n

    Now with added "\ + 'support for '\ + 'TLA.

    \n' self.assertEqual(content, expected) except ImportError: @@ -66,25 +115,150 @@ class RstReaderTest(unittest.TestCase): class MdReaderTest(unittest.TestCase): @unittest.skipUnless(readers.Markdown, "markdown isn't installed") - def test_article_with_md_extention(self): - # test to ensure the md extension is being processed by the correct reader + def test_article_with_metadata(self): reader = readers.MarkdownReader({}) - content, metadata = reader.read(_filename('article_with_md_extension.md')) + content, metadata = reader.read( + _path('article_with_md_extension.md')) + expected = { + 'category': 'test', + 'title': 'Test md File', + 'summary': '

    I have a lot to test

    ', + 'date': datetime.datetime(2010, 12, 2, 10, 14), + 'tags': ['foo', 'bar', 'foobar'], + } + for key, value in metadata.items(): + self.assertEquals(value, expected[key], key) + + @unittest.skipUnless(readers.Markdown, "markdown isn't installed") + def test_article_with_file_extensions(self): + reader = readers.MarkdownReader({}) + # test to ensure the md file extension is being processed by the + # correct reader + content, metadata = reader.read( + _path('article_with_md_extension.md')) expected = "

    Test Markdown File Header

    \n"\ "

    Used for pelican test

    \n"\ "

    The quick brown fox jumped over the lazy dog's back.

    " - + self.assertEqual(content, expected) + # test to ensure the mkd file extension is being processed by the + # correct reader + content, metadata = reader.read( + _path('article_with_mkd_extension.mkd')) + expected = "

    Test Markdown File Header

    \n

    Used for pelican"\ + " test

    \n

    This is another markdown test file. Uses"\ + " the mkd extension.

    " + self.assertEqual(content, expected) + # test to ensure the markdown file extension is being processed by the + # correct reader + content, metadata = reader.read( + _path('article_with_markdown_extension.markdown')) + expected = "

    Test Markdown File Header

    \n

    Used for pelican"\ + " test

    \n

    This is another markdown test file. Uses"\ + " the markdown extension.

    " self.assertEqual(content, expected) @unittest.skipUnless(readers.Markdown, "markdown isn't installed") - def test_article_with_mkd_extension(self): - # test to ensure the mkd extension is being processed by the correct reader - reader = readers.MarkdownReader({}) - content, metadata = reader.read(_filename('article_with_mkd_extension.mkd')) - expected = "

    Test Markdown File Header

    \n"\ - "

    Used for pelican test

    \n"\ - "

    This is another markdown test file. Uses the mkd extension.

    " - + def test_article_with_markdown_markup_extension(self): + # test to ensure the markdown markup extension is being processed as expected + content, metadata = readers.read_file( + _path('article_with_markdown_markup_extensions.md'), + settings={'MD_EXTENSIONS': ['toc', 'codehilite', 'extra']}) + expected = '
    \n'\ + '\n'\ + '
    \n'\ + '

    Level1

    \n'\ + '

    Level2

    ' + + self.assertEqual(content, expected) + + @unittest.skipUnless(readers.Markdown, "markdown isn't installed") + def test_article_with_filename_metadata(self): + content, metadata = readers.read_file( + _path('2012-11-30_md_w_filename_meta#foo-bar.md'), + settings={}) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + } + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) + + content, metadata = readers.read_file( + _path('2012-11-30_md_w_filename_meta#foo-bar.md'), + settings={ + 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2}).*' + }) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + 'date': datetime.datetime(2012, 11, 30), + } + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) + + content, metadata = readers.read_file( + _path('2012-11-30_md_w_filename_meta#foo-bar.md'), + settings={ + 'FILENAME_METADATA': '(?P\d{4}-\d{2}-\d{2})' + '_(?P.*)' + '#(?P.*)-(?P.*)' + }) + expected = { + 'category': 'yeah', + 'author': 'Alexis Métaireau', + 'date': datetime.datetime(2012, 11, 30), + 'slug': 'md_w_filename_meta', + 'mymeta': 'foo', + } + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) + +class AdReaderTest(unittest.TestCase): + + @unittest.skipUnless(readers.asciidoc, "asciidoc isn't installed") + def test_article_with_asc_extension(self): + # test to ensure the asc extension is being processed by the correct reader + reader = readers.AsciiDocReader({}) + content, metadata = reader.read(_path('article_with_asc_extension.asc')) + expected = '
    \n

    Used for pelican test

    \n'\ + '

    The quick brown fox jumped over the lazy dog’s back.

    \n' + self.assertEqual(content, expected) + expected = { + 'category': 'Blog', + 'author': 'Author O. Article', + 'title': 'Test AsciiDoc File Header', + 'date': datetime.datetime(2011, 9, 15, 9, 5), + 'tags': ['Linux', 'Python', 'Pelican'], + } + + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) + + + expected = { + 'category': 'Blog', + 'author': 'Author O. Article', + 'title': 'Test AsciiDoc File Header', + 'date': datetime.datetime(2011, 9, 15, 9, 5), + 'tags': ['Linux', 'Python', 'Pelican'], + } + + for key, value in expected.items(): + self.assertEquals(value, metadata[key], key) + + @unittest.skipUnless(readers.asciidoc, "asciidoc isn't installed") + def test_article_with_asc_options(self): + # test to ensure the ASCIIDOC_OPTIONS is being used + reader = readers.AsciiDocReader(dict(ASCIIDOC_OPTIONS=["-a revision=1.0.42"])) + content, metadata = reader.read(_path('article_with_asc_options.asc')) + expected = '
    \n

    Used for pelican test

    \n'\ + '

    version 1.0.42

    \n'\ + '

    The quick brown fox jumped over the lazy dog’s back.

    \n' self.assertEqual(content, expected) class HTMLReaderTest(unittest.TestCase): diff --git a/tests/test_settings.py b/tests/test_settings.py index 25df74bd..0e610c55 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import copy from os.path import dirname, abspath, join -from pelican.settings import read_settings, configure_settings, _DEFAULT_CONFIG +from pelican.settings import read_settings, configure_settings, _DEFAULT_CONFIG, DEFAULT_THEME from .support import unittest @@ -15,7 +18,7 @@ class TestSettingsConfiguration(unittest.TestCase): self.settings = read_settings(default_conf) def test_overwrite_existing_settings(self): - self.assertEqual(self.settings.get('SITENAME'), u"Alexis' log") + self.assertEqual(self.settings.get('SITENAME'), "Alexis' log") self.assertEqual(self.settings.get('SITEURL'), 'http://blog.notmyidea.org') @@ -31,21 +34,43 @@ class TestSettingsConfiguration(unittest.TestCase): def test_read_empty_settings(self): """providing no file should return the default values.""" settings = read_settings(None) - self.assertDictEqual(settings, _DEFAULT_CONFIG) + expected = copy.deepcopy(_DEFAULT_CONFIG) + expected["FEED_DOMAIN"] = '' #This is added by configure settings + self.maxDiff = None + self.assertDictEqual(settings, expected) + + def test_settings_return_independent(self): + """Make sure that the results from one settings call doesn't + effect past or future instances.""" + self.PATH = abspath(dirname(__file__)) + default_conf = join(self.PATH, 'default_conf.py') + settings = read_settings(default_conf) + settings['SITEURL'] = 'new-value' + new_settings = read_settings(default_conf) + self.assertNotEqual(new_settings['SITEURL'], settings['SITEURL']) + + def test_defaults_not_overwritten(self): + """This assumes 'SITENAME': 'A Pelican Blog'""" + settings = read_settings(None) + settings['SITENAME'] = 'Not a Pelican Blog' + self.assertNotEqual(settings['SITENAME'], _DEFAULT_CONFIG['SITENAME']) def test_configure_settings(self): """Manipulations to settings should be applied correctly.""" - # SITEURL should not have a trailing slash - settings = {'SITEURL': 'http://blog.notmyidea.org/', 'LOCALE': ''} + settings = { + 'SITEURL': 'http://blog.notmyidea.org/', + 'LOCALE': '', + 'PATH': '.', + 'THEME': DEFAULT_THEME, + } configure_settings(settings) + # SITEURL should not have a trailing slash self.assertEqual(settings['SITEURL'], 'http://blog.notmyidea.org') # FEED_DOMAIN, if undefined, should default to SITEURL - settings = {'SITEURL': 'http://blog.notmyidea.org', 'LOCALE': ''} - configure_settings(settings) self.assertEqual(settings['FEED_DOMAIN'], 'http://blog.notmyidea.org') - settings = {'FEED_DOMAIN': 'http://feeds.example.com', 'LOCALE': ''} + settings['FEED_DOMAIN'] = 'http://feeds.example.com' configure_settings(settings) self.assertEqual(settings['FEED_DOMAIN'], 'http://feeds.example.com') diff --git a/tests/test_utils.py b/tests/test_utils.py index 2e3f714b..c176325e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,14 +1,32 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function +import logging import shutil import os import datetime import time from pelican import utils -from .support import get_article, unittest +from .support import get_article, LoggedTestCase +from pelican.utils import NoFilesError -class TestUtils(unittest.TestCase): +class TestUtils(LoggedTestCase): + _new_attribute = 'new_value' + + @utils.deprecated_attribute( + old='_old_attribute', new='_new_attribute', + since=(3, 1, 0), remove=(4, 1, 3)) + def _old_attribute(): return None + + def test_deprecated_attribute(self): + value = self._old_attribute + self.assertEquals(value, self._new_attribute) + self.assertLogCountEqual( + count=1, + msg=('_old_attribute has been deprecated since 3.1.0 and will be ' + 'removed by version 4.1.3. Use _new_attribute instead'), + level=logging.WARNING) def test_get_date(self): # valid ones @@ -40,17 +58,19 @@ class TestUtils(unittest.TestCase): samples = (('this is a test', 'this-is-a-test'), ('this is a test', 'this-is-a-test'), - (u'this → is ← a ↑ test', 'this-is-a-test'), - ('this--is---a test', 'this-is-a-test')) + ('this → is ← a ↑ test', 'this-is-a-test'), + ('this--is---a test', 'this-is-a-test'), + ('unicode測試許功蓋,你看到了嗎?', 'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'), + ('大飯原発4号機、18日夜起動へ', 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),) for value, expected in samples: self.assertEquals(utils.slugify(value), expected) def test_get_relative_path(self): - samples = (('/test/test', '../../.'), - ('/test/test/', '../../../.'), - ('/', '../.')) + samples = (('test/test.html', '..'), + ('test/test/test.html', '../..'), + ('test.html', '.')) for value, expected in samples: self.assertEquals(utils.get_relative_path(value), expected) @@ -72,22 +92,35 @@ class TestUtils(unittest.TestCase): self.assertNotIn(fr_article1, index) def test_files_changed(self): - "Test if file changes are correctly detected" + """Test if file changes are correctly detected + Make sure to handle not getting any files correctly""" - path = os.path.join(os.path.dirname(__file__), 'content') - filename = os.path.join(path, 'article_with_metadata.rst') - changed = utils.files_changed(path, 'rst') + dirname = os.path.join(os.path.dirname(__file__), 'content') + path = os.path.join(dirname, 'article_with_metadata.rst') + changed = utils.files_changed(dirname, 'rst') self.assertEquals(changed, True) - changed = utils.files_changed(path, 'rst') + changed = utils.files_changed(dirname, 'rst') self.assertEquals(changed, False) t = time.time() - os.utime(filename, (t, t)) - changed = utils.files_changed(path, 'rst') + os.utime(path, (t, t)) + changed = utils.files_changed(dirname, 'rst') self.assertEquals(changed, True) self.assertAlmostEqual(utils.LAST_MTIME, t, delta=1) + empty_path = os.path.join(os.path.dirname(__file__), 'empty') + try: + os.mkdir(empty_path) + os.mkdir(os.path.join(empty_path, "empty_folder")) + shutil.copy(__file__, empty_path) + with self.assertRaises(NoFilesError): + utils.files_changed(empty_path, 'rst') + except OSError: + self.fail("OSError Exception in test_files_changed test") + finally: + shutil.rmtree(empty_path, True) + def test_clean_output_dir(self): test_directory = os.path.join(os.path.dirname(__file__), 'clean_output') content = os.path.join(os.path.dirname(__file__), 'content') @@ -96,3 +129,16 @@ class TestUtils(unittest.TestCase): self.assertTrue(os.path.isdir(test_directory)) self.assertListEqual([], os.listdir(test_directory)) shutil.rmtree(test_directory) + + def test_clean_output_dir_not_there(self): + test_directory = os.path.join(os.path.dirname(__file__), 'does_not_exist') + utils.clean_output_dir(test_directory) + self.assertTrue(not os.path.exists(test_directory)) + + def test_clean_output_dir_is_file(self): + test_directory = os.path.join(os.path.dirname(__file__), 'this_is_a_file') + f = open(test_directory, 'w') + f.write('') + f.close() + utils.clean_output_dir(test_directory) + self.assertTrue(not os.path.exists(test_directory)) diff --git a/tests/test_webassets.py b/tests/test_webassets.py new file mode 100644 index 00000000..87b2789c --- /dev/null +++ b/tests/test_webassets.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# from __future__ import unicode_literals + +import hashlib +import os +from codecs import open +from tempfile import mkdtemp +from shutil import rmtree + +from pelican import Pelican +from pelican.settings import read_settings +from .support import unittest, skipIfNoExecutable, module_exists + +CUR_DIR = os.path.dirname(__file__) +THEME_DIR = os.path.join(CUR_DIR, 'themes', 'assets') +CSS_REF = open(os.path.join(THEME_DIR, 'static', 'css', + 'style.min.css')).read() +CSS_HASH = hashlib.md5(CSS_REF).hexdigest()[0:8] + + +@unittest.skipUnless(module_exists('webassets'), "webassets isn't installed") +@skipIfNoExecutable(['scss', '-v']) +@skipIfNoExecutable(['cssmin', '--version']) +class TestWebAssets(unittest.TestCase): + """Base class for testing webassets.""" + + def setUp(self, override=None): + self.temp_path = mkdtemp() + settings = { + 'PATH': os.path.join(CUR_DIR, 'content', 'TestCategory'), + 'OUTPUT_PATH': self.temp_path, + 'PLUGINS': ['pelican.plugins.assets', ], + 'THEME': THEME_DIR, + } + if override: + settings.update(override) + + self.settings = read_settings(override=settings) + pelican = Pelican(settings=self.settings) + pelican.run() + + def tearDown(self): + rmtree(self.temp_path) + + def check_link_tag(self, css_file, html_file): + """Check the presence of `css_file` in `html_file`.""" + + link_tag = ''.\ + format(css_file=css_file) + html = open(html_file).read() + self.assertRegexpMatches(html, link_tag) + + +class TestWebAssetsRelativeURLS(TestWebAssets): + """Test pelican with relative urls.""" + + def test_jinja2_ext(self): + """Test that the Jinja2 extension was correctly added.""" + + from webassets.ext.jinja2 import AssetsExtension + self.assertIn(AssetsExtension, self.settings['JINJA_EXTENSIONS']) + + def test_compilation(self): + """Compare the compiled css with the reference.""" + + gen_file = os.path.join(self.temp_path, 'theme', 'gen', + 'style.{0}.min.css'.format(CSS_HASH)) + self.assertTrue(os.path.isfile(gen_file)) + + css_new = open(gen_file).read() + self.assertEqual(css_new, CSS_REF) + + def test_template(self): + """Look in the output files for the link tag.""" + + css_file = './theme/gen/style.{0}.min.css'.format(CSS_HASH) + html_files = ['index.html', 'archives.html', + 'this-is-an-article-with-category.html'] + for f in html_files: + self.check_link_tag(css_file, os.path.join(self.temp_path, f)) + + self.check_link_tag( + '../theme/gen/style.{0}.min.css'.format(CSS_HASH), + os.path.join(self.temp_path, 'category/yeah.html')) + + +class TestWebAssetsAbsoluteURLS(TestWebAssets): + """Test pelican with absolute urls.""" + + def setUp(self): + TestWebAssets.setUp(self, override={'RELATIVE_URLS': False, + 'SITEURL': 'http://localhost'}) + + def test_absolute_url(self): + """Look in the output files for the link tag with absolute url.""" + + css_file = 'http://localhost/theme/gen/style.{0}.min.css'.\ + format(CSS_HASH) + html_files = ['index.html', 'archives.html', + 'this-is-an-article-with-category.html'] + for f in html_files: + self.check_link_tag(css_file, os.path.join(self.temp_path, f)) diff --git a/tests/themes/assets/static/css/style.min.css b/tests/themes/assets/static/css/style.min.css new file mode 100644 index 00000000..daf9c3cb --- /dev/null +++ b/tests/themes/assets/static/css/style.min.css @@ -0,0 +1 @@ +body{font:14px/1.5 "Droid Sans",sans-serif;background-color:#e4e4e4;color:#242424}a{color:red}a:hover{color:orange} \ No newline at end of file diff --git a/tests/themes/assets/static/css/style.scss b/tests/themes/assets/static/css/style.scss new file mode 100644 index 00000000..10cd05be --- /dev/null +++ b/tests/themes/assets/static/css/style.scss @@ -0,0 +1,19 @@ +/* -*- scss-compile-at-save: nil -*- */ + +$baseFontFamily : "Droid Sans", sans-serif; +$textColor : #242424; +$bodyBackground : #e4e4e4; + +body { + font: 14px/1.5 $baseFontFamily; + background-color: $bodyBackground; + color: $textColor; +} + +a { + color: red; + + &:hover { + color: orange; + } +} diff --git a/tests/themes/assets/templates/base.html b/tests/themes/assets/templates/base.html new file mode 100644 index 00000000..05a32d06 --- /dev/null +++ b/tests/themes/assets/templates/base.html @@ -0,0 +1,7 @@ +{% extends "!simple/base.html" %} + +{% block head %} + {% assets filters="scss,cssmin", output="gen/style.%(version)s.min.css", "css/style.scss" %} + + {% endassets %} +{% endblock %} diff --git a/tox.ini b/tox.ini index a7dc9ec6..6d1a134a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,75 @@ +# This tests the unified codebase (py26-py32) of Pelican. +# depends on some external libraries that aren't released yet. +# +# To run Pelican, you will already have checked out and installed them. +# +# Now we must tell tox about this package, otherwise tox would load the old +# libraries from PyPi. +# +# Run tox from the libraries source tree. It will save its package in +# the distshare directory from where the tests here will pick it up. +# +# Do that for +# https://github.com/dmdm/smartypants.git +# +# and typogrify: +# https://github.com/dmdm/typogrify/tree/py3k +# +# and webassets: +# https://github.com/dmdm/webassets/tree/py3k +# +# +# CAVEAT: +# ------- +# +# 1/ +# Please be aware that my ports of typogrify and webassets are just 2to3'd. +# They are not backwards compatible with Python 2. +# +# 2/ +# Webassets still has unresolved issues, so I deactivated it for Py32 tests. + + [tox] -envlist = py26,py27 +envlist = py27,py32,py33 [testenv] -commands = nosetests -s tests +commands = + unit2 discover [] + nosetests -s tests +deps = + +[testenv:py27] deps = nose - Jinja2 - Pygments - docutils - feedgenerator unittest2 mock Markdown - BeautifulSoup + BeautifulSoup4 + feedgenerator + typogrify + webassets + +[testenv:py32] +deps = + nose + unittest2py3k + mock + Markdown + BeautifulSoup4 + feedgenerator +# {distshare}/smartypants-1.6.0.3.zip +# {distshare}/typogrify-2.0.0.zip +# {distshare}/webassets-0.8.dev.zip + +[testenv:py33] +deps = + nose + unittest2py3k + mock + Markdown + BeautifulSoup4 + feedgenerator +# {distshare}/smartypants-1.6.0.3.zip +# {distshare}/typogrify-2.0.0.zip +# {distshare}/webassets-0.8.dev.zip