diff --git a/dev_requirements.txt b/dev_requirements.txt index ec3245d1..acf01773 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,10 +1,8 @@ -Jinja2>=2.4 -Pygments -docutils -feedgenerator +# Tests unittest2 -pytz mock +# Optional Packages Markdown -blinker BeautifulSoup +typogrify +webassets \ No newline at end of file diff --git a/docs/contribute.rst b/docs/contribute.rst index 82419f17..0820d5c3 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -23,7 +23,7 @@ different projects. To create a virtual environment, use the following syntax:: - $ mkvirtualenv pelican + $ mkvirtualenv pelican To clone the Pelican source:: @@ -65,5 +65,5 @@ 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 +particular will give you some useful hints about ways in which the code/formatting can be improved. diff --git a/docs/faq.rst b/docs/faq.rst index c5c751e6..e76bea6a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -26,7 +26,7 @@ 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 +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. @@ -43,7 +43,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 `_. @@ -105,7 +105,7 @@ I'm getting a warning about feeds generated without SITEURL being set properly 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 +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. @@ -124,5 +124,5 @@ setting names). Here is an exact list of the renamed setting names:: Older 2.x themes that referenced the old setting names may not link properly. In order to rectify this, please update your theme for compatibility with 3.0+ -by changing the relevant values in your template files. For an example of +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 151eff3a..76f03a61 100644 --- a/docs/fr/configuration.rst +++ b/docs/fr/configuration.rst @@ -77,8 +77,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 @@ -155,7 +158,5 @@ SITEURL : STATIC_PATHS : Les chemins statiques que vous voulez avoir accès sur le chemin de sortie "statique" ; - - - - +MARKDOWN_EXTENSIONS : + Liste des extentions Markdown que vous souhaitez utiliser ; diff --git a/docs/getting_started.rst b/docs/getting_started.rst index b7cbe951..93c626c0 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -17,7 +17,7 @@ While the above is the simplest method, the recommended approach is to create a virtual environment for Pelican via virtualenv_ and virtualenvwrapper_ before installing Pelican. Assuming you've followed the virtualenvwrapper `installation `_ -and `shell configuration +and `shell configuration `_ steps, you can then open a new terminal session and create a new virtual environment for Pelican:: @@ -26,7 +26,7 @@ environment for 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 @@ -69,6 +69,7 @@ Optionally: * pygments, for syntax highlighting * Markdown, for supporting Markdown as an input format +* Typogrify, for typographical enhancements Kickstart a blog ================ @@ -209,7 +210,7 @@ Pages If you create a folder named ``pages``, 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 +Then, use the ``DISPLAY_PAGES_ON_MENU`` setting, which will add all the pages to the menu. If you want to exclude any pages from being linked to or listed in the menu @@ -219,7 +220,7 @@ things like making error pages that fit the generated theme of your site. 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 @@ -277,14 +278,15 @@ For RestructuredText, use the code-block directive:: -For Markdown, include the language identifier just above code blocks:: +For Markdown, include the language identifier just above the code block, +indenting both the identifier and code:: + + A block of text. :::identifier - - (indent both the identifier and code) -The specified identifier (e.g. ``python``, ``ruby``) should be one that +The specified identifier (e.g. ``python``, ``ruby``) should be one that appears on the `list of available lexers `_. Publishing drafts diff --git a/docs/importer.rst b/docs/importer.rst index ccf3ffe2..ba96d9c2 100644 --- a/docs/importer.rst +++ b/docs/importer.rst @@ -31,7 +31,7 @@ BeatifulSoup can be installed like any other Python package:: $ pip install BeautifulSoup -For pandoc, install a package for your operating system from the +For pandoc, install a package for your operating system from the `pandoc site `_. diff --git a/docs/index.rst b/docs/index.rst index 477b4342..3fc1cf9f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ Pelican Pelican is a static site generator, written in Python_. -* Write your weblog entries directly with your editor of choice (vim!) +* 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 @@ -79,4 +79,4 @@ A French version of the documentation is available at :doc:`fr/index`. .. _`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 \ No newline at end of file +.. _webchat: http://webchat.freenode.net/?channels=pelican&uio=d4 diff --git a/docs/internals.rst b/docs/internals.rst index 6b6f991f..a6264476 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -52,7 +52,7 @@ Take a look at the Markdown reader:: text = open(filename) md = Markdown(extensions = ['meta', 'codehilite']) content = md.convert(text) - + metadata = {} for name, value in md.Meta.items(): if name in _METADATA_FIELDS: @@ -81,7 +81,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 a074a0a2..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`` diff --git a/docs/plugins.rst b/docs/plugins.rst index 53858668..c275c57c 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -16,7 +16,7 @@ To load plugins, you have to specify them in your settings file. You have two ways to do so. Either by specifying strings with the path to the callables:: - PLUGINS = ['pelican.plugins.gravatar',] + PLUGINS = ['pelican.plugins.gravatar',] Or by importing them and adding them to the list:: @@ -48,35 +48,71 @@ 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__ -pages_generate_context pages_generator, metadata -pages_generator_init pages_generator invoked in the PagesGenerator.__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. +article_generate_context article_generator, metadata +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 request if you need them! +.. note:: + + The signal ``content_object_init`` can send different type of object as + 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 under ``pelican.plugins``: -Tag cloud ---------- +* `GitHub activity`_ +* `Global license`_ +* `Gravatar`_ +* `HTML tags for reStructuredText`_ +* `Related posts`_ +* `Sitemap`_ -Translation ------------ +Ideas for plugins that haven't been written yet: + +* Tag cloud +* Translation + +Plugin descriptions +=================== GitHub activity --------------- @@ -108,3 +144,131 @@ variable, as in the example:: ``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. + +HTML tags for reStructuredText +------------------------------ + +This plugin allows you to use HTML tags from within reST documents. Following +is a usage example, which is in this case a contact form:: + + .. html:: + +
+

+ +
+ +
+ +

+
+ +Related posts +------------- + +This plugin adds the ``related_posts`` variable to the article's context. +To enable, add the following to your settings file:: + + from pelican.plugins import related_posts + PLUGINS = [related_posts] + +You can then use the ``article.related_posts`` variable in your templates. +For example:: + + {% if article.related_posts %} +
    + {% for related_post in article.related_posts %} +
  • {{ related_post }}
  • + {% endfor %} +
+ {% endif %} + +Sitemap +------- + +The sitemap plugin generates plain-text or XML sitemaps. You can use the +``SITEMAP`` variable in your settings file to configure the behavior of the +plugin. + +The ``SITEMAP`` variable must be a Python dictionary, it 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 informations 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 ad08f020..af6bc8c0 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -16,6 +16,9 @@ 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. @@ -43,9 +46,12 @@ Setting name (default value) What doe If tuple object, it will instead generate the default datetime object by passing the tuple to the datetime.datetime constructor. -`JINJA_EXTENSIONS` (``[]``) A list of any Jinja2 extensions you want to use. `DELETE_OUTPUT_DIRECTORY` (``False``) Delete the content of the output directory before generating new files. +`FILES_TO_COPY` (``()``) A list of files 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 @@ -58,14 +64,20 @@ Setting name (default value) What doe 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`. +`OUTPUT_SOURCES` (``False``) Set to True if you want to copy the articles and pages in their + original format (e.g. Markdown or ReStructeredText) 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. @@ -100,7 +112,12 @@ Setting name (default value) What doe 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 look for the templates. + Can be used to separate templates from the theme. + Example: projects, resume, profile ... + This templates need to use ``DIRECT_TEMPLATES`` setting +`MARKDOWN_EXTENSIONS` (``['toc',]``) A list of any Markdown extensions you want to use. ===================================================================== ===================================================================== .. [#] Default is the system locale. @@ -144,37 +161,37 @@ 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/'. -================================================ ===================================================== -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/{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. +==================================================== ===================================================== .. note:: @@ -197,14 +214,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)', } @@ -223,13 +240,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)'), } @@ -313,10 +330,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. ================================================ ===================================================== @@ -330,9 +347,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:: @@ -352,12 +369,13 @@ Translations Pelican offers a way to translate articles. See the 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 @@ -373,19 +391,19 @@ Setting name (default value) What does it do? 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. @@ -393,22 +411,32 @@ Setting name (default value) What does it do? `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/getpelican/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 ? @@ -444,26 +472,27 @@ adding the following to your configuration:: Asset management ---------------- -The `WEBASSETS` setting allows to use the `webassets`_ module to manage assets -(css, js). The module must first be installed:: +The `WEBASSETS` setting 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 -`webassets` allows to concatenate your assets and to use almost all of the -hype tools of the moment (see the `documentation`_): +The `webassets` module allows you to perform a number of useful asset management +functions, including: -* css minifier (`cssmin`, `yuicompressor`, ...) -* css compiler (`less`, `sass`, ...) -* js minifier (`uglifyjs`, `yuicompressor`, `closure`, ...) +* 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. +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 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: +When using 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. For example... .. code-block:: jinja @@ -471,43 +500,43 @@ template tag, for example: {% endassets %} -will produce a minified css file with the version identifier: +... will produce a minified css file with a version identifier: .. code-block:: html -The filters can be combined, for example to use the `sass` compiler and minify -the output:: +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 %} + {% assets filters="sass,cssmin", output="css/style.min.css", "css/style.scss" %} + + {% endassets %} -Another example for javascript: +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: +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, +Pelican's debug mode is propagated to `webassets` to disable asset packaging and instead work with the uncompressed assets. However, this also means that -the `less` and `sass` files are not compiled, this should be fixed in a future -version of webassets (cf. the related `bug report +the LESS and SASS files are not compiled. This should be fixed in a future +version of `webassets` (cf. the related `bug report `_). .. _webassets: https://github.com/miracle2k/webassets -.. _documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html +.. _webassets documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html Example settings ================ diff --git a/docs/themes.rst b/docs/themes.rst index d3dd4d9e..d06c97a8 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -35,7 +35,7 @@ To make your own theme, you must follow the following structure:: * `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,14 +55,14 @@ 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) +categories A key-value dict containing the categories (keys) and the list of respective articles (values) pages The list of pages ============= =================================================== @@ -92,8 +92,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 +108,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 +118,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 +134,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 +170,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,11 +182,12 @@ 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 @@ -202,7 +203,8 @@ Here is a complete list of the feed variables:: CATEGORY_FEED_RSS TAG_FEED_ATOM TAG_FEED_RSS - TRANSLATION_FEED + TRANSLATION_FEED_ATOM + TRANSLATION_FEED_RSS Inheritance diff --git a/docs/tips.rst b/docs/tips.rst index 8905103b..abb739b1 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -12,11 +12,11 @@ file generator, we can take advantage of this. User Pages ---------- -GitHub allows you to create user pages in the form of ``username.github.com``. +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 +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 @@ -35,7 +35,7 @@ really easy, which can be installed via:: $ pip install ghp-import -Then, given a repository containing your articles, you would simply run +Then, given a repository containing your articles, you would simply run Pelican and upload the output to GitHub:: $ pelican -s pelican.conf.py . diff --git a/pelican/__init__.py b/pelican/__init__.py index a69752d8..9809b19b 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -1,4 +1,3 @@ -import copy import os import re import sys @@ -9,10 +8,12 @@ import argparse from pelican import signals from pelican.generators import (ArticlesGenerator, PagesGenerator, - StaticGenerator, PdfGenerator, LessCSSGenerator) + StaticGenerator, PdfGenerator, + LessCSSGenerator, SourceFileGenerator) from pelican.log import init -from pelican.settings import read_settings, _DEFAULT_CONFIG -from pelican.utils import clean_output_dir, files_changed, file_changed, NoFilesError +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 @@ -24,42 +25,21 @@ 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 = copy.deepcopy(_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.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY'] self.init_path() self.init_plugins() @@ -78,7 +58,7 @@ class Pelican(object): 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): @@ -139,8 +119,16 @@ class Pelican(object): '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'] + 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""" @@ -179,12 +167,28 @@ class Pelican(object): 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['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): @@ -241,11 +245,26 @@ 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): @@ -253,15 +272,12 @@ def get_instance(args): 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: @@ -276,7 +292,7 @@ def main(): # have. if files_changed(pelican.path, pelican.markup) or \ files_changed(pelican.theme, ['']): - if files_found_error == False: + if not files_found_error: files_found_error = True pelican.run() @@ -292,9 +308,11 @@ def main(): logger.warning("Keyboard interrupt, quitting.") break except NoFilesError: - if files_found_error == True: - logger.warning("No valid files found in content. Nothing to generate.") + if files_found_error: + logger.warning("No valid files found in content. " + "Nothing to generate.") files_found_error = False + time.sleep(1) # sleep to avoid cpu load except Exception, e: logger.warning( "Caught exception \"{}\". Reloading.".format(e) diff --git a/pelican/contents.py b/pelican/contents.py index 851607a5..0dee19f3 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -11,7 +11,7 @@ from sys import platform, stdin from pelican.settings import _DEFAULT_CONFIG from pelican.utils import slugify, truncate_html_words - +from pelican import signals logger = logging.getLogger(__name__) @@ -106,6 +106,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: @@ -195,15 +197,23 @@ class URLWrapper(object): def __unicode__(self): return self.name - def _from_settings(self, key): + def _from_settings(self, key, get_page_name=False): + """Returns URL information as defined in settings. + When get_page_name=True returns URL without anything after {slug} + e.g. if in settings: CATEGORY_URL="cat/{slug}.html" this returns "cat/{slug}" + Useful for pagination.""" setting = "%s_%s" % (self.__class__.__name__.upper(), key) value = self.settings[setting] if not isinstance(value, basestring): logger.warning(u'%s is set to %s' % (setting, value)) return value else: - return unicode(value).format(**self.as_dict()) + if get_page_name: + return unicode(value[:value.find('{slug}') + len('{slug}')]).format(**self.as_dict()) + else: + return unicode(value).format(**self.as_dict()) + page_name = property(functools.partial(_from_settings, key='URL', get_page_name=True)) url = property(functools.partial(_from_settings, key='URL')) save_as = property(functools.partial(_from_settings, key='SAVE_AS')) diff --git a/pelican/generators.py b/pelican/generators.py index ae9334da..fdec93fa 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -6,6 +6,7 @@ import logging import datetime import subprocess +from codecs import open from collections import defaultdict from functools import partial from itertools import chain @@ -16,7 +17,7 @@ from jinja2.exceptions import TemplateNotFound from pelican.contents import Article, Page, Category, is_valid_content from pelican.readers import read_file -from pelican.utils import copy, process_translations, open +from pelican.utils import copy, process_translations from pelican import signals @@ -36,8 +37,11 @@ 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__)) @@ -124,7 +128,8 @@ class ArticlesGenerator(Generator): def generate_feeds(self, writer): """Generate the feeds from the current context, and output files.""" - if self.settings.get('FEED_ATOM') is None and self.settings.get('FEED_RSS') is None: + if self.settings.get('FEED_ATOM') is None \ + and self.settings.get('FEED_RSS') is None: return elif self.settings.get('SITEURL') is '': logger.warning( @@ -150,7 +155,8 @@ class ArticlesGenerator(Generator): self.settings['CATEGORY_FEED_RSS'] % cat, feed_type='rss') - if self.settings.get('TAG_FEED_ATOM') or self.settings.get('TAG_FEED_RSS'): + if self.settings.get('TAG_FEED_ATOM') \ + or self.settings.get('TAG_FEED_RSS'): for tag, arts in self.tags.items(): arts.sort(key=attrgetter('date'), reverse=True) if self.settings.get('TAG_FEED_ATOM'): @@ -162,15 +168,21 @@ class ArticlesGenerator(Generator): 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.""" @@ -188,7 +200,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, @@ -203,7 +215,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.""" @@ -213,7 +225,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.""" @@ -223,7 +235,7 @@ 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.""" @@ -269,7 +281,7 @@ class ArticlesGenerator(Generator): 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'] + category = self.settings['DEFAULT_CATEGORY'] else: category = os.path.basename(os.path.dirname(f))\ .decode('utf-8') @@ -352,10 +364,12 @@ class ArticlesGenerator(Generator): self.authors = list(self.authors.items()) self.authors.sort(key=lambda item: item[0].name) - + self._update_context(('articles', 'dates', 'tags', 'categories', 'tag_cloud', 'authors', 'related_posts')) + signals.article_generator_finalized.send(self) + def generate_output(self, writer): self.generate_feeds(writer) self.generate_pages(writer) @@ -370,7 +384,7 @@ class PagesGenerator(Generator): self.hidden_translations = [] super(PagesGenerator, self).__init__(*args, **kwargs) signals.pages_generator_init.send(self) - + def generate_context(self): all_pages = [] hidden_pages = [] @@ -378,7 +392,7 @@ class PagesGenerator(Generator): os.path.join(self.path, self.settings['PAGE_DIR']), exclude=self.settings['PAGE_EXCLUDES']): try: - content, metadata = read_file(f) + content, metadata = read_file(f, settings=self.settings) except Exception, e: logger.warning(u'Could not process %s\n%s' % (f, str(e))) continue @@ -429,7 +443,23 @@ class StaticGenerator(Generator): # 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/' + + # Let ASSET_URL honor Pelican's RELATIVE_URLS setting. + # Hint for templates: + # Current version of webassets seem to remove any relative + # paths at the beginning of the URL. So, if RELATIVE_URLS + # is on, ASSET_URL will start with 'theme/', regardless if we + # set assets_url here to './theme/' or to 'theme/'. + # XXX However, this breaks the ASSET_URL if user navigates to + # a sub-URL, e.g. if he clicks on a category. To workaround this + # issue, I use + # + # instead of + # + if self.settings.get('RELATIVE_URLS'): + assets_url = './theme/' + else: + assets_url = self.settings['SITEURL'] + '/theme/' assets_src = os.path.join(self.output_path, 'theme') self.assets_env = AssetsEnvironment(assets_src, assets_url) @@ -453,13 +483,20 @@ 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"): @@ -467,7 +504,7 @@ class PdfGenerator(Generator): 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) + self.pdfcreator.createPdf(text=f.read(), output=output_pdf) logger.info(u' [ok] writing %s' % output_pdf) def generate_context(self): @@ -491,6 +528,19 @@ class PdfGenerator(Generator): for page in self.context['pages']: self._create_pdf(page, pdf_path) +class SourceFileGenerator(Generator): + def generate_context(self): + self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION'] + + def _create_source(self, obj, output_path): + filename = os.path.splitext(obj.save_as)[0] + dest = os.path.join(output_path, filename + self.output_extension) + copy('', obj.filename, dest) + + def generate_output(self, writer=None): + logger.info(u' Generating source files...') + for object in chain(self.context['articles'], self.context['pages']): + self._create_source(object, self.output_path) class LessCSSGenerator(Generator): """Compile less css files.""" 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/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/sitemap.py b/pelican/plugins/sitemap.py new file mode 100644 index 00000000..ebce1f04 --- /dev/null +++ b/pelican/plugins/sitemap.py @@ -0,0 +1,190 @@ +import collections +import os.path + +from datetime import datetime +from logging import warning, info +from codecs import open + +from pelican import signals, contents + +TXT_HEADER = u"""{0}/index.html +{0}/archives.html +{0}/tags.html +{0}/categories.html +""" + +XML_HEADER = u""" + +""" + +XML_URL = u""" + +{0}/{1} +{2} +{3} +{4} + +""" + +XML_FOOTER = u""" + +""" + + +def format_date(date): + if date.tzinfo: + tz = date.strftime('%s') + tz = tz[:-2] + ':' + tz[-2:] + else: + tz = "-00:00" + return date.strftime("%Y-%m-%dT%H:%M:%S") + tz + + +class SitemapGenerator(object): + + def __init__(self, context, settings, path, theme, output_path, *null): + + self.output_path = output_path + self.context = context + self.now = datetime.now() + self.siteurl = settings.get('SITEURL') + + self.format = 'xml' + + self.changefreqs = { + 'articles': 'monthly', + 'indexes': 'daily', + 'pages': 'monthly' + } + + self.priorities = { + 'articles': 0.5, + 'indexes': 0.5, + 'pages': 0.5 + } + + config = settings.get('SITEMAP', {}) + + if not isinstance(config, dict): + warning("sitemap plugin: the SITEMAP setting must be a dict") + else: + fmt = config.get('format') + pris = config.get('priorities') + chfreqs = config.get('changefreqs') + + if fmt not in ('xml', 'txt'): + warning("sitemap plugin: SITEMAP['format'] must be `txt' or `xml'") + warning("sitemap plugin: Setting SITEMAP['format'] on `xml'") + elif fmt == 'txt': + self.format = fmt + return + + valid_keys = ('articles', 'indexes', 'pages') + valid_chfreqs = ('always', 'hourly', 'daily', 'weekly', 'monthly', + 'yearly', 'never') + + if isinstance(pris, dict): + for k, v in pris.iteritems(): + if k in valid_keys and not isinstance(v, (int, float)): + default = self.priorities[k] + warning("sitemap plugin: priorities must be numbers") + warning("sitemap plugin: setting SITEMAP['priorities']" + "['{0}'] on {1}".format(k, default)) + pris[k] = default + self.priorities.update(pris) + elif pris is not None: + warning("sitemap plugin: SITEMAP['priorities'] must be a dict") + warning("sitemap plugin: using the default values") + + if isinstance(chfreqs, dict): + for k, v in chfreqs.iteritems(): + if k in valid_keys and v not in valid_chfreqs: + default = self.changefreqs[k] + warning("sitemap plugin: invalid changefreq `{0}'".format(v)) + warning("sitemap plugin: setting SITEMAP['changefreqs']" + "['{0}'] on '{1}'".format(k, default)) + chfreqs[k] = default + self.changefreqs.update(chfreqs) + elif chfreqs is not None: + warning("sitemap plugin: SITEMAP['changefreqs'] must be a dict") + warning("sitemap plugin: using the default values") + + + + def write_url(self, page, fd): + + if getattr(page, 'status', 'published') != 'published': + return + + page_path = os.path.join(self.output_path, page.url) + if not os.path.exists(page_path): + return + + lastmod = format_date(getattr(page, 'date', self.now)) + + if isinstance(page, contents.Article): + pri = self.priorities['articles'] + chfreq = self.changefreqs['articles'] + elif isinstance(page, contents.Page): + pri = self.priorities['pages'] + chfreq = self.changefreqs['pages'] + else: + pri = self.priorities['indexes'] + chfreq = self.changefreqs['indexes'] + + + if self.format == 'xml': + fd.write(XML_URL.format(self.siteurl, page.url, lastmod, chfreq, pri)) + else: + fd.write(self.siteurl + '/' + loc + '\n') + + + def generate_output(self, writer): + path = os.path.join(self.output_path, 'sitemap.{0}'.format(self.format)) + + pages = self.context['pages'] + self.context['articles'] \ + + [ c for (c, a) in self.context['categories']] \ + + [ t for (t, a) in self.context['tags']] \ + + [ a for (a, b) in self.context['authors']] + + for article in self.context['articles']: + pages += article.translations + + info('writing {0}'.format(path)) + + with open(path, 'w', encoding='utf-8') as fd: + + if self.format == 'xml': + fd.write(XML_HEADER) + else: + fd.write(TXT_HEADER.format(self.siteurl)) + + FakePage = collections.namedtuple('FakePage', + ['status', + 'date', + 'url']) + + for standard_page_url in ['index.html', + 'archives.html', + 'tags.html', + 'categories.html']: + fake = FakePage(status='published', + date=self.now, + url=standard_page_url) + self.write_url(fake, fd) + + for page in pages: + self.write_url(page, fd) + + if self.format == 'xml': + fd.write(XML_FOOTER) + + +def get_generators(generators): + return SitemapGenerator + + +def register(): + signals.get_generators.connect(get_generators) diff --git a/pelican/readers.py b/pelican/readers.py index e3ea154d..dab829b9 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -16,7 +16,7 @@ except ImportError: import re from pelican.contents import Category, Tag, Author -from pelican.utils import get_date, open +from pelican.utils import get_date, pelican_open _METADATA_PROCESSORS = { @@ -102,7 +102,7 @@ class RstReader(Reader): def _get_publisher(self, filename): 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) @@ -129,8 +129,13 @@ class MarkdownReader(Reader): def read(self, filename): """Parse content and metadata of markdown files""" - text = open(filename) - md = Markdown(extensions=set(self.extensions + ['meta'])) + markdown_extensions = self.settings.get('MARKDOWN_EXTENSIONS', []) + if isinstance(markdown_extensions, (str, unicode)): + markdown_extensions = [m.strip() for m in + markdown_extensions.split(',')] + text = pelican_open(filename) + md = Markdown(extensions=set( + self.extensions + markdown_extensions + ['meta'])) content = md.convert(text) metadata = {} @@ -146,7 +151,7 @@ class HtmlReader(Reader): def read(self, filename): """Parse content and metadata of (x)HTML files""" - with open(filename) as content: + with pelican_open(filename) as content: metadata = {'title': 'unnamed'} for i in self._re.findall(content): key = i.split(':')[0][5:].strip() diff --git a/pelican/settings.py b/pelican/settings.py index 92c68ddc..4d1ed81e 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -26,12 +26,14 @@ _DEFAULT_CONFIG = {'PATH': '.', 'THEME_STATIC_PATHS': ['static', ], 'FEED_ATOM': 'feeds/all.atom.xml', 'CATEGORY_FEED_ATOM': 'feeds/%s.atom.xml', - 'TRANSLATION_FEED': 'feeds/all-%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', 'DEFAULT_CATEGORY': 'misc', 'DEFAULT_DATE': 'fs', 'WITH_FUTURE_DATES': True, @@ -58,6 +60,7 @@ _DEFAULT_CONFIG = {'PATH': '.', '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', @@ -75,16 +78,28 @@ _DEFAULT_CONFIG = {'PATH': '.', 'SUMMARY_MAX_LENGTH': 50, 'WEBASSETS': False, 'PLUGINS': [], + 'MARKDOWN_EXTENSIONS': ['toc', ], } -def read_settings(filename=None): +def read_settings(filename=None, override=None): if filename: local_settings = get_settings_from_file(filename) + # 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(filename), local_settings[p]))) + if p != 'THEME' or os.path.exists(p): + local_settings[p] = absp else: local_settings = copy.deepcopy(_DEFAULT_CONFIG) - configured_settings = configure_settings(local_settings, None, filename) - return configured_settings + + if override: + local_settings.update(override) + + return configure_settings(local_settings) def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG): @@ -94,9 +109,8 @@ def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG): context = copy.deepcopy(default_settings) if module is not None: - context.update( - (k, v) for k, v in inspect.getmembers(module) if k.isupper() - ) + context.update( + (k, v) for k, v in inspect.getmembers(module) if k.isupper()) return context @@ -111,19 +125,23 @@ def get_settings_from_file(filename, default_settings=_DEFAULT_CONFIG): return get_settings_from_module(module, default_settings=default_settings) -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 = copy.deepcopy(_DEFAULT_CONFIG) +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)') - # 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])) - ) + # 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'] @@ -174,4 +192,11 @@ def configure_settings(settings, default_settings=None, filename=None): logger.warn("You must install the webassets module to use WEBASSETS.") settings['WEBASSETS'] = False + if 'OUTPUT_SOURCES_EXTENSION' in settings: + if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], str): + settings['OUTPUT_SOURCES_EXTENSION'] = _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION'] + logger.warn("Detected misconfiguration with OUTPUT_SOURCES_EXTENSION." + " falling back to the default extension " + + _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION']) + return settings diff --git a/pelican/signals.py b/pelican/signals.py index 4d9ab512..73e718b5 100644 --- a/pelican/signals.py +++ b/pelican/signals.py @@ -1,7 +1,11 @@ from blinker import signal initialized = signal('pelican_initialized') +finalized = signal('pelican_finalized') 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 b4ba7888..3d94200b 100644 --- a/pelican/themes/notmyidea/static/css/main.css +++ b/pelican/themes/notmyidea/static/css/main.css @@ -97,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 { 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/simple/templates/base.html b/pelican/themes/simple/templates/base.html index 1f55a40f..c1d9cb78 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -11,16 +11,16 @@ {% endif %} {% if CATEGORY_FEED_ATOM %} - + {% endif %} {% if CATEGORY_FEED_RSS %} - + {% endif %} {% if TAG_FEED_ATOM %} - + {% endif %} {% if TAG_FEED_RSS %} - + {% endif %} {% endblock head %} diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index b6437c92..fc28c6a4 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -184,6 +184,8 @@ def build_header(title, date, author, categories, tags): 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: @@ -196,6 +198,8 @@ def build_markdown_header(title, date, author, categories, tags): 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: @@ -216,7 +220,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals 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)): diff --git a/pelican/tools/pelican_quickstart.py b/pelican/tools/pelican_quickstart.py index 388718ec..447a80c5 100755 --- a/pelican/tools/pelican_quickstart.py +++ b/pelican/tools/pelican_quickstart.py @@ -9,13 +9,12 @@ 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', @@ -24,8 +23,8 @@ 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' } @@ -77,7 +76,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 @@ -148,14 +147,16 @@ 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.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)) @@ -201,9 +202,13 @@ Please answer the following questions so this script can generate the files need try: with codecs.open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w', 'utf-8') as fd: + conf_python = dict() + for key, value in CONF.iteritems(): + 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: print('Error: {0}'.format(e)) @@ -228,11 +233,16 @@ Please answer the following questions so this script can generate the files need print('Error: {0}'.format(e)) if develop: + conf_shell = dict() + for key, value in CONF.iteritems(): + if isinstance(value, basestring) 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)) + fd.write(template.safe_substitute(conf_shell)) fd.close() os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 0755) except OSError, e: diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in index 4c5a4fcb..8e5c80f9 100644 --- a/pelican/tools/templates/Makefile.in +++ b/pelican/tools/templates/Makefile.in @@ -61,7 +61,7 @@ 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) 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/utils.py b/pelican/utils.py index ca3015ce..767dc117 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -6,7 +6,7 @@ import shutil import logging from collections import defaultdict -from codecs import open as _open +from codecs import open from datetime import datetime from itertools import groupby from jinja2 import Markup @@ -14,6 +14,7 @@ from operator import attrgetter logger = logging.getLogger(__name__) + class NoFilesError(Exception): pass @@ -37,9 +38,9 @@ def get_date(string): raise ValueError("'%s' is not a valid date" % string) -def open(filename): +def pelican_open(filename): """Open a file and return it's content""" - return _open(filename, encoding='utf-8').read() + return open(filename, encoding='utf-8').read() def slugify(value): @@ -86,16 +87,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, 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) diff --git a/pelican/writers.py b/pelican/writers.py index bac06bc5..53b6c48a 100644 --- a/pelican/writers.py +++ b/pelican/writers.py @@ -148,9 +148,9 @@ class Writer(object): paginators[key] = Paginator(object_list, len(object_list)) # generated pages, and write + name_root, ext = os.path.splitext(name) for page_num in range(paginators.values()[0].num_pages): paginated_localcontext = localcontext.copy() - paginated_name = name for key in paginators.iterkeys(): paginator = paginators[key] page = paginator.page(page_num + 1) @@ -158,9 +158,10 @@ class Writer(object): {'%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) 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/output/basic/drafts/a-draft-article.html b/tests/output/basic/drafts/a-draft-article.html index 24c1c3bc..7da9c416 100644 --- a/tests/output/basic/drafts/a-draft-article.html +++ b/tests/output/basic/drafts/a-draft-article.html @@ -55,8 +55,8 @@
- - Thu 26 July 2012 + + Mon 17 September 2012 diff --git a/tests/output/basic/feeds/all-fr.atom.xml b/tests/output/basic/feeds/all-fr.atom.xml index 39220a4f..979e21ef 100644 --- a/tests/output/basic/feeds/all-fr.atom.xml +++ b/tests/output/basic/feeds/all-fr.atom.xml @@ -1,4 +1,4 @@ -A Pelican Blog/2012-07-26T22:04:43ZTrop bien !2012-07-26T22:04:43ZDummy Authortag:,2012-07-26:oh-yeah-fr.html<p>Et voila du contenu en français</p> +A Pelican Blog/2012-09-17T15:02:39ZTrop bien !2012-09-17T15:02:39ZDummy Authortag:,2012-09-17:oh-yeah-fr.html<p>Et voila du contenu en français</p> Deuxième article2012-02-29T00:00:00ZDummy Authortag:,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/oh-yeah-fr.html b/tests/output/basic/oh-yeah-fr.html index cfcb7f1a..335428ef 100644 --- a/tests/output/basic/oh-yeah-fr.html +++ b/tests/output/basic/oh-yeah-fr.html @@ -55,8 +55,8 @@
- - Thu 26 July 2012 + + Mon 17 September 2012 diff --git a/tests/output/basic/tag/bar.html b/tests/output/basic/tag/bar.html index f58ba6bd..77462fe6 100644 --- a/tests/output/basic/tag/bar.html +++ b/tests/output/basic/tag/bar.html @@ -51,7 +51,7 @@

Ceci est un article, en français.

@@ -92,8 +92,8 @@ Translations:
  • -

    Ceci est un article, en français.

    +

    This is some article, in english

    - read more + read more
    diff --git a/tests/output/basic/tag/baz.html b/tests/output/basic/tag/baz.html index a5f9d9cc..4ebe1f1d 100644 --- a/tests/output/basic/tag/baz.html +++ b/tests/output/basic/tag/baz.html @@ -51,7 +51,7 @@