aboutsummaryrefslogtreecommitdiffstats
path: root/nikola
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2012-12-12 20:15:48 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2012-12-12 20:15:48 -0300
commit0f2c04e70a0ffdd0892d6970cafbcd952d221db5 (patch)
treed36f7747c4b9cb5c5e00cae5b137d22214b1c7be /nikola
parentca1f5a392261a7c6b82b5ac1015427605909d8c9 (diff)
Imported Upstream version 5upstream/5
Diffstat (limited to 'nikola')
-rw-r--r--nikola/PyRSS2Gen.py10
-rw-r--r--nikola/__init__.py1
-rwxr-xr-xnikola/data/samplesite/conf.py10
-rwxr-xr-xnikola/data/samplesite/dodo.py21
l---------[-rw-r--r--]nikola/data/samplesite/stories/manual.txt809
l---------[-rw-r--r--]nikola/data/samplesite/stories/theming.txt237
-rw-r--r--nikola/data/themes/default/messages/de.py16
-rw-r--r--nikola/data/themes/default/messages/en.py14
-rw-r--r--nikola/data/themes/default/messages/es.py16
-rw-r--r--nikola/data/themes/default/messages/fr.py10
-rw-r--r--nikola/data/themes/default/messages/gr.py14
-rw-r--r--nikola/data/themes/default/messages/it.py16
-rw-r--r--nikola/data/themes/default/messages/ru.py14
-rw-r--r--nikola/data/themes/default/templates/base.tmpl2
-rw-r--r--nikola/data/themes/default/templates/gallery.tmpl10
-rw-r--r--nikola/data/themes/default/templates/index.tmpl6
-rw-r--r--nikola/data/themes/default/templates/post.tmpl6
-rw-r--r--nikola/data/themes/jinja-default/templates/base.tmpl2
-rw-r--r--nikola/data/themes/jinja-default/templates/gallery.tmpl12
-rw-r--r--nikola/data/themes/jinja-default/templates/index.tmpl6
-rw-r--r--nikola/data/themes/jinja-default/templates/post.tmpl6
-rw-r--r--nikola/data/themes/site/templates/post.tmpl8
-rw-r--r--nikola/filters.py8
-rw-r--r--nikola/jinja_templates.py37
-rw-r--r--nikola/mako_templates.py65
-rw-r--r--nikola/md.py29
-rw-r--r--nikola/nikola.py1479
-rw-r--r--nikola/plugin_categories.py85
-rw-r--r--nikola/plugins/command_bootswatch_theme.plugin10
-rw-r--r--nikola/plugins/command_bootswatch_theme.py47
-rw-r--r--nikola/plugins/command_build.plugin10
-rw-r--r--nikola/plugins/command_build.py32
-rw-r--r--nikola/plugins/command_check.plugin10
-rw-r--r--nikola/plugins/command_check.py109
-rw-r--r--nikola/plugins/command_deploy.plugin9
-rw-r--r--nikola/plugins/command_deploy.py16
-rw-r--r--nikola/plugins/command_import_wordpress.plugin10
-rw-r--r--nikola/plugins/command_import_wordpress.py163
-rw-r--r--nikola/plugins/command_init.plugin10
-rw-r--r--nikola/plugins/command_init.py34
-rw-r--r--nikola/plugins/command_install_theme.plugin10
-rw-r--r--nikola/plugins/command_install_theme.py62
-rw-r--r--nikola/plugins/command_new_post.plugin10
-rw-r--r--nikola/plugins/command_new_post.py100
-rw-r--r--nikola/plugins/command_serve.plugin10
-rw-r--r--nikola/plugins/command_serve.py40
-rw-r--r--nikola/plugins/compile_html.plugin10
-rw-r--r--nikola/plugins/compile_html.py20
-rw-r--r--nikola/plugins/compile_markdown.plugin10
-rw-r--r--nikola/plugins/compile_markdown/__init__.py33
-rw-r--r--nikola/plugins/compile_rest.plugin10
-rw-r--r--nikola/plugins/compile_rest/__init__.py (renamed from nikola/rest.py)47
-rw-r--r--nikola/plugins/compile_rest/pygments_code_block_directive.py (renamed from nikola/pygments_code_block_directive.py)0
-rw-r--r--nikola/plugins/compile_rest/youtube.py (renamed from nikola/youtube.py)0
-rw-r--r--nikola/plugins/task_archive.plugin10
-rw-r--r--nikola/plugins/task_archive.py77
-rw-r--r--nikola/plugins/task_copy_assets.plugin10
-rw-r--r--nikola/plugins/task_copy_assets.py35
-rw-r--r--nikola/plugins/task_copy_files.plugin10
-rw-r--r--nikola/plugins/task_copy_files.py35
-rw-r--r--nikola/plugins/task_create_bundles.plugin10
-rw-r--r--nikola/plugins/task_create_bundles.py85
-rw-r--r--nikola/plugins/task_indexes.plugin10
-rw-r--r--nikola/plugins/task_indexes.py81
-rw-r--r--nikola/plugins/task_redirect.plugin10
-rw-r--r--nikola/plugins/task_redirect.py48
-rw-r--r--nikola/plugins/task_render_galleries.plugin10
-rw-r--r--nikola/plugins/task_render_galleries.py305
-rw-r--r--nikola/plugins/task_render_listings.plugin10
-rw-r--r--nikola/plugins/task_render_listings.py81
-rw-r--r--nikola/plugins/task_render_pages.plugin10
-rw-r--r--nikola/plugins/task_render_pages.py35
-rw-r--r--nikola/plugins/task_render_posts.plugin10
-rw-r--r--nikola/plugins/task_render_posts.py52
-rw-r--r--nikola/plugins/task_render_rss.plugin10
-rw-r--r--nikola/plugins/task_render_rss.py41
-rw-r--r--nikola/plugins/task_render_sources.plugin10
-rw-r--r--nikola/plugins/task_render_sources.py54
-rw-r--r--nikola/plugins/task_render_tags.plugin10
-rw-r--r--nikola/plugins/task_render_tags.py180
-rw-r--r--nikola/plugins/task_sitemap.plugin10
-rw-r--r--nikola/plugins/task_sitemap/__init__.py62
-rwxr-xr-xnikola/plugins/task_sitemap/sitemap_gen.py (renamed from nikola/sitemap_gen.py)1
-rw-r--r--nikola/plugins/template_jinja.plugin9
-rw-r--r--nikola/plugins/template_jinja.py38
-rw-r--r--nikola/plugins/template_mako.plugin9
-rw-r--r--nikola/plugins/template_mako.py68
-rw-r--r--nikola/post.py4
-rw-r--r--nikola/utils.py150
-rw-r--r--nikola/wordpress.py134
90 files changed, 2602 insertions, 2893 deletions
diff --git a/nikola/PyRSS2Gen.py b/nikola/PyRSS2Gen.py
index 6c4bda3..198ebb5 100644
--- a/nikola/PyRSS2Gen.py
+++ b/nikola/PyRSS2Gen.py
@@ -1,5 +1,7 @@
"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds."""
+# flake8: noqa
+
__name__ = "PyRSS2Gen"
__version__ = (1, 0, 0)
__author__ = "Andrew Dalke <dalke@dalkescientific.com>"
@@ -7,11 +9,7 @@ __author__ = "Andrew Dalke <dalke@dalkescientific.com>"
_generator_name = __name__ + "-" + ".".join(map(str, __version__))
import datetime
-try:
- import cStringIO
- StringIO = cStringIO
-except ImportError:
- import StringIO
+import io
# Could make this the base class; will need to add 'publish'
class WriteXmlMixin:
@@ -23,7 +21,7 @@ class WriteXmlMixin:
handler.endDocument()
def to_xml(self, encoding = "iso-8859-1"):
- f = StringIO.StringIO()
+ f = io.StringIO()
self.write_xml(f, encoding)
return f.getvalue()
diff --git a/nikola/__init__.py b/nikola/__init__.py
index e69de29..3b6ad2a 100644
--- a/nikola/__init__.py
+++ b/nikola/__init__.py
@@ -0,0 +1 @@
+from nikola import Nikola # NOQA
diff --git a/nikola/data/samplesite/conf.py b/nikola/data/samplesite/conf.py
index 4389f03..552eb68 100755
--- a/nikola/data/samplesite/conf.py
+++ b/nikola/data/samplesite/conf.py
@@ -52,11 +52,11 @@ post_pages = (
# 'rest' is reStructuredText
# 'markdown' is MarkDown
# 'html' assumes the file is html and just copies it
-#post_compilers = {
-# "rest": ('.txt', '.rst'),
-# "markdown": ('.md', '.mdown', '.markdown')
-# "html": ('.html', '.htm')
-# }
+post_compilers = {
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown'),
+ "html": ('.html', '.htm')
+ }
# Nikola is multilingual!
#
diff --git a/nikola/data/samplesite/dodo.py b/nikola/data/samplesite/dodo.py
deleted file mode 100755
index 1be7663..0000000
--- a/nikola/data/samplesite/dodo.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# Please don't edit this file unless you really know what you are doing.
-# The configuration is now in conf.py
-
-from doit.reporter import ExecutedOnlyReporter
-
-from nikola.nikola import Nikola
-
-import conf
-
-DOIT_CONFIG = {
- 'reporter': ExecutedOnlyReporter,
- 'default_tasks': ['render_site'],
-}
-SITE = Nikola(**conf.__dict__)
-
-
-def task_render_site():
- return SITE.gen_tasks()
diff --git a/nikola/data/samplesite/stories/manual.txt b/nikola/data/samplesite/stories/manual.txt
index f8804e6..9992900 100644..120000
--- a/nikola/data/samplesite/stories/manual.txt
+++ b/nikola/data/samplesite/stories/manual.txt
@@ -1,808 +1 @@
-The Nikola Handbook
-===================
-
-:Version: 2.1+svn
-:Author: Roberto Alsina <ralsina@netmanagers.com.ar>
-
-.. class:: alert alert-info pull-right
-
-.. contents::
-
-
-All You Need to Know
---------------------
-
-After you have Nikola installed:
-
-Create a site:
- ``nikola init mysite``
-
-Create a post:
- ``doit new_post``
-
-Edit the post:
- The filename should be in the output of the previous command.
-
-Build the site:
- ``doit``
-
-Start the test server:
- ``doit serve``
-
-See the site:
- http://127.0.0.1:8000
-
-That should get you going. If you want to know more, this manual will always be here
-for you.
-
-DON'T READ THIS MANUAL. IF YOU NEED TO READ IT I FAILED, JUST USE THE THING.
-
-On the other hand, if anything about Nikola is not as obvious as it should be, by all
-means tell me about it :-)
-
-What's Nikola and what can you do with it?
-------------------------------------------
-
-Nikola is a static website and blog generator. The very short explanation is
-that it takes some texts you wrote, and uses them to create a folder full
-of HTML files. If you upload that folder to a server, you will have a
-rather full-featured website, done with little effort.
-
-It's original goal is to create blogs, but it supports most kind of sites, and
-can be used as a CMS, as long as what you present to the user is your own content
-instead of something the user generates.
-
-Nikola can do:
-
-* A blog (`example <http://lateral.netmanagers.com.ar>`__)
-* Your company's site
-* Your personal site
-* A software project's site (`example <http://nikola.ralsina.com.ar>`__)
-* A book's site
-
-Since Nikola-based sites don't run any code on the server, there is no way to process
-user input in forms.
-
-Nikola can't do:
-
-* Twitter
-* Facebook
-* An Issue tracker
-* Anything with forms, really (except for comments_!)
-
-Keep in mind that "static" doesn't mean **boring**. You can have animations, slides
-or whatever fancy CSS/HTML5 thingie you like. It only means all that HTML is
-generated already before being uploaded. On the other hand, Nikola sites will
-tend to be content-heavy. What Nikola is good at is at putting what you write
-out there.
-
-Getting Help
-------------
-
-* Feel free to contact me at ralsina@netmanagers.com.ar for questions about Nikola.
-* You can file bugs at `the issue tracker <http://code.google.com/p/nikola-generator/issues/list>`__
-* You can discuss Nikola at the `nikola-discuss google group <http://groups.google.com/group/nikola-discuss>`_
-* You can subscribe to `the Nikola Blog <http://nikola.ralsina.com.ar/blog>`_
-* You can follow `Nikola on Twitter <https://twitter.com/#!/nikolagenerator>`_
-
-Why Static?
------------
-
-Most "modern" websites are *dynamic* in the sense that the contents of the site
-live in a database, and are converted into presentation-ready HTML only when a
-user wants to see the page. That's great. However, it presents some minor issues
-that static site generators try to solve.
-
-In a static site, the whole site, every page, *everything*, is created before
-the first user even sees it and uploaded to the server as a simple folder full
-of HTML files (and images, CSS, etc).
-
-So, let's see some reasons for using static sites:
-
-Security
- Dynamic sites are prone to experience security issues. The solution for that
- is constant vigilance, keeping the software behind the site updated, and
- plain old good luck. The stack of software used to provide a static site,
- like those Nikola generates, is much smaller (Just a webserver).
-
- A smaller software stack implies less security risk.
-
-Obsolescense
- If you create a site using (for example) Wordpress, what happens when Wordpress
- releases a new version? You have to update your Wordpress. That is not optional,
- because of security and support issues. If I release a new version of Nikola, and
- you don't update, *nothing* happens. You can continue to use the version you
- have now forever, no problems.
-
- Also, in the longer term, the very foundations of dynamic sites shift. Can you
- still deploy a blog software based on Django 0.96? What happens when your
- host stops supporting the php version you rely on? And so on.
-
- You may say those are long term issues, or that they won't matter for years. Well,
- I believe things should work forever, or as close to it as we can make them.
- Nikola's static output and its input files will work as long as you can install
- a Python > 2.5 (soon 3.x) in a Linux, Windows, or Mac and can find a server
- that sends files over HTTP. That's probably 10 or 15 years at least.
-
- Also, static sites are easily handled by the Internet Archive.
-
-Cost and Performance
- On dynamic sites, every time a reader wants a page, a whole lot of database
- queries are made. Then a whole pile of code chews that data, and HTML is
- produced, which is sent to the user. All that requires CPU and memory.
-
- On a static site, the highly optimized HTTP server reads the file from disk
- (or, if it's a popular file, from disk cache), and sends it to the user. You could
- probably serve a bazillion (technical term) pageviews from a phone using
- static sites.
-
-Lockin
- On server-side blog platforms, sometimes you can't export your own data, or
- it's in strange formats you can't use in other services. I have switched
- blogging platforms from Advogato to PyCs to two homebrewed systems, to Nikola,
- and have never lost a file, a URL, or a comment. That's because I have *always*
- had my own data in a format of my choice.
-
- With Nikola, you own your files, and you can do anything with them.
-
-Features
---------
-
-Nikola has a very defined featureset: it has every feature I needed for my own sites.
-Hopefully, it will be enough for others, and anyway, I am open to suggestions.
-
-If you want to create a blog or a site, Nikola provides:
-
-* Front page (and older posts pages)
-* RSS Feeds
-* Pages and feeds for each tag you used
-* Custom search
-* Full yearly archives
-* Custom output paths for generated pages
-* Easy page template customization
-* Static pages (not part of the blog)
-* Internationalization support (my own blog is English/Spanish)
-* Google sitemap generation
-* Custom deployment (if it's a command, you can use it)
-* A (very) basic look and feel you can customize, and is even text-mode friendly
-* The input format is light markup (`reStructuredText <quickstart.html>`_ or
- `Markdown <http://daringfireball.net/projects/markdown/>`_)
-* Easy-to-create image galleries
-
-Also:
-
-* A preview webserver
-* "Live" re-rendering while you edit
-* "Smart" builds: only what changed gets rebuilt (usually in 1 or 2 seconds)
-* Very easy to extend with minimal Python knowledge.
-
-Installing Nikola
------------------
-
-This is currently lacking on detail. Considering the niche Nikola is aimed at,
-I suspect that's not a problem yet. So, when I say "get", the specific details
-of how to "get" something for your specific operating system are left to you.
-
-The short version is: ``pip install https://github.com/ralsina/nikola/zipball/master``
-
-Longer version:
-
-#. Get python, if you don't have it.
-#. Get `doit <http://python-doit.sf.net>`_
-#. Get `docutils <http://docutils.sf.net>`_
-#. Get `Mako <http://makotemplates.org>`_
-#. Get `PIL <http://www.pythonware.com/products/pil/>`_
-#. Get `Pygments <http://pygments.org/>`_
-#. Get `unidecode <http://pypi.python.org/pypi/Unidecode/>`_
-#. Get `lxml <http://lxml.de/>`_
-
-Any non-prehistorical version of the above should work, and if you are in Linux
-you can try to use your distribution's packages if they exist, but the newer the better.
-
-Then get Nikola itself (<http://nikola.ralsina.com.ar/>), unzip it, and
-run ``python setup.py install``.
-
-After that, run ``nikola init sitename`` and that will create a folder called
-``sitename`` containing a functional demo site.
-
-Getting Started
----------------
-
-To create posts and pages in Nikola, you write them in restructured text or Markdown, light
-markups that are later converted to HTML (I may add support for textile or other
-markups later). There is a great `quick tutorial to learn restructured text. <quickstart.html>`_
-
-First, let's see how you "build" your site. Nikola comes with a minimal site to get you started.
-
-The tool used to do builds is called `doit <http://python-doit.sf.net>`_, and it rebuilds the
-files that are not up to date, so your site always reflects your latest content. To do our
-first build, just run "doit"::
-
- $ doit
- Parsing metadata
- . render_posts:stories/manual.html
- . render_posts:posts/1.html
- . render_posts:stories/1.html
- . render_archive:output/2012/index.html
- . render_archive:output/archive.html
- . render_indexes:output/index.html
- . render_pages:output/posts/welcome-to-nikola.html
- . render_pages:output/stories/about-nikola.html
- . render_pages:output/stories/handbook.html
- . render_rss:output/rss.xml
- . render_sources:output/stories/about-nikola.txt
- :
- :
- :
-
-Nikola will print a line for every output file it generates. If we do it again, that
-will be much much shorter::
-
- $ doit
- Parsing metadata
- . sitemap
-
-That is because `doit <http://python-doit.sf.net>`_ is smart enough not to generate
-all the pages again, unless you changed something that the page requires. So, if you change
-the text of a post, or its title, that post page, and all index pages where it is mentioned,
-will be recreated. If you change the post page template, then all the post pages will be rebuilt.
-
-Nikola is a series of doit *tasks*, and you can see them by doing ``doit list``::
-
- $ doit list
- Scanning posts . . done!
- copy_assets Create tasks to copy the assets of the whole theme chain.
- copy_files Copy static files into the output folder.
- deploy Deploy site.
- new_page Create a new post (interactive).
- new_post Create a new post (interactive).
- redirect Generate redirections.
- render_archive Render the post archives.
- render_galleries Render image galleries.
- render_indexes Render 10-post-per-page indexes.
- render_pages Build final pages from metadata and HTML fragments.
- render_posts Build HTML fragments from metadata and reSt.
- render_rss Generate RSS feeds.
- render_site Render the post archives.
- render_sources Publish the rst sources because why not?
- render_tags Render the tag pages.
- serve Start test server. (Usage: doit serve [--address 127.0.0.1] [--port 8000])
- sitemap Generate Google sitemap.
-
-You can make Nikola redo everything by calling ``doit clean``, you can make it do just a specific
-part of the site using task names, for example ``doit render_pages``, and even individual files like
-``doit render_indexes:output/index.html``
-
-The ``serve`` task is special, in that instead of generating a file it starts a web server so
-you can see the site you are creating::
-
- $ doit serve
- Parsing metadata
- . serve
- Serving HTTP on 127.0.0.1 port 8000 ...
-
-After you do this, you can point your web browser to http://localhost:8000 and you should see
-the sample site. This is useful as a "preview" of your work. You can combine add ``auto`` and do
-``doit auto serve`` which makes doit automatically regenerate your pages as needed, and
-it's a live preview!
-
-By default, the ``serve`` task runs the web server on port 8000 on the IP address 127.0.0.1.
-You can pass in an IP address and port number explicity using ``-a IP_ADDRESS``
-(short version of ``--address``) or ``-p PORT_NUMBER`` (short version of ``--port``)
-Example usage::
-
- $ doit serve --address 0.0.0.0 --port 8080
- Parsing metadata
- . serve
- Serving HTTP on 0.0.0.0 port 8080 ...
-
-The ``deploy`` task is discussed in the Deployment_ section.
-
-Creating a Blog Post
---------------------
-
-A post consists of two files, a metadata file (``post-title.meta``) and a
-file containing the contents written in `restructured text <http://docutils.sf.net>`_
-(``post-title.txt``), markdown or HTML. Which input type is used is guessed using
-the ``post_compilers`` option in ``conf.py`` but by default, the extensions
-supported are:
-
-.txt .rst
- Restructured Text
-
-.md .markdown .mdown
- Markdown
-
-.htm .html
- HTML
-
-The default configuration expects them to be placed in ``posts`` but that can be
-changed (see below, the post_pages option)
-
-You can just create them in ``posts`` or use a little helper task provided by Nikola::
-
- $ doit new_post
- Parsing metadata
- . new_post
- Creating New Post
- -----------------
-
- Enter title: How to Make Money
- Your post's metadata is at: posts/how-to-make-money.meta
- Your post's text is at: posts/how-to-make-money.txt
-
-The format for the ``.meta`` file is as follows::
-
- How to Make Money
- how-to-make-money
- 2012/04/09 13:59
-
-The first line is the title. The second one is the pagename. Since often titles will have
-characters that look bad on URLs, it's generated as a "clean" version of the title.
-The third line is the post's date, and is set to "now".
-
-You can add three more optional lines. A fourth line that is a list of tags
-separated with commas (spaces around the commas are ignored)::
-
- programming, python, fame, fortune
-
-And a fifth line that's a URL for an original source of the post.
-
-And a sixth line that's the page description.
-
-If you are writing a multilingual site, you can also create a per-language
-metadata file. This one can have two lines:
-
-1) The translated title for the post or page
-2) A translated version of the pagename
-
-You can edit these files with your favourite text editor, and once you are happy
-with the contents, generate the pages as explained in `Getting Started`_
-
-Currently supported languages are
-
-* English
-* Spanish
-* French
-* German
-* Russian
-* Greek.
-
-If you wish to add support for more languages, check out the instructions
-at the `theming guide <http://nikola.ralsina.com.ar/theming.html>`.
-
-The post page is generated using the ``post.tmpl`` template, which you can use
-to customize the output.
-
-The place where the post will be placed by ``new_post`` is based on the ``post_pages``
-configuration option::
-
- # post_pages contains (wildcard, destination, template, use_in_feed) tuples.
- #
- # The wildcard is used to generate a list of reSt source files (whatever/thing.txt)
- # That fragment must have an associated metadata file (whatever/thing.meta),
- # and opcionally translated files (example for spanish, with code "es"):
- # whatever/thing.txt.es and whatever/thing.meta.es
- #
- # From those files, a set of HTML fragment files will be generated:
- # cache/whatever/thing.html (and maybe cache/whatever/thing.html.es)
- #
- # These files are combinated with the template to produce rendered
- # pages, which will be placed at
- # output / TRANSLATIONS[lang] / destination / pagename.html
- #
- # where "pagename" is specified in the metadata file.
- #
- # if use_in_feed is True, then those posts will be added to the site's
- # rss feeds.
- #
- post_pages = (
- ("posts/*.txt", "posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "story.tmpl", False),
- )
-
-It will use the first location that has the last parameter set to True, or the last
-one in the list if all of them have it set to False.
-
-Alternatively, you can not have a meta file and embed the metadata in the post itself.
-
-In restructured text::
-
- .. tags: test,demo
- .. slug: demo-test
- .. date: 2012/04/09 13:59
- .. link: http://foo.bar/baz
-
-In Markdown:
- <!--
- .. tags: test,demo
- .. slug: demo-test
- .. date: 2012/04/09 13:59
- .. link: http://foo.bar/baz
- -->
-
-Teasers
-~~~~~~~
-
-If for any reason you want to provide feeds that only display the beginning of
-your post, you only need to add a "magical comment" in your post.
-
-In restructuredtext::
-
- .. TEASER_END
-
-In Markdown::
-
- <!-- TEASER_END -->
-
-Additionally, if you want also the "index" pages to show only the teasers, you can
-set the variable ``INDEX_TEASERS`` to ``True`` in ``conf.py``.
-
-Drafts
-~~~~~~
-
-If you add a "draft" tag to a post, then it will not be shown in indexes and feeds.
-It *will* be compiled, and if you deploy it it *will* be made available, so use
-with care.
-
-
-Creating a Page
----------------
-
-Pages are the same as posts, except that:
-
-* They are not added to the front page
-* They don't appear on the RSS feed
-* They use the ``story.tmpl`` template instead of ``post.tmpl`` by default
-
-The default configuration expects the page's metadata and text files to be on the
-``stories`` folder, but that can be changed (see post_pages option above).
-
-You can create the page's files manually or use the helper ``new_page`` that works exactly like
-the ``new_post`` described above, except it will place the files in the folder that
-has ``use_in_feed`` set to False.
-
-Redirections
-------------
-
-If you need a page to be available in more than one place, you can define redirections
-in your ``conf.py``::
-
- # A list of redirection tuples, [("foo/from.html", "/bar/to.html")].
- #
- # A HTML file will be created in output/foo/from.html that redirects
- # to the "/bar/to.html" URL. notice that the "from" side MUST be a
- # relative URL.
- #
- # If you don't need any of these, just set to []
-
- REDIRECTIONS = [("index.html", "/weblog/index.html")]
-
-It's better if you can do these using your web server's configuration, but if
-you can't, this will work.
-
-Configuration
--------------
-
-The configuration file is called ``conf.py`` and can be used to customize a lot of
-what Nikola does. Its syntax is python, but if you don't know the language, it
-still should not be terribly hard to grasp.
-
-The default ``conf.py`` you get with Nikola should be fairly complete, and is quite
-commented, but just in case, here is a full,
-`customized example configuration <sampleconfig.html>`_ (the one I use for
-`my site <http://lateral.netmanagers.com.ar>`_)
-
-Adding Files
-------------
-
-Any files you want to be in ``output/`` but are not generated by Nikola (for example,
-``favicon.ico``, just put it in ``files/``. Everything there is copied into
-``output`` by the ``copy_files`` task. Remember that you can't have files that collide
-with files Nikola generates (it will give an error).
-
-.. admonition:: Important
-
- Don't put any files manually in ``output/``. Ever. Really. Maybe someday Nikola
- will just wipe ``output/`` and then you will be sorry. So, please don't do that.
-
-If you want to copy more than one folder of static files into ``output`` you can
-change the FILES_FOLDERS option::
-
- # One or more folders containing files to be copied as-is into the output.
- # The format is a dictionary of "source" "relative destination".
- # Default is:
- # FILES_FOLDERS = {'files': '' }
- # Which means copy 'files' into 'output'
-
-Post Processing Filters
------------------------
-
-You can apply post processing to the files in your site, in order to optimize them
-or change them in arbitrary ways. For example, you may want to compress all CSS
-and JS files using yui-compressor.
-
-To do that, you can use the provided helper adding this in your ``config.py``::
-
- from nikola import filters
-
- FILTERS = {
- ".css": [filters.yui_compressor],
- ".js": [filters.yui_compressor],
- }
-
-Where ``filters.yui_compressor`` is a helper function provided by Nikola. You can
-replace that with strings describing command lines, or arbitrary python functions.
-
-If there's any specific thing you expect to be generally useful as a filter, contact
-me and I will add it to the filters library so that more people use it.
-
-Customizing Your Site
----------------------
-
-There are lots of things you can do to persoalize your website, but let's see the easy ones!
-
-Basics
- You can assume this needs to be changed::
-
- # Data about this site
- BLOG_TITLE = "Demo Site"
- BLOG_URL = "http://nikola.ralsina.com.ar"
- BLOG_EMAIL = "joe@demo.site"
- BLOG_DESCRIPTION = "This is a demo site for Nikola."
-
-CSS tweaking
- The default configuration includes a file, ``themes/default/assets/css/custom.css``
- which is empty. Put your CSS there, for minimal disruption of the provided CSS files.
-
- If you feel tempted to touch other files in assets, you probably will be better off
- with a `custom theme <theming.html>`_.
-
-Template tweaking
- If you really want to change the pages radically, you will want to do a
- `custom theme <theming.html>`_.
-
-
-Sidebar
- ``LICENSE`` is a HTML snippet for things like a CC badge, or whatever you prefer.
-
- The 'sidebar_links' option lets you define what links go in the right-hand
- sidebar, so you can link to important pages, or to other sites.
-
- The ``SEARCH_FORM`` option contains the HTML code for a search form based on
- duckduckgo.com which should always work, but feel free to change it to
- something else.
-
-Footer
- ``CONTENT_FOOTER`` is displayed, small at the bottom of all pages, I use it for
- the copyright notice.
-
-Analytics
- This is probably a misleading name, but the ``ANALYTICS`` option lets you define
- a HTML snippet that will be added at the bottom of body. The main usage is
- a Google analytics snippet or something similar, but you can really put anything
- there.
-
-Getting More Themes
--------------------
-
-There are not so many themes for Nikola. On occasion, I port something I like, and make
-it available for download. Nikola has a builtin theme download/install mechanism, its
-``install_theme`` task::
-
- $ doit install_theme -l
- Scanning posts . . done!
- . install_theme
- Themes:
- -------
- blogtxt
- readable
-
- $ doit install_theme -n blogtxt
- Scanning posts . . done!
- . install_theme
- Downloading: http://nikola.ralsina.com.ar/themes/blogtxt.zip
- Extracting: blogtxt into themes
-
-And there you are, you now have themes/blogtxt installed. It's very rudimentary, but it
-should work in most cases.
-
-If you create a nice theme, please share it! You can post about it on
-`the nikola forum <http://groups.google.com/group/nikola-discuss>`_ and I will
-make it available for download.
-
-One other option is to tweak an existing theme using a different color scheme,
-typography and CSS in general. Nikola provides a ``bootswatch_theme`` option
-to create a custom theme by downloading free CSS files from http://bootswatch.com::
-
- $ doit bootswatch_theme -n custom_theme -s spruce -p site
- Scanning posts . . done!
- . bootswatch_theme
- Creating custom_theme theme from spruce and site
- Downloading: http://bootswatch.com/spruce/bootstrap.min.css
- Downloading: http://bootswatch.com/spruce/bootstrap.css
- Theme created. Change the THEME setting to "custom_theme" to use it.
-
-You can even try what different swatches do on an existing site using
-their handy `bootswatchlet <http://news.bootswatch.com/post/29555952123/a-bookmarklet-for-bootswatch>`_
-
-Play with it, there's cool stuff there. This feature was suggested by
-`clodo <http://elgalpondebanquito.com.ar>`_.
-
-Deployment
-----------
-
-Nikola doesn't really have a concept of deployment. However, if you can specify your
-deployment procedure as a series of commands, you can put them in the ``DEPLOY_COMMANDS``
-option, and run them with ``doit deploy``.
-
-One caveat is that if any command has a % in it, you should double them.
-
-Here is an example, from my own site's deployment script::
-
- DEPLOY_COMMANDS = [
- 'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral',
- 'rdiff-backup output ~/bartleblog-backup',
- "links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar'",
- 'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup',
- ]
-
-Other interesting ideas are using
-`git as a deployment mechanism <http://toroid.org/ams/git-website-howto>`_ (or any other VCS
-for that matter), using `lftp mirror <http://lftp.yar.ru/>`_ or unison, or dropbox, or
-Ubuntu One. Any way you can think of to copy files from one place to another is good enough.
-
-Comments
---------
-
-While Nikola creates static sites, there is a minimum level of user interaction you
-are probably expecting: comments.
-
-The default templates contain support for `Disqus <http://disqus.com>`_. All you have
-to do is register a forum, put its short name in the ``DISQUS_FORUM`` option.
-
-Disqus is a good option because:
-
-1) It doesn't require any server-side software on your site
-2) They offer you a way to export your comments, so you can take
- them with you if you need to.
-3) It's free.
-4) It's damn nice.
-
-.. admonition:: Important
-
- In some cases, when you run the test site, you won't see the comments.
- That can be fixed by adding the disqus_developer flag to the templates
- but it's probably more trouble than it's worth.
-
-
-Image Galleries
----------------
-
-To create an image gallery, all you have to do is add a folder inside ``galleries``,
-and put images there. Nikola will take care of creating thumbnails, index page, etc.
-
-If you click on images on a gallery, you should see a bigger image, thanks to
-the excellent `colorbox <http://www.jacklmoore.com/colorbox>`_
-
-The gallery pages are generated using the ``gallery.tmpl`` template, and you can
-customize it there (you could switch to another lightbox instead of colorbox, change
-its settings, change the layout, etc.).
-
-The ``conf.py`` options affecting gallery pages are these::
-
- # Galleries are folders in galleries/
- # Final location of galleries will be output / GALLERY_PATH / gallery_name
- GALLERY_PATH = "galleries"
- THUMBNAIL_SIZE = 180
- MAX_IMAGE_SIZE = 1280
- USE_FILENAME_AS_TITLE = True
-
-If you add a file in ``galleries/gallery_name/index.txt`` its contents will be
-converted to HTML and inserted above the images in the gallery page.
-
-If you add some image filenames in ``galleries/gallery_name/exclude.meta``, they
-will be excluded in the gallery page.
-
-If ``USE_FILENAME_AS_TITLE`` is True the filename (parsed as a readable string)
-is used as the photo caption. If the filename starts with a number, it will
-be stripped. For example ``03_an_amazing_sunrise.jpg`` will be render as *An amazing sunrise*.
-
-Here is a `demo gallery </galleries/demo>`_ of historic, public domain Nikola
-Tesla pictures taken from `this site <http://kerryr.net/pioneers/gallery/tesla.htm>`_.
-
-Optimizing Your Website
------------------------
-
-One of the main goals of Nikola is to make your site fast and light. So here are a few
-tips we have found when setting up Nikola with Apache. If you have more, or
-different ones, or about other webservers, please share!
-
-#. Use a speed testing tool. I used Yahoo's YSlow but you can use any of them, and
- it's probably a good idea to use more than one.
-
-#. Enable compression in Apache::
-
- AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
-
-#. If even after you did the previous step the CSS files are not sent compressed::
-
- AddType text/css .css
-
-In the future we will be adding HTML/CSS/JS minimization and image recompression but
-that's not there yet, so you may want to use 3rd party tools to achieve that.
-
-Restructured Text Extensions
-----------------------------
-
-Nikola includes support for a few directives that are not part of docutils, but which
-we think are handy for website development.
-
-Youtube
-~~~~~~~
-
-To link to a youtube video, you need the id of the video. For example, if the
-URL of the video is http://www.youtube.com/watch?v=8N_tupPBtWQ what you need is
-**8N_tupPBtWQ**
-
-Once you have that, all you need to do is::
-
- .. youtube:: 8N_tupPBtWQ
-
-code-block
-~~~~~~~~~~
-
-This is a somewhat complicated directive to display code nicely. You can just
-embed code like this::
-
- .. code-block:: python
-
- print "Hello World!"
-
-Or you can include the code from a file:
-
- .. code-block:: python
- :include: /foo/bar/baz.py
-
-listing
-~~~~~~~
-
-To use this, you have to put your source code files inside ``listings`` or whatever your
-``LISTINGS_FOLDER`` variable is set to. Assuming you have a ``foo.py`` inside that folder::
-
- .. listing:: foo.py
-
-Will include the source code from ``foo.py`` and also create a ``listings/foo.py.html`` page
-and the listing will have a title linking to it.
-
-Advanced Code Options
-~~~~~~~~~~~~~~~~~~~~~
-
-Both code-block and listing support a number of options, including these:
-
-start-at
- A string, the diplayed code will start when it finds this
-end-at
- A string, the diplayed code will end when it finds this
-start-after
- A string, the diplayed code will start in the line after this
-end-before
- A string, the diplayed code will end in the line before this
-linenos
- Display line numbers
-linenos_offset
- Use the original file's line numbers (warning: broken)
-tab-width
- Size of the tabs (default 4)
-
-License
--------
-
-Nikola is released under the `GPL version 3 <http://www.gnu.org/licenses/gpl-3.0.html>`_ which
-is a free software license. Some components shipped along with Nikola, or required by it are
-released under other licenses.
-
-If you are not familiar with free software licensing: In general, you should be able to
-do pretty much anything you want, unless you modify Nikola. If you modify it, and share
-it with someone else, that someone else should get all your modifications under the same
-license you got it.
+../../../../docs/manual.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/stories/theming.txt b/nikola/data/samplesite/stories/theming.txt
index 339ecd4..d2dddb6 100644..120000
--- a/nikola/data/samplesite/stories/theming.txt
+++ b/nikola/data/samplesite/stories/theming.txt
@@ -1,236 +1 @@
-Theming Nikola
-==============
-
-:Version: 2.1+svn
-:Author: Roberto Alsina <ralsina@netmanagers.com.ar>
-
-.. class:: alert alert-info pull-right
-
-.. contents::
-
-The Structure
--------------
-
-Themes are located in the ``themes`` folder where Nikola is installed, one folder per theme.
-The folder name is the theme name.
-
-A Nikola theme consists of three folders:
-
-assets
- This is where you would put your CSS, Javascript and image files. It will be copied
- into ``output/assets`` when you build the site, and the templates will contain
- references to them.
-
- The included themes use `Bootstrap <http://twitter.github.com/bootstrap/>`_
- and `Colorbox <http://www.jacklmoore.com/colorbox>`_ so they are in assets,
- along with CSS files for syntax highligting and reStructuredText, and a
- minified copy of jQuery.
-
- If you want to base your theme on other frameworks (or on no framework at all)
- just remember to put there everything you need for deployment.
-
-templates
- This contains the templates used to generate the pages. While Nikola will use a
- certain set of template names by default, you can add others for specific parts
- of your site.
-
-messages
- Nikola tries to be multilingual. This is where you put the strings for your theme
- so that it can be translated into other languages.
-
-And these optional files:
-
-parent
- A text file that, on its first line, contains the name of the **parent theme**.
- Any resources missing on this theme, will be looked up in the parent theme
- (and then in the grandparent, etc).
-
- The ``parent`` is so you don't have to create a full theme each time: just create an
- empty theme, set the parent, and add the bits you want modified.
-
-engine
- A text file which, on the first line, contains the name of the template engine
- this theme needs. Currently supported values are "mako" and "jinja".
- If this file is not given, "mako" is assumed.
-
-bundles
- A text file containing a list of files to be turned into bundles using WebAssets.
- For example::
-
- assets/css/all.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,custom.css
-
- This creates a file called "assets/css/all.css" in your output that is the
- combination of all the other file paths, relative to the output file.
- This makes the page much more efficient because it avoids multiple connections to the server,
- at the cost of some extra difficult debugging.
-
- WebAssets supports bundling CSS and JS files.
-
- Templates should use either the bundle or the individual files based on the ``use_bundles``
- variable, which in turn is set by the ``USE_BUNDLES`` option.
-
-Creating a New Theme
---------------------
-
-In your site's folder, create a ``themes`` folder. Choose a theme to start from, and
-create ``themes/yourthemename/parent`` as a file containing the parent theme's name.
-There, you just created a new theme. Of course it looks exactly like the other one,
-so let's customize it.
-
-Templates
----------
-
-In templates there is a number of files whose name ends in ``.tmpl``. Those are the
-theme's page templates. They are done usig the `Mako <http://makotemplates.org>`_
-template language. If you want to do a theme, you should learn the Mako syntax first.
-
-Mako has a nifty concept of template inheritance. That means that, a
-template can inherit from another and only change small bits of the output. For example,
-``base.tmpl`` defines the whole layout for a page but has only a placeholder for content
-so ``post.tmpl`` only define the content, and the layout is inherited from ``base.tmpl``.
-
-These are the templates that come with the included themes:
-
-base.tmpl
- This template defines the basic page layout for the site. It's mostly plain HTML
- but defines a few blocks that can be re-defined by inheriting templates:
-
- * ``extra_head`` is a block that is added before ``</head>``, (ex: for adding extra CSS)
- * ``belowtitle`` is used by default to display a list of translations but you can put
- anything there.
- * ``content`` is where the inheriting templates will place the main content of the page.
- * ``permalink`` is an absolute path to the page (ex: "/archive/index.html")
-
- This template always receives the following variables you can use:
-
- * ``lang`` is the laguage for this page.
- * ``title`` is the page's title.
- * ``description`` is the page's description.
- * ``blog_title`` is the blog's title.
- * ``blog_author`` is the blog's author.
- * ``messages`` contains the theme's strings and translations.
- * ``_link`` is an utility function to create links to other pages in the site.
- It takes three arguments, kind, name, lang:
-
- kind is one of:
-
- * tag_index (name is ignored)
- * tag (and name is the tag name)
- * tag_rss (name is the tag name)
- * archive (and name is the year, or None for the main archive index)
- * index (name is the number in index-number)
- * rss (name is ignored)
- * gallery (name is the gallery name)
-
- The returned value is always an absolute path, like "/archive/index.html".
-
- * ``rel_link`` converts absolute paths to relative ones. You can use it with
- ``_link`` and ``permalink`` to create relative links, which makes the site
- able to work when moved inside the server. Example: ``rel_link(permalink, url)``
-
- * Anything you put in your ``GLOBAL_CONTEXT`` option in ``dodo.py``. This
- usually includes ``sidebar_links``, ``search_form``, and others.
-
- The included themes use at least these:
-
- * ``rss_link`` a link to custom RSS feed, although it may be empty)
- * ``blog_url`` the URL for your site
- * ``blog_title`` the name of your site
- * ``content_footer`` things like copyright notices, disclaimers, etc.
- * ``license`` a larger license badge
- * ``analytics`` google scripts, or any JS you want to tack at the end of the body
- of the page.
- * ``disqus_forum``: a `Disqus <http://disqus.com>`_ ID you can use to enable comments.
-
- It's probably a bad idea to do a theme that *requires* more than this (please put
- a ``README`` in it saying what the user should add in its ``dodo.py``), but there is no
- problem in requiring less.
-
-post.tmpl
- Template used for blog posts. Can use everything ``base.tmpl`` uses, plus:
-
- * ``post``: a Post object. This has a number of members:
-
- * ``post.title(language)``: returns a localized title
- * ``post.date``
- * ``post.tags``: A list of tags
- * ``post.text(language)``: the translated text of the post
- * ``post.permalink(language, absolute)``: Link to the post in that language.
- If ``absolute`` is ``True`` the link contains the full URL. This is useful
- for things like Disqus comment forms.
- * ``post.next_post`` is None or a Post object that is next newest in the timeline.
- * ``post.prev_post`` is None or a Post object that is next oldest in the timeline.
-
-story.tmpl
- Used for pages that are not part of a blog, usually a cleaner, less
- intrusive layout than ``post.tmpl``, but same parameters.
-
-gallery.tmpl
- Template used for image galleries. Can use everything ``base.tmpl`` uses, plus:
-
- * ``text``: A descriptive text for the gallery.
- * ``images``: A list of (thumbnail, image) paths.
-
-index.tmpl
- Template used to render the multipost indexes. Can use everything ``base.tmpl`` uses, plus:
-
- * ``posts``: a list of Post objects, as described above.
- * ``prevlink``: a link to a previous page
- * ``nextlink``: a link to the next page
-
-list.tmpl
- Template used to display generic lists of links. Can use everything ``base.tmpl`` uses, plus:
-
- * ``items``: a list of (text, link) elements.
-
-You can add other templates for specific pages, which the user can the use in his ``post_pages``
-option in ``dodo.py``. Also, keep in mind that your theme is yours, there is no reason why
-you would need to maintain the inheritance as it is, or not require whatever data you want.
-
-Messages and Translations
--------------------------
-
-When you modify templates, you may want to add text in them (for example: "About Me").
-Instead of adding the text directly, which makes it impossible to translate to other
-languages, add it like this::
-
- ${messages[lang]["About Me"]}
-
-Then, in ``messages/en.py`` add it along the other strings::
-
- MESSAGES = [
- u"Posts for year %s",
- u"Archive",
- u"Posts about %s:",
- u"Tags",
- u"Also available in: ",
- u"More posts about",
- u"Posted:",
- u"Original site",
- u"Read in english",
- u"About Me",
- ]
-
-Then, when I want to use your theme in spanish, all I have to do is add a line in ``messages/es.py``::
-
- MESSAGES = {
- u"LANGUAGE": u"Español",
- u"Posts for year %s": u"Posts del año %s",
- u"Archive": u"Archivo",
- u"Posts about %s:": u"Posts sobre %s",
- u"Tags": u"Tags",
- u"Also available in: ": u"También disponible en: ",
- u"More posts about": u"Más posts sobre",
- u"Posted:": u"Publicado:",
- u"Original site": u"Sitio original",
- u"Read in english": u"Leer en español",
- u"About Me": u"Acerca del autor",
- }
-
-And voilá, your theme works in spanish. Don't remove strings from these files even if it seems
-your theme is not using them. Some are used internally in Nikola to generate titles and
-similar things.
-
-To create a new translation, just copy one of the existing ones, translate the right side of
-every string to your language, save it and send it to me, I will add it to Nikola!
-
+../../../../docs/theming.txt \ No newline at end of file
diff --git a/nikola/data/themes/default/messages/de.py b/nikola/data/themes/default/messages/de.py
index f58b0a1..6e16a21 100644
--- a/nikola/data/themes/default/messages/de.py
+++ b/nikola/data/themes/default/messages/de.py
@@ -4,16 +4,18 @@ MESSAGES = {
u"LANGUAGE": u"Deutsch",
u"Posts for year %s": u"Eintr&auml;ge aus dem Jahr %s",
u"Archive": u"Archiv",
- u"Posts about %s:": u"Eintr&auml;ge &uuml;ber %s",
+ u"Posts about %s": u"Eintr&auml;ge &uuml;ber %s",
u"Tags": u"Tags",
- u"Also available in: ": u"Auch verf&uuml;gbar in: ",
+ u"Also available in": u"Auch verf&uuml;gbar in",
u"More posts about": u"Weitere Eintr&auml;ge &uuml;ber",
- u"Posted:": u"Ver&ouml;ffentlicht:",
+ u"Posted": u"Ver&ouml;ffentlicht",
u"Original site": u"Original-Seite",
u"Read in English": u"Auf Deutsch lesen",
- u"Older posts &rarr;": u"&Auml;ltere Eintr&auml;ge &rarr;",
- u"&larr; Newer posts": u"&larr; Neuere Eintr&auml;ge",
- u"&larr; Previous post": u"&larr; Vorheriger Eintrag",
- u"Next post &rarr;": u"N&auml;chster Eintrag &rarr;",
+ u"Older posts": u"&Auml;ltere Eintr&auml;ge",
+ u"Newer posts": u"Neuere Eintr&auml;ge",
+ u"Previous post": u"Vorheriger Eintrag",
+ u"Next post": u"N&auml;chster Eintrag",
u"Source": u"Source",
+ u"Read more": u"Weiterlesen",
+ u"old posts page %d": u'Vorherige Eintr&auml;ge %d'
}
diff --git a/nikola/data/themes/default/messages/en.py b/nikola/data/themes/default/messages/en.py
index 5a4a9bd..95b1210 100644
--- a/nikola/data/themes/default/messages/en.py
+++ b/nikola/data/themes/default/messages/en.py
@@ -1,17 +1,17 @@
MESSAGES = [
u"Posts for year %s",
u"Archive",
- u"Posts about %s:",
+ u"Posts about %s",
u"Tags",
- u"Also available in: ",
+ u"Also available in",
u"More posts about",
- u"Posted:",
+ u"Posted",
u"Original site",
u"Read in English",
- u"&larr; Newer posts",
- u"Older posts &rarr;",
- u"&larr; Previous post",
- u"Next post &rarr;",
+ u"Newer posts",
+ u"Older posts",
+ u"Previous post",
+ u"Next post",
u"old posts page %d",
u"Read more",
u"Source",
diff --git a/nikola/data/themes/default/messages/es.py b/nikola/data/themes/default/messages/es.py
index 82d2300..78de676 100644
--- a/nikola/data/themes/default/messages/es.py
+++ b/nikola/data/themes/default/messages/es.py
@@ -4,18 +4,18 @@ MESSAGES = {
u"LANGUAGE": u"Español",
u"Posts for year %s": u"Posts del año %s",
u"Archive": u"Archivo",
- u"Posts about %s:": u"Posts sobre %s",
+ u"Posts about %s": u"Posts sobre %s",
u"Tags": u"Tags",
- u"Also available in: ": u"También disponible en: ",
+ u"Also available in": u"También disponible en",
u"More posts about": u"Más posts sobre",
- u"Posted:": u"Publicado:",
+ u"Posted": u"Publicado",
u"Original site": u"Sitio original",
u"Read in English": u"Leer en español",
- u"Older posts &rarr;": u"Posts anteriores &rarr;",
- u"&larr; Newer posts": u"&larr; Posts posteriores",
- u"&larr; Previous post": u"&larr; Post anterior",
- u"Next post &rarr;": u"Siguiente post &rarr;",
+ u"Older posts": u"Posts anteriores",
+ u"Newer posts": u"Posts posteriores",
+ u"Previous post": u"Post anterior",
+ u"Next post": u"Siguiente post",
u"old posts page %d": u"posts antiguos página %d",
- u"Read more": u"Leer mas",
+ u"Read more": u"Leer más",
u"Source": u"Código",
}
diff --git a/nikola/data/themes/default/messages/fr.py b/nikola/data/themes/default/messages/fr.py
index d4bf0a6..5db1a1f 100644
--- a/nikola/data/themes/default/messages/fr.py
+++ b/nikola/data/themes/default/messages/fr.py
@@ -4,14 +4,14 @@ MESSAGES = {
u"LANGUAGE": u"Français",
u"Posts for year %s": u"Billets de l'année %s",
u"Archive": u"Archives",
- u"Posts about %s:": u"Billets sur %s",
+ u"Posts about %s": u"Billets sur %s",
u"Tags": u"Étiquettes",
- u"Also available in: ": u"Disponible aussi en : ",
+ u"Also available in": u"Disponible aussi en",
u"More posts about": u"Plus de billets sur",
- u"Posted:": u"Publié :",
+ u"Posted": u"Publié",
u"Original site": u"Site d'origine",
u"Read in English": u"Lire en français",
- u"&larr; Newer posts": u"&larr; Billets récents",
- u"Older posts &rarr;": u"Anciens billets &rarr;",
+ u"Newer posts": u"Billets récents",
+ u"Older posts": u"Anciens billets",
u"Source": u"Source",
}
diff --git a/nikola/data/themes/default/messages/gr.py b/nikola/data/themes/default/messages/gr.py
index 62139c9..fa6bb32 100644
--- a/nikola/data/themes/default/messages/gr.py
+++ b/nikola/data/themes/default/messages/gr.py
@@ -4,17 +4,17 @@ MESSAGES = {
u"LANGUAGE": u"Ελληνικά",
u"Posts for year %s": u"Αναρτήσεις για τη χρονιά %s",
u"Archive": u"Αρχείο",
- u"Posts about %s:": u"Αναρτήσεις για %s",
+ u"Posts about %s": u"Αναρτήσεις για %s",
u"Tags": u"Ετικέτες",
- u"Also available in: ": u"Διαθέσιμο και στο: ",
+ u"Also available in": u"Διαθέσιμο και στο",
u"More posts about": u"Περισσότερες αναρτήσεις για",
- u"Posted:": u"Αναρτήθηκε :",
+ u"Posted": u"Αναρτήθηκε",
u"Original site": u"Ιστοσελίδα αρχικής ανάρτησης",
u"Read in English": u"Διαβάστε στα Ελληνικά",
- u"&larr; Newer posts": u"&larr; Νεότερες αναρτήσεις",
- u"Older posts &rarr;": u"Παλαιότερες αναρτήσεις &rarr;",
- u"&larr; Previous post": u"&larr; Προηγούμενη ανάρτηση",
- u"Next post &rarr;": u"Επόμενη ανάρτηση &rarr;",
+ u"Newer posts": u"Νεότερες αναρτήσεις",
+ u"Older posts": u"Παλαιότερες αναρτήσεις",
+ u"Previous post": u"Προηγούμενη ανάρτηση",
+ u"Next post": u"Επόμενη ανάρτηση",
u"old posts page %d": u"σελίδα παλαιότερων αναρτήσεων %d",
u"Source": u"Source",
}
diff --git a/nikola/data/themes/default/messages/it.py b/nikola/data/themes/default/messages/it.py
index a4f37f0..01a97d5 100644
--- a/nikola/data/themes/default/messages/it.py
+++ b/nikola/data/themes/default/messages/it.py
@@ -2,18 +2,18 @@ MESSAGES = {
u"LANGUAGE": u"Italiano",
u"Posts for year %s": u"Articoli per l'anno %s",
u"Archive": u"Archivio",
- u"Posts about %s:": u"Articoli su %s",
+ u"Posts about %s": u"Articoli su %s",
u"Tags": u"Tags",
- u"Also available in: ": u"Anche disponibile in: ",
+ u"Also available in": u"Anche disponibile in",
u"More posts about": u"Altri articoli su",
- u"Posted:": u"Pubblicato:",
+ u"Posted": u"Pubblicato",
u"Original site": u"Sito originale",
u"Read in English": u"Leggi in italiano",
- u"&larr; Newer posts": u"&larr; Articoli recenti",
- u"Older posts &rarr;": u"Articoli più vecchi",
- u"Older posts &rarr;": u"Articoli vecchi",
- u"&larr; Previous post": u"&larr; Articolo precedente",
- u"Next post &rarr;": u"&larr; Articolo successivo",
+ u"Newer posts": u"Articoli recenti",
+ u"Older posts": u"Articoli più vecchi",
+ u"Older posts": u"Articoli vecchi",
+ u"Previous post": u"Articolo precedente",
+ u"Next post": u"Articolo successivo",
u"old posts page %d": u"pagina dei vecchi articoli %d",
u"Read more": u"Espandi",
u"Source": u"Source",
diff --git a/nikola/data/themes/default/messages/ru.py b/nikola/data/themes/default/messages/ru.py
index 2bd652b..5d5cb01 100644
--- a/nikola/data/themes/default/messages/ru.py
+++ b/nikola/data/themes/default/messages/ru.py
@@ -4,14 +4,18 @@ MESSAGES = {
u"LANGUAGE": u"Русский",
u"Posts for year %s": u"Записи за %s год",
u"Archive": u"Архив",
- u"Posts about %s:": u"Записи с тэгом %s:",
+ u"Posts about %s": u"Записи с тэгом %s:",
u"Tags": u"Тэги",
- u"Also available in: ": u"Также доступно в: ",
+ u"Also available in": u"Также доступно в",
u"More posts about": u"Больше записей о",
- u"Posted:": u"Опубликовано",
+ u"Posted": u"Опубликовано",
u"Original site": u"Оригинальный сайт",
u"Read in English": u"Прочесть по-русски",
- u"Older posts &rarr;": u"Старые записи &rarr;",
- u"&larr; Newer posts": u"&larr; Новые записи",
+ u"Older posts": u"Старые записи",
+ u"Newer posts": u"Новые записи",
+ u"Previous post": u"Предыдущая запись",
+ u"Next post": u"Следующая запись",
+ u"old posts page %d": u"страница со старыми записями %d",
+ u"Read more": u"Продолжить чтение",
u"Source": u"Source",
}
diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl
index b031423..cb5e0dd 100644
--- a/nikola/data/themes/default/templates/base.tmpl
+++ b/nikola/data/themes/default/templates/base.tmpl
@@ -53,7 +53,7 @@
<%block name="belowtitle">
%if len(translations) > 1:
<small>
- ${(messages[lang][u"Also available in: "])}
+ ${(messages[lang][u"Also available in"])}:&nbsp;
%for langname in translations.keys():
%if langname != lang:
<a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a>
diff --git a/nikola/data/themes/default/templates/gallery.tmpl b/nikola/data/themes/default/templates/gallery.tmpl
index 3c48413..37d749f 100644
--- a/nikola/data/themes/default/templates/gallery.tmpl
+++ b/nikola/data/themes/default/templates/gallery.tmpl
@@ -3,11 +3,21 @@
<%block name="sourcelink"></%block>
<%block name="content">
+ <ul class="breadcrumb">
+ % for link, crumb in crumbs:
+ <li><a href="${link}">/ ${crumb}</a></li>
+ % endfor
+ </ul>
%if text:
<p>
${text}
</p>
%endif
+ <ul>
+ % for folder in folders:
+ <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
+ % endfor
+ </ul>
<ul class="thumbnails">
%for image in images:
<li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}>
diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl
index 45e2172..2c7b4be 100644
--- a/nikola/data/themes/default/templates/index.tmpl
+++ b/nikola/data/themes/default/templates/index.tmpl
@@ -5,7 +5,7 @@
<div class="postbox">
<h1><a href="${post.permalink(lang)}">${post.title(lang)}</a>
<small>&nbsp;&nbsp;
- ${messages[lang]["Posted:"]} ${post.date}
+ ${messages[lang]["Posted"]}: ${post.date}
</small></h1>
<hr>
${post.text(lang, index_teasers)}
@@ -19,12 +19,12 @@
<ul class="pager">
%if prevlink:
<li class="previous">
- <a href="${prevlink}">${messages[lang]["&larr; Newer posts"]}</a>
+ <a href="${prevlink}">&larr; ${messages[lang]["Newer posts"]}</a>
</li>
%endif
%if nextlink:
<li class="next">
- <a href="${nextlink}">${messages[lang]["Older posts &rarr;"]}</a>
+ <a href="${nextlink}">${messages[lang]["Older posts"]} &rarr;</a>
</li>
%endif
</ul>
diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl
index b40ff89..6bbb460 100644
--- a/nikola/data/themes/default/templates/post.tmpl
+++ b/nikola/data/themes/default/templates/post.tmpl
@@ -8,7 +8,7 @@
% endif
<hr>
<small>
- ${messages[lang]["Posted:"]} ${post.date}&nbsp;&nbsp;|&nbsp;&nbsp;
+ ${messages[lang]["Posted"]}: ${post.date}&nbsp;&nbsp;|&nbsp;&nbsp;
%if len(translations) > 1:
%for langname in translations.keys():
@@ -32,12 +32,12 @@
<ul class="pager">
%if post.prev_post:
<li class="previous">
- <a href="${post.prev_post.permalink(lang)}">${messages[lang]["&larr; Previous post"]}</a>
+ <a href="${post.prev_post.permalink(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
</li>
%endif
%if post.next_post:
<li class="next">
- <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post &rarr;"]}</a>
+ <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} &rarr;</a>
</li>
%endif
</ul>
diff --git a/nikola/data/themes/jinja-default/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl
index cdd911c..546e1a7 100644
--- a/nikola/data/themes/jinja-default/templates/base.tmpl
+++ b/nikola/data/themes/jinja-default/templates/base.tmpl
@@ -52,7 +52,7 @@
{% block belowtitle%}
{% if translations|length > 1 %}
<small>
- {{ messages[lang]["Also available in: "] }}
+ {{ messages[lang]["Also available in"] }}:&nbsp;
{% for langname in translations.keys() %}
{% if langname != lang %}
<a href="{{_link("index", None, langname)}}">{{messages[langname]["LANGUAGE"]}}</a>
diff --git a/nikola/data/themes/jinja-default/templates/gallery.tmpl b/nikola/data/themes/jinja-default/templates/gallery.tmpl
index a08b148..dcd8a43 100644
--- a/nikola/data/themes/jinja-default/templates/gallery.tmpl
+++ b/nikola/data/themes/jinja-default/templates/gallery.tmpl
@@ -2,15 +2,25 @@
{% block sourcelink %}{% endblock %}
{% block content %}
+ <ul class="breadcrumb">
+ {% for link, crumb in crumbs %}
+ <li><a href="{{link}}">/ {{crumb}}</a></li>
+ {% endfor %}
+ </ul>
{% if text %}
<p>
{{ text }}
</p>
{% endif %}
+ <ul>
+ {% for folder in folders %}
+ <li><a href="{{folder}}"><i class="icon-folder-open"></i>&nbsp;{{folder}}</a></li>
+ {% endfor %}
+ </ul>
<ul class="thumbnails">
{% for image in images %}
<li><a href="{{image[0]}}" class="thumbnail image-reference"><img src="{{image[2]}}" /></a></li>
- <img src="${image[1]}" /></a></li>
+ <img src="{{image[1]}}" /></a></li>
{% endfor %}
</ul>
{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl
index c1fbb94..6244e10 100644
--- a/nikola/data/themes/jinja-default/templates/index.tmpl
+++ b/nikola/data/themes/jinja-default/templates/index.tmpl
@@ -4,7 +4,7 @@
<div class="postbox">
<h1><a href="{{post.permalink(lang)}}">{{post.title(lang)}}</a>
<small>&nbsp;&nbsp;
- {{messages[lang]["Posted:"]}} {{post.date}}
+ {{messages[lang]["Posted"]}}: {{post.date}}
</small></h1>
<hr>
{{post.text(lang, index_teasers)}}
@@ -18,12 +18,12 @@
<ul class="pager">
{%if prevlink %}
<li class="previous">
- <a href="{{prevlink}}">${messages[lang]["&larr; Newer posts"]}</a>
+ <a href="{{prevlink}}">&larr; {{messages[lang]["Newer posts"]}}</a>
</li>
{% endif %}
{% if nextlink %}
<li class="next">
- <a href="{{nextlink}}">${messages[lang]["Older posts &rarr;"]}</a>
+ <a href="{{nextlink}}">${messages[lang]["Older posts"]} &rarr;</a>
</li>
{% endif %}
</ul>
diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl
index 876c1a7..4748959 100644
--- a/nikola/data/themes/jinja-default/templates/post.tmpl
+++ b/nikola/data/themes/jinja-default/templates/post.tmpl
@@ -7,7 +7,7 @@
{% endif %}
<hr>
<small>
- {{messages[lang]["Posted:"]}} {{post.date}}&nbsp;&nbsp;|&nbsp;&nbsp;
+ {{messages[lang]["Posted"]}}: {{post.date}}&nbsp;&nbsp;|&nbsp;&nbsp;
{% if translations|length > 1 %}
{% for langname in translations.keys() %}
@@ -31,12 +31,12 @@
<ul class="pager">
{%if post.prev_post %}
<li class="previous">
- <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">{{messages[lang]["&larr; Previous post"]}}</a>
+ <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">&larr; {{messages[lang]["Previous post"]}}</a>
</li>
{% endif %}
{%if post.next_post %}
<li class="next">
- <a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post &rarr;"]}}</a>
+ <a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post"]}} &rarr;</a>
</li>
{% endif %}
</ul>
diff --git a/nikola/data/themes/site/templates/post.tmpl b/nikola/data/themes/site/templates/post.tmpl
index 99c0f1f..f777366 100644
--- a/nikola/data/themes/site/templates/post.tmpl
+++ b/nikola/data/themes/site/templates/post.tmpl
@@ -8,7 +8,7 @@
% endif
<hr>
<small>
- ${messages[lang]["Posted:"]} ${post.date}
+ ${messages[lang]["Posted"]}: ${post.date}
%if len(translations) > 1:
%for langname in translations.keys():
@@ -30,12 +30,12 @@
<ul class="pager">
%if post.prev_post:
<li class="previous">
- <a href="${post.prev_post.permalink(lang)}">${messages[lang]["&larr; Previous post"]}</a>
+ <a href="${post.prev_post.permalink(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
</li>
%endif
%if post.next_post:
<li class="next">
- <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post &rarr;"]}</a>
+ <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} &rarr;</a>
</li>
%endif
</ul>
@@ -45,11 +45,11 @@
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
%endif
+ </div>
</%block>
<%block name="sourcelink">
<li>
<a href="${post.pagenames[lang]+post.source_ext()}">${messages[lang]["Source"]}</a>
</li>
- </div>
</%block>
diff --git a/nikola/filters.py b/nikola/filters.py
index caea95e..f450d10 100644
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -1,9 +1,11 @@
"""Utility functions to help you run filters on files."""
import os
+import shutil
import subprocess
import tempfile
+
def runinplace(command, infile):
"""Runs a command in-place on a file.
@@ -22,8 +24,10 @@ def runinplace(command, infile):
tmpdir = tempfile.mkdtemp()
tmpfname = os.path.join(tmpdir, os.path.basename(infile))
command = command.replace('%1', infile)
- command = command.replace('%2', infile)
+ command = command.replace('%2', tmpfname)
subprocess.check_call(command, shell=True)
+ shutil.move(tmpfname, infile)
+
def yui_compressor(infile):
- return runinplace('yui-compressor %1 -o %2', infile) \ No newline at end of file
+ return runinplace(r'yui-compressor %1 -o %2', infile)
diff --git a/nikola/jinja_templates.py b/nikola/jinja_templates.py
deleted file mode 100644
index f55465f..0000000
--- a/nikola/jinja_templates.py
+++ /dev/null
@@ -1,37 +0,0 @@
-########################################
-# Jinja template handlers
-########################################
-
-import os
-
-import jinja2
-
-lookup = None
-cache = {}
-
-
-def get_template_lookup(directories):
- return jinja2.Environment(loader=jinja2.FileSystemLoader(
- directories,
- encoding='utf-8',
- ))
-
-
-def render_template(template_name, output_name, context, global_context):
- template = lookup.get_template(template_name)
- local_context = {}
- local_context.update(global_context)
- local_context.update(context)
- output = template.render(**local_context)
- if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
- with open(output_name, 'w+') as output:
- output.write(output.encode('utf8'))
- return output
-
-
-def template_deps(template_name):
- return []
diff --git a/nikola/mako_templates.py b/nikola/mako_templates.py
deleted file mode 100644
index e4a79d9..0000000
--- a/nikola/mako_templates.py
+++ /dev/null
@@ -1,65 +0,0 @@
-########################################
-# Mako template handlers
-########################################
-
-import os
-import shutil
-
-from mako import util, lexer
-from mako.lookup import TemplateLookup
-
-lookup = None
-cache = {}
-
-
-def get_deps(filename):
- text = util.read_file(filename)
- lex = lexer.Lexer(text=text, filename=filename)
- lex.parse()
-
- deps = []
- for n in lex.template.nodes:
- if getattr(n, 'keyword', None) == "inherit":
- deps.append(n.attributes['file'])
- # TODO: include tags are not handled
- return deps
-
-
-def get_template_lookup(directories):
- cache_dir = os.path.join('cache', '.mako.tmp')
- if os.path.exists(cache_dir):
- shutil.rmtree(cache_dir)
- return TemplateLookup(
- directories=directories,
- module_directory=cache_dir,
- output_encoding='utf-8',
- )
-
-
-def render_template(template_name, output_name, context, global_context):
- template = lookup.get_template(template_name)
- local_context = {}
- local_context.update(global_context)
- local_context.update(context)
- data = template.render_unicode(**local_context)
- if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
- with open(output_name, 'w+') as output:
- output.write(data)
- return data
-
-
-def template_deps(template_name):
- # We can cache here because depedencies should
- # not change between runs
- if cache.get(template_name, None) is None:
- template = lookup.get_template(template_name)
- dep_filenames = get_deps(template.filename)
- deps = [template.filename]
- for fname in dep_filenames:
- deps += template_deps(fname)
- cache[template_name] = tuple(deps)
- return list(cache[template_name])
diff --git a/nikola/md.py b/nikola/md.py
deleted file mode 100644
index 16bcec8..0000000
--- a/nikola/md.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Implementation of compile_html based on markdown."""
-
-__all__ = ['compile_html']
-
-import codecs
-import os
-import re
-
-from markdown import markdown
-
-
-def compile_html(source, dest):
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- with codecs.open(dest, "w+", "utf8") as out_file:
- with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
-
- output = markdown(data, ['fenced_code', 'codehilite'])
- # remove the H1 because there is "title" h1.
- output = re.sub(r'<h1>.*</h1>', '', output)
- # python-markdown's highlighter uses the class 'codehilite' to wrap
- # code, # instead of the standard 'code'. None of the standard pygments
- # stylesheets use this class, so swap it to be 'code'
- output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
- r'\1code\2', output)
- out_file.write(output)
diff --git a/nikola/nikola.py b/nikola/nikola.py
index aa43398..8b69d02 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -1,34 +1,35 @@
# -*- coding: utf-8 -*-
-import codecs
from collections import defaultdict
from copy import copy
-import datetime
import glob
-import json
import os
-from StringIO import StringIO
import sys
-import tempfile
-import urllib2
import urlparse
-from doit.tools import PythonInteractiveAction
import lxml.html
-from pygments import highlight
-from pygments.lexers import get_lexer_for_filename, TextLexer
-from pygments.formatters import HtmlFormatter
-try:
- import webassets
-except ImportError:
- webassets = None
+from yapsy.PluginManager import PluginManager
+
+if os.getenv('DEBUG'):
+ import logging
+ logging.basicConfig(level=logging.DEBUG)
+else:
+ import logging
+ logging.basicConfig(level=logging.ERROR)
from post import Post
import utils
+from plugin_categories import (
+ Command,
+ LateTask,
+ PageCompiler,
+ Task,
+ TemplateSystem,
+)
config_changed = utils.config_changed
-__all__ = ['Nikola', 'nikola_main']
+__all__ = ['Nikola']
class Nikola(object):
@@ -68,6 +69,7 @@ class Nikola(object):
'FILTERS': {},
'USE_BUNDLES': True,
'TAG_PAGES_ARE_INDEXES': False,
+ 'THEME': 'default',
'post_compilers': {
"rest": ['.txt', '.rst'],
"markdown": ['.md', '.mdown', '.markdown'],
@@ -75,29 +77,48 @@ class Nikola(object):
},
}
self.config.update(config)
- if not self.config['TRANSLATIONS']:
- self.config['TRANSLATIONS']={
- self.config['DEFAULT_LANG']: ''}
-
- if self.config['USE_BUNDLES'] and not webassets:
- self.config['USE_BUNDLES'] = False
+ self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
+ {self.config['DEFAULT_LANG']: ''})
- self.get_compile_html = utils.CompileHtmlGetter(
- self.config.pop('post_compilers'))
-
- self.GLOBAL_CONTEXT = self.config['GLOBAL_CONTEXT']
self.THEMES = utils.get_theme_chain(self.config['THEME'])
- self.templates_module = utils.get_template_module(
- utils.get_template_engine(self.THEMES), self.THEMES)
- self.template_deps = self.templates_module.template_deps
-
- self.theme_bundles = utils.get_theme_bundles(self.THEMES)
-
self.MESSAGES = utils.load_messages(self.THEMES,
self.config['TRANSLATIONS'])
- self.GLOBAL_CONTEXT['messages'] = self.MESSAGES
+ self.plugin_manager = PluginManager(categories_filter={
+ "Command": Command,
+ "Task": Task,
+ "LateTask": LateTask,
+ "TemplateSystem": TemplateSystem,
+ "PageCompiler": PageCompiler,
+ })
+ self.plugin_manager.setPluginInfoExtension('plugin')
+ self.plugin_manager.setPluginPlaces([
+ os.path.join(os.path.dirname(__file__), 'plugins'),
+ os.path.join(os.getcwd(), 'plugins'),
+ ])
+ self.plugin_manager.collectPlugins()
+
+ self.commands = {}
+ # Activate all command plugins
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory("Command"):
+ self.plugin_manager.activatePluginByName(pluginInfo.name)
+ pluginInfo.plugin_object.set_site(self)
+ pluginInfo.plugin_object.short_help = pluginInfo.description
+ self.commands[pluginInfo.name] = pluginInfo.plugin_object
+
+ # Activate all task plugins
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
+ self.plugin_manager.activatePluginByName(pluginInfo.name)
+ pluginInfo.plugin_object.set_site(self)
+
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
+ self.plugin_manager.activatePluginByName(pluginInfo.name)
+ pluginInfo.plugin_object.set_site(self)
+
+ # set global_context for template rendering
+ self.GLOBAL_CONTEXT = self.config.get('GLOBAL_CONTEXT', {})
+ self.GLOBAL_CONTEXT['messages'] = self.MESSAGES
self.GLOBAL_CONTEXT['_link'] = self.link
self.GLOBAL_CONTEXT['rel_link'] = self.rel_link
self.GLOBAL_CONTEXT['abs_link'] = self.abs_link
@@ -108,19 +129,74 @@ class Nikola(object):
'INDEX_DISPLAY_POST_COUNT']
self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
- self.DEPS_CONTEXT = {}
- for k, v in self.GLOBAL_CONTEXT.items():
- if isinstance(v, (str, unicode, int, float, dict)):
- self.DEPS_CONTEXT[k] = v
+ # Load template plugin
+ template_sys_name = utils.get_template_engine(self.THEMES)
+ pi = self.plugin_manager.getPluginByName(
+ template_sys_name, "TemplateSystem")
+ if pi is None:
+ sys.stderr.write("Error loading %s template system plugin\n"
+ % template_sys_name)
+ sys.exit(1)
+ self.template_system = pi.plugin_object
+ self.template_system.set_directories(
+ [os.path.join(utils.get_theme_path(name), "templates")
+ for name in self.THEMES])
+
+ # Load compiler plugins
+ self.compilers = {}
+ self.inverse_compilers = {}
+
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory(
+ "PageCompiler"):
+ self.compilers[pluginInfo.name] = \
+ pluginInfo.plugin_object.compile_html
+
+ def get_compiler(self, source_name):
+ """Get the correct compiler for a post from `conf.post_compilers`
+
+ To make things easier for users, the mapping in conf.py is
+ compiler->[extensions], although this is less convenient for us. The
+ majority of this function is reversing that dictionary and error
+ checking.
+ """
+ ext = os.path.splitext(source_name)[1]
+ try:
+ compile_html = self.inverse_compilers[ext]
+ except KeyError:
+ # Find the correct compiler for this files extension
+ langs = [lang for lang, exts in
+ self.config['post_compilers'].items()
+ if ext in exts]
+ if len(langs) != 1:
+ if len(set(langs)) > 1:
+ exit("Your file extension->compiler definition is"
+ "ambiguous.\nPlease remove one of the file extensions"
+ "from 'post_compilers' in conf.py\n(The error is in"
+ "one of %s)" % ', '.join(langs))
+ elif len(langs) > 1:
+ langs = langs[:1]
+ else:
+ exit("post_compilers in conf.py does not tell me how to "
+ "handle '%s' extensions." % ext)
+
+ lang = langs[0]
+ compile_html = self.compilers[lang]
+ self.inverse_compilers[ext] = compile_html
+
+ return compile_html
def render_template(self, template_name, output_name, context):
- data = self.templates_module.render_template(
- template_name, None, context, self.GLOBAL_CONTEXT)
+ local_context = {}
+ local_context["template_name"] = template_name
+ local_context.update(self.config['GLOBAL_CONTEXT'])
+ local_context.update(context)
+ data = self.template_system.render_template(
+ template_name, None, local_context)
assert output_name.startswith(self.config["OUTPUT_FOLDER"])
url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:]
- #this to support windows paths
+ # This is to support windows paths
url_part = "/".join(url_part.split(os.sep))
src = urlparse.urljoin(self.config["BLOG_URL"], url_part)
@@ -289,130 +365,39 @@ class Nikola(object):
return exists
def gen_tasks(self):
+ task_dep = []
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
+ for task in pluginInfo.plugin_object.gen_tasks():
+ yield task
+ if pluginInfo.plugin_object.is_default:
+ task_dep.append(pluginInfo.plugin_object.name)
- yield self.task_serve(output_folder=self.config['OUTPUT_FOLDER'])
- yield self.task_install_theme()
- yield self.task_bootswatch_theme()
- yield self.gen_task_new_post(self.config['post_pages'])
- yield self.gen_task_new_page(self.config['post_pages'])
- yield self.gen_task_copy_assets(themes=self.THEMES,
- output_folder=self.config['OUTPUT_FOLDER'],
- filters=self.config['FILTERS']
- )
- if webassets:
- yield self.gen_task_build_bundles(theme_bundles=self.theme_bundles,
- output_folder=self.config['OUTPUT_FOLDER'],
- filters=self.config['FILTERS']
- )
- yield self.gen_task_deploy(commands=self.config['DEPLOY_COMMANDS'])
- yield self.gen_task_sitemap(blog_url=self.config['BLOG_URL'],
- output_folder=self.config['OUTPUT_FOLDER']
- )
- yield self.gen_task_render_pages(
- translations=self.config['TRANSLATIONS'],
- post_pages=self.config['post_pages'],
- filters=self.config['FILTERS'])
- yield self.gen_task_render_sources(
- translations=self.config['TRANSLATIONS'],
- default_lang=self.config['DEFAULT_LANG'],
- output_folder=self.config['OUTPUT_FOLDER'],
- post_pages=self.config['post_pages'])
- yield self.gen_task_render_posts(
- translations=self.config['TRANSLATIONS'],
- default_lang=self.config['DEFAULT_LANG'],
- timeline=self.timeline
- )
- yield self.gen_task_render_indexes(
- translations=self.config['TRANSLATIONS'],
- messages=self.MESSAGES,
- output_folder=self.config['OUTPUT_FOLDER'],
- index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'],
- index_teasers=self.config['INDEX_TEASERS'],
- filters=self.config['FILTERS'],
- )
- yield self.gen_task_render_archive(
- translations=self.config['TRANSLATIONS'],
- messages=self.MESSAGES,
- output_folder=self.config['OUTPUT_FOLDER'],
- filters=self.config['FILTERS'],
- )
- yield self.gen_task_render_tags(
- translations=self.config['TRANSLATIONS'],
- messages=self.MESSAGES,
- blog_title=self.config['BLOG_TITLE'],
- blog_url=self.config['BLOG_URL'],
- blog_description=self.config['BLOG_DESCRIPTION'],
- output_folder=self.config['OUTPUT_FOLDER'],
- filters=self.config['FILTERS'],
- tag_pages_are_indexes=self.config['TAG_PAGES_ARE_INDEXES'],
- index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'],
- index_teasers=self.config['INDEX_TEASERS'],
- )
- yield self.gen_task_render_rss(
- translations=self.config['TRANSLATIONS'],
- blog_title=self.config['BLOG_TITLE'],
- blog_url=self.config['BLOG_URL'],
- blog_description=self.config['BLOG_DESCRIPTION'],
- output_folder=self.config['OUTPUT_FOLDER'])
- yield self.gen_task_render_galleries(
- max_image_size=self.config['MAX_IMAGE_SIZE'],
- thumbnail_size=self.config['THUMBNAIL_SIZE'],
- default_lang=self.config['DEFAULT_LANG'],
- output_folder=self.config['OUTPUT_FOLDER'],
- use_filename_as_title=self.config['USE_FILENAME_AS_TITLE'],
- blog_description=self.config['BLOG_DESCRIPTION']
- )
- yield self.gen_task_render_listings(
- listings_folder=self.config['LISTINGS_FOLDER'],
- default_lang=self.config['DEFAULT_LANG'],
- output_folder=self.config['OUTPUT_FOLDER'])
- yield self.gen_task_redirect(
- redirections=self.config['REDIRECTIONS'],
- output_folder=self.config['OUTPUT_FOLDER'])
- yield self.gen_task_copy_files(
- output_folder=self.config['OUTPUT_FOLDER'],
- files_folders=self.config['FILES_FOLDERS'],
- filters=self.config['FILTERS'])
-
- task_dep = [
- 'render_listings',
- 'render_archive',
- 'render_galleries',
- 'render_indexes',
- 'render_pages',
- 'render_posts',
- 'render_rss',
- 'render_sources',
- 'render_tags',
- 'copy_assets',
- 'copy_files',
- 'sitemap',
- 'redirect'
- ]
-
- if webassets:
- task_dep.append( 'build_bundles' )
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
+ for task in pluginInfo.plugin_object.gen_tasks():
+ yield task
+ if pluginInfo.plugin_object.is_default:
+ task_dep.append(pluginInfo.plugin_object.name)
yield {
'name': 'all',
'actions': None,
'clean': True,
'task_dep': task_dep
- }
+ }
def scan_posts(self):
"""Scan all the posts."""
if not self._scanned:
print "Scanning posts ",
targets = set([])
- for wildcard, destination, _, use_in_feeds in self.config['post_pages']:
+ for wildcard, destination, _, use_in_feeds in \
+ self.config['post_pages']:
print ".",
for base_path in glob.glob(wildcard):
post = Post(base_path, destination, use_in_feeds,
self.config['TRANSLATIONS'],
self.config['DEFAULT_LANG'],
self.config['BLOG_URL'],
- self.get_compile_html(base_path),
self.MESSAGES)
for lang, langpath in self.config['TRANSLATIONS'].items():
dest = (destination, langpath, post.pagenames[lang])
@@ -448,7 +433,8 @@ class Nikola(object):
post_name = os.path.splitext(post)[0]
context = {}
post = self.global_data[post_name]
- deps = post.deps(lang) + self.template_deps(template_name)
+ deps = post.deps(lang) + \
+ self.template_system.template_deps(template_name)
context['post'] = post
context['lang'] = lang
context['title'] = post.title(lang)
@@ -468,6 +454,7 @@ class Nikola(object):
deps_dict['NEXT_LINK'] = [post.next_post.permalink(lang)]
deps_dict['OUTPUT_FOLDER'] = self.config['OUTPUT_FOLDER']
deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS']
+ deps_dict['global'] = self.config['GLOBAL_CONTEXT']
task = {
'name': output_name.encode('utf-8'),
@@ -481,187 +468,11 @@ class Nikola(object):
yield utils.apply_filters(task, filters)
- def gen_task_render_pages(self, **kw):
- """Build final pages from metadata and HTML fragments.
-
- Required keyword arguments:
-
- translations
- post_pages
- """
- self.scan_posts()
- flag = False
- for lang in kw["translations"]:
- for wildcard, destination, template_name, _ in kw["post_pages"]:
- for task in self.generic_page_renderer(lang,
- wildcard, template_name, destination, kw["filters"]):
- # TODO: enable or remove
- #task['uptodate'] = task.get('uptodate', []) +\
- #[config_changed(kw)]
- task['basename'] = 'render_pages'
- flag = True
- yield task
- if flag == False: # No page rendered, yield a dummy task
- yield {
- 'basename': 'render_pages',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
- def gen_task_render_sources(self, **kw):
- """Publish the rst sources because why not?
-
- Required keyword arguments:
-
- translations
- default_lang
- post_pages
- output_folder
- """
- self.scan_posts()
- flag = False
- for lang in kw["translations"]:
- # TODO: timeline is global
- for post in self.timeline:
- output_name = os.path.join(kw['output_folder'],
- post.destination_path(lang, post.source_ext()))
- source = post.source_path
- if lang != kw["default_lang"]:
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
- yield {
- 'basename': 'render_sources',
- 'name': output_name.encode('utf8'),
- 'file_dep': [source],
- 'targets': [output_name],
- 'actions': [(utils.copy_file, (source, output_name))],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
- if flag == False: # No page rendered, yield a dummy task
- yield {
- 'basename': 'render_sources',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
- def gen_task_render_posts(self, **kw):
- """Build HTML fragments from metadata and reSt.
-
- Required keyword arguments:
-
- translations
- default_lang
- timeline
- """
- self.scan_posts()
- flag = False
- for lang in kw["translations"]:
- # TODO: timeline is global, get rid of it
- deps_dict = copy(kw)
- deps_dict.pop('timeline')
- for post in kw['timeline']:
- source = post.source_path
- dest = post.base_path
- if lang != kw["default_lang"]:
- dest += '.' + lang
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
- flag = True
- yield {
- 'basename': 'render_posts',
- 'name': dest.encode('utf-8'),
- 'file_dep': post.fragment_deps(lang),
- 'targets': [dest],
- 'actions': [(post.compile_html, [source, dest])],
- 'clean': True,
- 'uptodate': [config_changed(deps_dict)],
- }
- if flag == False: # Return a dummy task
- yield {
- 'basename': 'render_posts',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
- def gen_task_render_indexes(self, **kw):
- """Render post-per-page indexes.
- The default is 10.
-
- Required keyword arguments:
-
- translations
- output_folder
- index_display_post_count
- index_teasers
- """
- self.scan_posts()
- template_name = "index.tmpl"
- # TODO: timeline is global, get rid of it
- posts = [x for x in self.timeline if x.use_in_feeds]
- # Split in smaller lists
- lists = []
- while posts:
- lists.append(posts[:kw["index_display_post_count"]])
- posts = posts[kw["index_display_post_count"]:]
- num_pages = len(lists)
- if not lists:
- yield {
- 'basename': 'render_indexes',
- 'actions': [],
- }
- for lang in kw["translations"]:
- for i, post_list in enumerate(lists):
- context = {}
- if self.config.get("INDEXES_TITLE", ""):
- indexes_title = self.config['INDEXES_TITLE']
- else:
- indexes_title = self.config["BLOG_TITLE"]
- if not i:
- output_name = "index.html"
- context["title"] = indexes_title
- else:
- output_name = "index-%s.html" % i
- if self.config.get("INDEXES_PAGES", ""):
- indexes_pages = self.config["INDEXES_PAGES"] % i
- else:
- indexes_pages = " (" + kw["messages"][lang]["old posts page %d"] % i + ")"
- context["title"] = indexes_title + indexes_pages
- context["prevlink"] = None
- context["nextlink"] = None
- context['index_teasers'] = kw['index_teasers']
- if i > 1:
- context["prevlink"] = "index-%s.html" % (i - 1)
- if i == 1:
- context["prevlink"] = "index.html"
- if i < num_pages - 1:
- context["nextlink"] = "index-%s.html" % (i + 1)
- context["permalink"] = self.link("index", i, lang)
- output_name = os.path.join(
- kw['output_folder'], self.path("index", i, lang))
- for task in self.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- task['basename'] = 'render_indexes'
- yield task
-
def generic_post_list_renderer(self, lang, posts,
output_name, template_name, filters, extra_context):
"""Renders pages with lists of posts."""
- deps = self.template_deps(template_name)
+ deps = self.template_system.template_deps(template_name)
for post in posts:
deps += post.deps(lang)
context = {}
@@ -675,6 +486,7 @@ class Nikola(object):
deps_context = copy(context)
deps_context["posts"] = [(p.titles[lang], p.permalink(lang))
for p in posts]
+ deps_context["global"] = self.config['GLOBAL_CONTEXT']
task = {
'name': output_name.encode('utf8'),
'targets': [output_name],
@@ -686,1026 +498,3 @@ class Nikola(object):
}
yield utils.apply_filters(task, filters)
-
- def gen_task_render_archive(self, **kw):
- """Render the post archives.
-
- Required keyword arguments:
-
- translations
- messages
- output_folder
- """
- # TODO add next/prev links for years
- template_name = "list.tmpl"
- # TODO: posts_per_year is global, kill it
- for year, posts in self.posts_per_year.items():
- for lang in kw["translations"]:
- output_name = os.path.join(
- kw['output_folder'], self.path("archive", year, lang))
- post_list = [self.global_data[post] for post in posts]
- post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
- post_list.reverse()
- context = {}
- context["lang"] = lang
- context["items"] = [("[%s] %s" %
- (post.date, post.title(lang)), post.permalink(lang))
- for post in post_list]
- context["permalink"] = self.link("archive", year, lang)
- context["title"] = kw["messages"][lang]["Posts for year %s"]\
- % year
- for task in self.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- yield task
-
- # And global "all your years" page
- years = self.posts_per_year.keys()
- years.sort(reverse=True)
- template_name = "list.tmpl"
- kw['years'] = years
- for lang in kw["translations"]:
- context = {}
- output_name = os.path.join(
- kw['output_folder'], self.path("archive", None, lang))
- context["title"] = kw["messages"][lang]["Archive"]
- context["items"] = [(year, self.link("archive", year, lang))
- for year in years]
- context["permalink"] = self.link("archive", None, lang)
- for task in self.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- task['basename'] = 'render_archive'
- yield task
-
- def gen_task_render_tags(self, **kw):
- """Render the tag pages.
-
- Required keyword arguments:
-
- translations
- messages
- blog_title
- blog_url
- blog_description
- output_folder
- tag_pages_are_indexes
- index_display_post_count
- index_teasers
- """
- if not self.posts_per_tag:
- yield {
- 'basename': 'render_tags',
- 'actions': [],
- }
- return
- def page_name(tagname, i, lang):
- """Given tag, n, returns a page name."""
- name = self.path("tag", tag, lang)
- if i:
- name = name.replace('.html', '-%s.html' % i)
- return name
-
- for tag, posts in self.posts_per_tag.items():
- post_list = [self.global_data[post] for post in posts]
- post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
- post_list.reverse()
- for lang in kw["translations"]:
- #Render RSS
- output_name = os.path.join(kw['output_folder'],
- self.path("tag_rss", tag, lang))
- deps = []
- post_list = [self.global_data[post] for post in posts
- if self.global_data[post].use_in_feeds]
- post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
- post_list.reverse()
- for post in post_list:
- deps += post.deps(lang)
- yield {
- 'name': output_name.encode('utf8'),
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, "%s (%s)" % (kw["blog_title"], tag),
- kw["blog_url"], kw["blog_description"],
- post_list, output_name))],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- 'basename': 'render_tags'
- }
-
- # Render HTML
- if kw['tag_pages_are_indexes']:
- # We render a sort of index page collection using only
- # this tag's posts.
-
- # FIXME: deduplicate this with render_indexes
- template_name = "index.tmpl"
- # Split in smaller lists
- lists = []
- while post_list:
- lists.append(post_list[:kw["index_display_post_count"]])
- post_list = post_list[kw["index_display_post_count"]:]
- num_pages = len(lists)
- for i, post_list in enumerate(lists):
- context = {}
- # On a tag page, the feeds are the tag's feeds, plus the site's
- rss_link = \
- """<link rel="alternate" type="application/rss+xml" """\
- """type="application/rss+xml" title="RSS for tag """\
- """%s (%s)" href="%s">""" % \
- (tag, lang, self.link("tag_rss", tag, lang))
- context ['rss_link'] = rss_link
- output_name = os.path.join(kw['output_folder'],
- page_name(tag, i, lang))
- context["title"] = kw["messages"][lang][u"Posts about %s:"]\
- % tag
- context["prevlink"] = None
- context["nextlink"] = None
- context['index_teasers'] = kw['index_teasers']
- if i > 1:
- context["prevlink"] = os.path.basename(page_name(tag, i - 1, lang))
- if i == 1:
- context["prevlink"] = os.path.basename(page_name(tag, 0, lang))
- if i < num_pages - 1:
- context["nextlink"] = os.path.basename(page_name(tag, i + 1, lang))
- context["permalink"] = self.link("tag", tag, lang)
- context["tag"] = tag
- for task in self.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- task['basename'] = 'render_tags'
- yield task
- else:
- # We render a single flat link list with this tag's posts
- template_name = "tag.tmpl"
- output_name = os.path.join(kw['output_folder'],
- self.path("tag", tag, lang))
- context = {}
- context["lang"] = lang
- context["title"] = kw["messages"][lang][u"Posts about %s:"]\
- % tag
- context["items"] = [("[%s] %s" % (post.date, post.title(lang)),
- post.permalink(lang)) for post in post_list]
- context["permalink"] = self.link("tag", tag, lang)
- context["tag"] = tag
- for task in self.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- task['basename'] = 'render_tags'
- yield task
-
- # And global "all your tags" page
- tags = self.posts_per_tag.keys()
- tags.sort()
- template_name = "tags.tmpl"
- kw['tags'] = tags
- for lang in kw["translations"]:
- output_name = os.path.join(
- kw['output_folder'], self.path('tag_index', None, lang))
- context = {}
- context["title"] = kw["messages"][lang][u"Tags"]
- context["items"] = [(tag, self.link("tag", tag, lang))
- for tag in tags]
- context["permalink"] = self.link("tag_index", None, lang)
- for task in self.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- ):
- task['uptodate'] = task.get('updtodate', []) +\
- [config_changed(kw)]
- yield task
-
- def gen_task_render_rss(self, **kw):
- """Generate RSS feeds.
-
- Required keyword arguments:
-
- translations
- blog_title
- blog_url
- blog_description
- output_folder
- """
-
- self.scan_posts()
- # TODO: timeline is global, kill it
- for lang in kw["translations"]:
- output_name = os.path.join(kw['output_folder'],
- self.path("rss", None, lang))
- deps = []
- posts = [x for x in self.timeline if x.use_in_feeds][:10]
- for post in posts:
- deps += post.deps(lang)
- yield {
- 'basename': 'render_rss',
- 'name': output_name,
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"], kw["blog_url"],
- kw["blog_description"], posts, output_name))],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- def gen_task_render_listings(self, **kw):
- """
- Required keyword arguments:
-
- listings_folder
- output_folder
- default_lang
- """
-
- # Things to ignore in listings
- ignored_extensions = (".pyc",)
-
- def render_listing(in_name, out_name):
- with open(in_name, 'r') as fd:
- try:
- lexer = get_lexer_for_filename(in_name)
- except:
- lexer = TextLexer()
- code = highlight(fd.read(), lexer,
- HtmlFormatter(cssclass='code',
- linenos="table",
- nowrap=False,
- lineanchors=utils.slugify(f),
- anchorlinenos=True))
- title = os.path.basename(in_name)
- crumbs = out_name.split(os.sep)[1:-1] + [title]
- # TODO: write this in human
- paths = ['/'.join(['..'] * (len(crumbs) - 2 - i)) for i in range(len(crumbs[:-2]))] + ['.', '#']
- context = {
- 'code': code,
- 'title': title,
- 'crumbs': zip(paths, crumbs),
- 'lang': kw['default_lang'],
- 'description': title,
- }
- self.render_template('listing.tmpl', out_name, context)
- flag = True
- template_deps = self.template_deps('listing.tmpl')
- for root, dirs, files in os.walk(kw['listings_folder']):
- # Render all files
- for f in files:
- ext = os.path.splitext(f)[-1]
- if ext in ignored_extensions:
- continue
- flag = False
- in_name = os.path.join(root, f)
- out_name = os.path.join(
- kw['output_folder'],
- root,
- f) + '.html'
- yield {
- 'basename': 'render_listings',
- 'name': out_name.encode('utf8'),
- 'file_dep': template_deps + [in_name],
- 'targets': [out_name],
- 'actions': [(render_listing, [in_name, out_name])],
- }
- if flag:
- yield {
- 'basename': 'render_listings',
- 'actions': [],
- }
-
- def gen_task_render_galleries(self, **kw):
- """Render image galleries.
-
- Required keyword arguments:
-
- image_size
- thumbnail_size,
- default_lang,
- output_folder,
- use_filename_as_title
- """
-
- # FIXME: lots of work is done even when images don't change,
- # which should be moved into the task.
- # Also, this is getting complex enough to be refactored into a file.
-
- template_name = "gallery.tmpl"
-
- gallery_list = glob.glob("galleries/*")
- # Fail quick if we don't have galleries, so we don't
- # require PIL
- Image = None
- if not gallery_list:
- yield {
- 'basename': 'render_galleries',
- 'actions': [],
- }
- return
- try:
- import Image as _Image
- import ExifTags
- Image = _Image
- except ImportError:
- try:
- from PIL import Image as _Image, ExifTags
- Image = _Image
- except ImportError:
- pass
- if Image:
- def _resize_image(src, dst, max_size):
- im = Image.open(src)
- w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in exif.items():
- decoded = ExifTags.TAGS.get(tag, tag)
-
- if decoded == 'Orientation':
- if value == 3:
- im = im.rotate(180)
- elif value == 6:
- im = im.rotate(270)
- elif value == 8:
- im = im.rotate(90)
-
- break
-
- im.thumbnail(size, Image.ANTIALIAS)
- im.save(dst)
-
- else:
- utils.copy_file(src, dst)
-
- def create_thumb(src, dst):
- return _resize_image(src, dst, kw['thumbnail_size'])
-
- def create_resized_image(src, dst):
- return _resize_image(src, dst, kw['max_image_size'])
-
- dates = {}
- def image_date(src):
- if src not in dates:
- im = Image.open(src)
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in exif.items():
- decoded = ExifTags.TAGS.get(tag, tag)
- if decoded == 'DateTimeOriginal':
- try:
- dates[src] = datetime.datetime.strptime(value, r'%Y:%m:%d %H:%M:%S')
- break
- except ValueError: #invalid EXIF date
- pass
- if src not in dates:
- dates[src] = datetime.datetime.fromtimestamp(os.stat(src).st_mtime)
- return dates[src]
-
- else:
- create_thumb = utils.copy_file
- create_resized_image = utils.copy_file
-
- # gallery_path is "gallery/name"
- for gallery_path in gallery_list:
- # gallery_name is "name"
- gallery_name = os.path.basename(gallery_path)
- # output_gallery is "output/GALLERY_PATH/name"
- output_gallery = os.path.dirname(os.path.join(kw["output_folder"],
- self.path("gallery", gallery_name, None)))
- if not os.path.isdir(output_gallery):
- yield {
- 'basename': 'render_galleries',
- 'name': output_gallery,
- 'actions': [(os.makedirs, (output_gallery,))],
- 'targets': [output_gallery],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
- # image_list contains "gallery/name/image_name.jpg"
- image_list = glob.glob(gallery_path + "/*jpg") +\
- glob.glob(gallery_path + "/*JPG") +\
- glob.glob(gallery_path + "/*PNG") +\
- glob.glob(gallery_path + "/*png")
-
- # Filter ignore images
- try:
- def add_gallery_path(index):
- return "{0}/{1}".format(gallery_path, index)
-
- exclude_path = os.path.join(gallery_path, "exclude.meta")
- try:
- f = open(exclude_path, 'r')
- excluded_image_name_list = f.read().split()
- except IOError:
- excluded_image_name_list = []
-
- excluded_image_list = map(add_gallery_path,
- excluded_image_name_list)
- image_set = set(image_list) - set(excluded_image_list)
- image_list = list(image_set)
- except IOError:
- pass
-
- image_list = [x for x in image_list if "thumbnail" not in x]
- # Sort by date
- image_list.sort(cmp=lambda a,b: cmp(image_date(a), image_date(b)))
- image_name_list = [os.path.basename(x) for x in image_list]
-
- thumbs = []
- # Do thumbnails and copy originals
- for img, img_name in zip(image_list, image_name_list):
- # img is "galleries/name/image_name.jpg"
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- # thumb_path is
- # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
- thumb_path = os.path.join(output_gallery,
- fname + ".thumbnail" + ext)
- # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
- orig_dest_path = os.path.join(output_gallery, img_name)
- thumbs.append(os.path.basename(thumb_path))
- yield {
- 'basename': 'render_galleries',
- 'name': thumb_path,
- 'file_dep': [img],
- 'targets': [thumb_path],
- 'actions': [
- (create_thumb, (img, thumb_path))
- ],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
- yield {
- 'basename': 'render_galleries',
- 'name': orig_dest_path,
- 'file_dep': [img],
- 'targets': [orig_dest_path],
- 'actions': [
- (create_resized_image, (img, orig_dest_path))
- ],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- # Remove excluded images
- if excluded_image_name_list:
- for img, img_name in zip(excluded_image_list,
- excluded_image_name_list):
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- excluded_thumb_dest_path = os.path.join(output_gallery,
- fname + ".thumbnail" + ext)
- excluded_dest_path = os.path.join(output_gallery, img_name)
- yield {
- 'basename': 'render_galleries',
- 'name': excluded_thumb_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_thumb_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_thumb_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
- yield {
- 'basename': 'render_galleries',
- 'name': excluded_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- output_name = os.path.join(output_gallery, "index.html")
- context = {}
- context["lang"] = kw["default_lang"]
- context["title"] = os.path.basename(gallery_path)
- context["description"] = kw["blog_description"]
- if kw['use_filename_as_title']:
- img_titles = ['title="%s"' % utils.unslugify(fn[:-4])
- for fn in image_name_list]
- else:
- img_titles = [''] * len(image_name_list)
- context["images"] = zip(image_name_list, thumbs, img_titles)
- context["permalink"] = self.link("gallery", gallery_name, None)
-
- # Use galleries/name/index.txt to generate a blurb for
- # the gallery, if it exists
- index_path = os.path.join(gallery_path, "index.txt")
- index_dst_path = os.path.join(gallery_path, "index.html")
- if os.path.exists(index_path):
- compile_html = self.get_compile_html(index_path)
- yield {
- 'basename': 'render_galleries',
- 'name': index_dst_path.encode('utf-8'),
- 'file_dep': [index_path],
- 'targets': [index_dst_path],
- 'actions': [(compile_html,
- [index_path, index_dst_path])],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- file_dep = self.template_deps(template_name) + image_list
-
- def render_gallery(output_name, context, index_dst_path):
- if os.path.exists(index_dst_path):
- with codecs.open(index_dst_path, "rb", "utf8") as fd:
- context['text'] = fd.read()
- file_dep.append(index_dst_path)
- else:
- context['text'] = ''
- self.render_template(template_name, output_name, context)
-
- yield {
- 'basename': 'render_galleries',
- 'name': gallery_path,
- 'file_dep': file_dep,
- 'targets': [output_name],
- 'actions': [(render_gallery,
- (output_name, context, index_dst_path))],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- @staticmethod
- def gen_task_redirect(**kw):
- """Generate redirections.
-
- Required keyword arguments:
-
- redirections
- output_folder
- """
-
- def create_redirect(src, dst):
- with codecs.open(src, "wb+", "utf8") as fd:
- fd.write(('<head>' +
- '<meta HTTP-EQUIV="REFRESH" content="0; url=%s">' +
- '</head>') % dst)
-
- if not kw['redirections']:
- # If there are no redirections, still needs to create a
- # dummy action so dependencies don't fail
- yield {
- 'basename': 'redirect',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
- else:
- for src, dst in kw["redirections"]:
- src_path = os.path.join(kw["output_folder"], src)
- yield {
- 'basename': 'redirect',
- 'name': src_path,
- 'targets': [src_path],
- 'actions': [(create_redirect, (src_path, dst))],
- 'clean': True,
- 'uptodate': [config_changed(kw)],
- }
-
- @staticmethod
- def gen_task_copy_files(**kw):
- """Copy static files into the output folder.
-
- required keyword arguments:
-
- output_folder
- files_folders
- """
-
- flag = False
- for src in kw['files_folders']:
- dst = kw['output_folder']
- filters = kw['filters']
- real_dst = os.path.join(dst, kw['files_folders'][src])
- for task in utils.copy_tree(src, real_dst, link_cutoff=dst):
- flag = True
- task['basename'] = 'copy_files'
- task['uptodate'] = task.get('uptodate', []) +\
- [config_changed(kw)]
- yield utils.apply_filters(task, filters)
- if not flag:
- yield {
- 'basename': 'copy_files',
- 'actions': (),
- }
-
- @staticmethod
- def gen_task_copy_assets(**kw):
- """Create tasks to copy the assets of the whole theme chain.
-
- If a file is present on two themes, use the version
- from the "youngest" theme.
-
- Required keyword arguments:
-
- themes
- output_folder
-
- """
- tasks = {}
- for theme_name in kw['themes']:
- src = os.path.join(utils.get_theme_path(theme_name), 'assets')
- dst = os.path.join(kw['output_folder'], 'assets')
- for task in utils.copy_tree(src, dst):
- if task['name'] in tasks:
- continue
- tasks[task['name']] = task
- task['uptodate'] = task.get('uptodate', []) + \
- [config_changed(kw)]
- task['basename'] = 'copy_assets'
- yield utils.apply_filters(task, kw['filters'])
-
- @staticmethod
- def gen_task_build_bundles(**kw):
- """Create tasks to build bundles from theme assets.
-
- theme_bundles
- output_folder
- filters
- """
-
- def build_bundle(output, inputs):
- env = webassets.Environment(
- os.path.join(kw['output_folder'], os.path.dirname(output)),
- os.path.dirname(output))
- bundle = webassets.Bundle(*inputs,
- output=os.path.basename(output))
- env.register(output, bundle)
- # This generates the file
- env[output].urls()
-
- flag = False
- for name, files in kw['theme_bundles'].items():
- output_path = os.path.join(kw['output_folder'], name)
- dname = os.path.dirname(name)
- file_dep = [os.path.join('output', dname, fname)
- for fname in files]
- task = {
- 'task_dep': ['copy_assets', 'copy_files'],
- 'file_dep': file_dep,
- 'name': name,
- 'actions': [(build_bundle, (name, files))],
- 'targets': [os.path.join(kw['output_folder'], name)],
- 'basename': 'build_bundles',
- 'uptodate': [config_changed(kw)]
- }
- flag = True
- yield utils.apply_filters(task, kw['filters'])
- if flag == False: # No page rendered, yield a dummy task
- yield {
- 'basename': 'build_bundles',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
-
- @staticmethod
- def new_post(post_pages, is_post=True):
- # Guess where we should put this
- for path, _, _, use_in_rss in post_pages:
- if use_in_rss == is_post:
- break
- else:
- path = post_pages[0][0]
-
- print "Creating New Post"
- print "-----------------\n"
- title = raw_input("Enter title: ").decode(sys.stdin.encoding)
- slug = utils.slugify(title)
- data = u'\n'.join([
- title,
- slug,
- datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
- ])
- output_path = os.path.dirname(path)
- meta_path = os.path.join(output_path, slug + ".meta")
- pattern = os.path.basename(path)
- if pattern.startswith("*."):
- suffix = pattern[1:]
- else:
- suffix = ".txt"
- txt_path = os.path.join(output_path, slug + suffix)
-
- if os.path.isfile(meta_path) or os.path.isfile(txt_path):
- print "The title already exists!"
- exit()
-
- with codecs.open(meta_path, "wb+", "utf8") as fd:
- fd.write(data)
- with codecs.open(txt_path, "wb+", "utf8") as fd:
- fd.write(u"Write your post here.")
- print "Your post's metadata is at: ", meta_path
- print "Your post's text is at: ", txt_path
-
- @classmethod
- def new_page(cls):
- cls.new_post(False)
-
- @classmethod
- def gen_task_new_post(cls, post_pages):
- """Create a new post (interactive)."""
- yield {
- "basename": "new_post",
- "actions": [PythonInteractiveAction(cls.new_post, (post_pages,))],
- }
-
- @classmethod
- def gen_task_new_page(cls, post_pages):
- """Create a new post (interactive)."""
- yield {
- "basename": "new_page",
- "actions": [PythonInteractiveAction(cls.new_post,
- (post_pages, False,))],
- }
-
- @staticmethod
- def gen_task_deploy(**kw):
- """Deploy site.
-
- Required keyword arguments:
-
- commands
-
- """
- yield {
- "basename": "deploy",
- "actions": kw['commands'],
- "verbosity": 2,
- }
-
- @staticmethod
- def gen_task_sitemap(**kw):
- """Generate Google sitemap.
-
- Required keyword arguments:
-
- blog_url
- output_folder
- """
-
- output_path = os.path.abspath(kw['output_folder'])
- sitemap_path = os.path.join(output_path, "sitemap.xml.gz")
-
- def sitemap():
- # Generate config
- config_data = """<?xml version="1.0" encoding="UTF-8"?>
- <site
- base_url="%s"
- store_into="%s"
- verbose="1" >
- <directory path="%s" url="%s" />
- <filter action="drop" type="wildcard" pattern="*~" />
- <filter action="drop" type="regexp" pattern="/\.[^/]*" />
- </site>""" % (
- kw["blog_url"],
- sitemap_path,
- output_path,
- kw["blog_url"],
- )
- config_file = tempfile.NamedTemporaryFile(delete=False)
- config_file.write(config_data)
- config_file.close()
-
- # Generate sitemap
- import sitemap_gen as smap
- sitemap = smap.CreateSitemapFromFile(config_file.name, True)
- if not sitemap:
- smap.output.Log('Configuration file errors -- exiting.', 0)
- else:
- sitemap.Generate()
- smap.output.Log('Number of errors: %d' %
- smap.output.num_errors, 1)
- smap.output.Log('Number of warnings: %d' %
- smap.output.num_warns, 1)
- os.unlink(config_file.name)
-
- yield {
- "basename": "sitemap",
- "task_dep": [
- "render_archive",
- "render_indexes",
- "render_pages",
- "render_posts",
- "render_rss",
- "render_sources",
- "render_tags"],
- "targets": [sitemap_path],
- "actions": [(sitemap,)],
- "uptodate": [config_changed(kw)],
- "clean": True,
- }
-
- @staticmethod
- def task_serve(**kw):
- """
- Start test server. (doit serve [--address 127.0.0.1] [--port 8000])
- By default, the server runs on port 8000 on the IP address 127.0.0.1.
-
- required keyword arguments:
-
- output_folder
- """
-
- def serve(address, port):
- from BaseHTTPServer import HTTPServer
- from SimpleHTTPServer import SimpleHTTPRequestHandler
-
- class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
- extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
- extensions_map[""] = "text/plain"
-
- os.chdir(kw['output_folder'])
-
- httpd = HTTPServer((address, port), OurHTTPRequestHandler)
- sa = httpd.socket.getsockname()
- print "Serving HTTP on", sa[0], "port", sa[1], "..."
- httpd.serve_forever()
-
- yield {
- "basename": 'serve',
- "actions": [(serve,)],
- "verbosity": 2,
- "params": [{'short': 'a',
- 'name': 'address',
- 'long': 'address',
- 'type': str,
- 'default': '127.0.0.1',
- 'help': 'Bind address (default: 127.0.0.1)'},
- {'short': 'p',
- 'name': 'port',
- 'long': 'port',
- 'type': int,
- 'default': 8000,
- 'help': 'Port number (default: 8000)'}],
- }
-
- @staticmethod
- def task_install_theme():
- """Install theme. (doit install_theme -n themename [-u URL]|[-l])."""
-
- def install_theme(name, url, listing):
- if name is None and not listing:
- print "This command needs either the -n or the -l option."
- return False
- data = urllib2.urlopen(url).read()
- data = json.loads(data)
- if listing:
- print "Themes:"
- print "-------"
- for theme in sorted(data.keys()):
- print theme
- return True
- else:
- if name in data:
- if os.path.isfile("themes"):
- raise IOError("'themes' isn't a directory!")
- elif not os.path.isdir("themes"):
- try:
- os.makedirs("themes")
- except:
- raise OSError("mkdir 'theme' error!")
- print 'Downloading: %s' % data[name]
- zip_file = StringIO()
- zip_file.write(urllib2.urlopen(data[name]).read())
- print 'Extracting: %s into themes' % name
- utils.extract_all(zip_file)
- else:
- print "Can't find theme %s" % name
- return False
-
- yield {
- "basename": 'install_theme',
- "actions": [(install_theme,)],
- "verbosity": 2,
- "params": [
- {
- 'short': 'u',
- 'name': 'url',
- 'long': 'url',
- 'type': str,
- 'default': 'http://nikola.ralsina.com.ar/themes/index.json',
- 'help': 'URL for theme collection.'
- },
- {
- 'short': 'l',
- 'name': 'listing',
- 'long': 'list',
- 'type': bool,
- 'default': False,
- 'help': 'List available themes.'
- },
- {
- 'short': 'n',
- 'name': 'name',
- 'long': 'name',
- 'type': str,
- 'default': None,
- 'help': 'Name of theme to install.'
- }],
- }
-
- @staticmethod
- def task_bootswatch_theme():
- """Given a swatch name and a parent theme, creates a custom theme."""
- def bootswatch_theme(name, parent, swatch):
- print "Creating %s theme from %s and %s" % (name, swatch, parent)
- try:
- os.makedirs(os.path.join('themes', name, 'assets', 'css'))
- except:
- pass
- for fname in ('bootstrap.min.css', 'bootstrap.css'):
- url = 'http://bootswatch.com/%s/%s' % (swatch, fname)
- print "Downloading: ", url
- data = urllib2.urlopen(url).read()
- with open(os.path.join(
- 'themes', name, 'assets', 'css', fname), 'wb+') as output:
- output.write(data)
-
- with open(os.path.join('themes', name, 'parent'), 'wb+') as output:
- output.write(parent)
- print 'Theme created. Change the THEME setting to "%s" to use it.'\
- % name
-
- yield {
- "basename": 'bootswatch_theme',
- "actions": [(bootswatch_theme,)],
- "verbosity": 2,
- "params": [
- {
- 'short': 'p',
- 'name': 'parent',
- 'long': 'parent',
- 'type': str,
- 'default': 'site',
- 'help': 'Name of parent theme.'
- },
- {
- 'short': 's',
- 'name': 'swatch',
- 'long': 'swatch',
- 'type': str,
- 'default': 'slate',
- 'help': 'Name of the swatch from bootswatch.com'
- },
- {
- 'short': 'n',
- 'name': 'name',
- 'long': 'name',
- 'type': str,
- 'default': 'custom',
- 'help': 'Name of the new theme'
- }
- ],
- }
-
-
-def nikola_main():
- print "Starting doit..."
- os.system("doit -f %s" % __file__)
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
new file mode 100644
index 0000000..cc59b24
--- /dev/null
+++ b/nikola/plugin_categories.py
@@ -0,0 +1,85 @@
+__all__ = [
+ 'Command',
+ 'LateTask',
+ 'PageCompiler',
+ 'Task',
+ 'TemplateSystem'
+]
+
+from yapsy.IPlugin import IPlugin
+
+
+class BasePlugin(IPlugin):
+ """Base plugin class."""
+
+ def set_site(self, site):
+ """Sets site, which is a Nikola instance."""
+ self.site = site
+
+
+class Command(BasePlugin):
+ """These plugins are exposed via the command line."""
+
+ name = "dummy_command"
+
+ short_help = "A short explanation."
+
+ def run(self):
+ """Do whatever this command does."""
+ raise Exception("Implement Me First")
+
+
+class BaseTask(BasePlugin):
+ """PLugins of this type are task generators."""
+
+ name = "dummy_task"
+
+ # default tasks are executed by default.
+ # the others have to be specifie in the command line.
+ is_default = True
+
+ def gen_tasks(self):
+ """Task generator."""
+ raise Exception("Implement Me First")
+
+
+class Task(BaseTask):
+ """PLugins of this type are task generators."""
+
+
+class LateTask(BaseTask):
+ """Plugins of this type are executed after all plugins of type Task."""
+
+ name = "dummy_latetask"
+
+
+class TemplateSystem(object):
+ """Plugins of this type wrap templating systems."""
+
+ name = "dummy templates"
+
+ def set_directories(self, directories):
+ """Sets the list of folders where templates are located."""
+ raise Exception("Implement Me First")
+
+ def template_deps(self, template_name):
+ """Returns filenames which are dependencies for a template."""
+ raise Exception("Implement Me First")
+
+ def render_template(name, output_name, context):
+ """Renders template to a file using context.
+
+ This must save the data to output_name *and* return it
+ so that the caller may do additional processing.
+ """
+ raise Exception("Implement Me First")
+
+
+class PageCompiler(object):
+ """Plugins that compile text files into HTML."""
+
+ name = "dummy compiler"
+
+ def compile_html(self, source, dest):
+ """Compile the source, save it on dest."""
+ raise Exception("Implement Me First")
diff --git a/nikola/plugins/command_bootswatch_theme.plugin b/nikola/plugins/command_bootswatch_theme.plugin
new file mode 100644
index 0000000..f75f734
--- /dev/null
+++ b/nikola/plugins/command_bootswatch_theme.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = bootswatch_theme
+Module = command_bootswatch_theme
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Given a swatch name and a parent theme, creates a custom theme.
+
diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command_bootswatch_theme.py
new file mode 100644
index 0000000..f077eb1
--- /dev/null
+++ b/nikola/plugins/command_bootswatch_theme.py
@@ -0,0 +1,47 @@
+from optparse import OptionParser
+import os
+import urllib2
+
+from nikola.plugin_categories import Command
+
+
+class CommandBootswatchTheme(Command):
+ """Given a swatch name and a parent theme, creates a custom theme."""
+
+ name = "bootswatch_theme"
+
+ def run(self, *args):
+ """Given a swatch name and a parent theme, creates a custom theme."""
+
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ parser.add_option("-n", "--name", dest="name",
+ help="New theme name (default: custom)", default='custom')
+ parser.add_option("-s", "--swatch", dest="swatch",
+ help="Name of the swatch from bootswatch.com (default: slate)",
+ default='slate')
+ parser.add_option("-p", "--parent", dest="parent",
+ help="Parent theme name (default: site)", default='site')
+ (options, args) = parser.parse_args(list(args))
+
+ name = options.name
+ swatch = options.swatch
+ parent = options.parent
+
+ print "Creating '%s' theme from '%s' and '%s'" % (
+ name, swatch, parent)
+ try:
+ os.makedirs(os.path.join('themes', name, 'assets', 'css'))
+ except:
+ pass
+ for fname in ('bootstrap.min.css', 'bootstrap.css'):
+ url = 'http://bootswatch.com/%s/%s' % (swatch, fname)
+ print "Downloading: ", url
+ data = urllib2.urlopen(url).read()
+ with open(os.path.join(
+ 'themes', name, 'assets', 'css', fname), 'wb+') as output:
+ output.write(data)
+
+ with open(os.path.join('themes', name, 'parent'), 'wb+') as output:
+ output.write(parent)
+ print 'Theme created. Change the THEME setting to "%s" to use it.'\
+ % name
diff --git a/nikola/plugins/command_build.plugin b/nikola/plugins/command_build.plugin
new file mode 100644
index 0000000..7d029a7
--- /dev/null
+++ b/nikola/plugins/command_build.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = build
+Module = command_build
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Build the site.
+
diff --git a/nikola/plugins/command_build.py b/nikola/plugins/command_build.py
new file mode 100644
index 0000000..cface15
--- /dev/null
+++ b/nikola/plugins/command_build.py
@@ -0,0 +1,32 @@
+import os
+import tempfile
+
+from nikola.plugin_categories import Command
+
+
+class CommandBuild(Command):
+ """Build the site."""
+
+ name = "build"
+
+ def run(self, *args):
+ """Build the site using doit."""
+
+ # FIXME: this is crap, do it right
+ with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as dodo:
+ dodo.write('''
+from doit.reporter import ExecutedOnlyReporter
+DOIT_CONFIG = {
+ 'reporter': ExecutedOnlyReporter,
+ 'default_tasks': ['render_site'],
+}
+from nikola import Nikola
+import conf
+SITE = Nikola(**conf.__dict__)
+
+
+def task_render_site():
+ return SITE.gen_tasks()
+ ''')
+ dodo.flush()
+ os.system('doit -f %s -d . %s' % (dodo.name, ' '.join(args)))
diff --git a/nikola/plugins/command_check.plugin b/nikola/plugins/command_check.plugin
new file mode 100644
index 0000000..d4dcd1c
--- /dev/null
+++ b/nikola/plugins/command_check.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = check
+Module = command_check
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Check the generated site
+
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
new file mode 100644
index 0000000..ce1e2e3
--- /dev/null
+++ b/nikola/plugins/command_check.py
@@ -0,0 +1,109 @@
+from optparse import OptionParser
+import os
+import sys
+import urllib
+from urlparse import urlparse
+
+import lxml.html
+
+from nikola.plugin_categories import Command
+
+
+class CommandCheck(Command):
+ """Check the generated site."""
+
+ name = "check"
+
+ def run(self, *args):
+ """Check the generated site."""
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ parser.add_option('-l', '--check-links', dest='links',
+ action='store_true',
+ help='Check for dangling links.')
+ parser.add_option('-f', '--check-files', dest='files',
+ action='store_true',
+ help='Check for unknown files.')
+
+ (options, args) = parser.parse_args(list(args))
+ if options.links:
+ scan_links()
+ if options.files:
+ scan_files()
+
+existing_targets = set([])
+
+
+def analize(task):
+ try:
+ filename = task.split(":")[-1]
+ d = lxml.html.fromstring(open(filename).read())
+ for l in d.iterlinks():
+ target = l[0].attrib[l[1]]
+ if target == "#":
+ continue
+ parsed = urlparse(target)
+ if parsed.scheme:
+ continue
+ if parsed.fragment:
+ target = target.split('#')[0]
+ target_filename = os.path.abspath(
+ os.path.join(os.path.dirname(filename),
+ urllib.unquote(target)))
+ if target_filename not in existing_targets:
+ if os.path.exists(target_filename):
+ existing_targets.add(target_filename)
+ else:
+ print "In %s broken link: " % filename, target
+ if '--find-sources' in sys.argv:
+ print "Possible sources:"
+ print os.popen(
+ 'nikola build list --deps %s' % task, 'r').read()
+ print "===============================\n"
+
+ except Exception as exc:
+ print "Error with:", filename, exc
+
+
+def scan_links():
+ print "Checking Links:\n===============\n"
+ for task in os.popen('nikola build list --all', 'r').readlines():
+ task = task.strip()
+ if task.split(':')[0] in (
+ 'render_tags',
+ 'render_archive',
+ 'render_galleries',
+ 'render_indexes',
+ 'render_pages',
+ 'render_site') and '.html' in task:
+ analize(task)
+
+
+def scan_files():
+ print "Checking Files:\n===============\n"
+ task_fnames = set([])
+ real_fnames = set([])
+ # First check that all targets are generated in the right places
+ for task in os.popen('nikola build list --all', 'r').readlines():
+ task = task.strip()
+ if 'output' in task and ':' in task:
+ fname = task.split(':')[-1]
+ task_fnames.add(fname)
+ # And now check that there are no non-target files
+ for root, dirs, files in os.walk('output'):
+ for src_name in files:
+ fname = os.path.join(root, src_name)
+ real_fnames.add(fname)
+
+ only_on_output = list(real_fnames - task_fnames)
+ if only_on_output:
+ only_on_output.sort()
+ print "\nFiles from unknown origins:\n"
+ for f in only_on_output:
+ print f
+
+ only_on_input = list(task_fnames - real_fnames)
+ if only_on_input:
+ only_on_input.sort()
+ print "\nFiles not generated:\n"
+ for f in only_on_input:
+ print f
diff --git a/nikola/plugins/command_deploy.plugin b/nikola/plugins/command_deploy.plugin
new file mode 100644
index 0000000..c8776b5
--- /dev/null
+++ b/nikola/plugins/command_deploy.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = deploy
+Module = command_deploy
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Deploy the site
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
new file mode 100644
index 0000000..cb2eb41
--- /dev/null
+++ b/nikola/plugins/command_deploy.py
@@ -0,0 +1,16 @@
+from optparse import OptionParser
+import os
+
+from nikola.plugin_categories import Command
+
+
+class Deploy(Command):
+ """Deploy site. """
+ name = "deploy"
+
+ def run(self, *args):
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ (options, args) = parser.parse_args(list(args))
+ for command in self.site.config['DEPLOY_COMMANDS']:
+ print "==>", command
+ os.system(command)
diff --git a/nikola/plugins/command_import_wordpress.plugin b/nikola/plugins/command_import_wordpress.plugin
new file mode 100644
index 0000000..a2477b9
--- /dev/null
+++ b/nikola/plugins/command_import_wordpress.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = import_wordpress
+Module = command_import_wordpress
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Import a wordpress site from a XML dump (requires markdown).
+
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py
new file mode 100644
index 0000000..e75d022
--- /dev/null
+++ b/nikola/plugins/command_import_wordpress.py
@@ -0,0 +1,163 @@
+import codecs
+import os
+from urlparse import urlparse
+from urllib import urlopen
+
+from lxml import etree, html
+from mako.template import Template
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+links = {}
+
+
+class CommandImportWordpress(Command):
+ """Import a wordpress dump."""
+
+ name = "import_wordpress"
+
+ def run(self, fname=None):
+ # Parse the data
+ if fname is None:
+ print "Usage: nikola import_wordpress wordpress_dump.xml"
+ return
+ context = {}
+ with open(fname) as fd:
+ xml = []
+ for line in fd:
+ # These explode etree and are useless
+ if '<atom:link rel=' in line:
+ continue
+ xml.append(line)
+ xml = '\n'.join(xml)
+
+ tree = etree.fromstring(xml)
+ channel = tree.find('channel')
+
+ context['DEFAULT_LANG'] = get_text_tag(channel, 'language', 'en')[:2]
+ context['BLOG_TITLE'] = get_text_tag(
+ channel, 'title', 'PUT TITLE HERE')
+ context['BLOG_DESCRIPTION'] = get_text_tag(
+ channel, 'description', 'PUT DESCRIPTION HERE')
+ context['BLOG_URL'] = get_text_tag(channel, 'link', '#')
+ author = channel.find('{http://wordpress.org/export/1.2/}author')
+ context['BLOG_EMAIL'] = get_text_tag(
+ author,
+ '{http://wordpress.org/export/1.2/}author_email',
+ "joe@example.com")
+ context['BLOG_AUTHOR'] = get_text_tag(
+ author,
+ '{http://wordpress.org/export/1.2/}author_display_name',
+ "Joe Example")
+ context['POST_PAGES'] = '''(
+ ("posts/*.wp", "posts", "post.tmpl", True),
+ ("stories/*.wp", "stories", "story.tmpl", False),
+ )'''
+ context['POST_COMPILERS'] = '''{
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown', '.wp'),
+ "html": ('.html', '.htm')
+ }
+ '''
+
+ # Generate base site
+ os.system('nikola init new_site')
+ conf_template = Template(filename=os.path.join(
+ os.path.dirname(utils.__file__), 'data', 'samplesite', 'conf.py.in'))
+ with codecs.open(os.path.join('new_site', 'conf.py'),
+ 'w+', 'utf8') as fd:
+ fd.write(conf_template.render(**context))
+
+ # Import posts
+ for item in channel.findall('item'):
+ import_attachment(item)
+ for item in channel.findall('item'):
+ import_item(item)
+
+
+def replacer(dst):
+ return links.get(dst, dst)
+
+
+def get_text_tag(tag, name, default):
+ t = tag.find(name)
+ if t is not None:
+ return t.text
+ else:
+ return default
+
+
+def import_attachment(item):
+ post_type = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}post_type', 'post')
+ if post_type == 'attachment':
+ url = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}attachment_url', 'foo')
+ link = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}link', 'foo')
+ path = urlparse(url).path
+ dst_path = os.path.join(*(['new_site', 'files']
+ + list(path.split('/'))))
+ dst_dir = os.path.dirname(dst_path)
+ if not os.path.isdir(dst_dir):
+ os.makedirs(dst_dir)
+ print "Downloading %s => %s" % (url, dst_path)
+ with open(dst_path, 'wb+') as fd:
+ fd.write(urlopen(url).read())
+ dst_url = '/'.join(dst_path.split(os.sep)[2:])
+ links[link] = '/' + dst_url
+ links[url] = '/' + dst_url
+ return
+
+
+def import_item(item):
+ """Takes an item from the feed and creates a post file."""
+ title = get_text_tag(item, 'title', 'NO TITLE')
+ # link is something like http://foo.com/2012/09/01/hello-world/
+ # So, take the path, utils.slugify it, and that's our slug
+ slug = utils.slugify(urlparse(get_text_tag(item, 'link', None)).path)
+ description = get_text_tag(item, 'description', '')
+ post_date = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}post_date', None)
+ post_type = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}post_type', 'post')
+ status = get_text_tag(item,
+ '{http://wordpress.org/export/1.2/}status', 'publish')
+ content = get_text_tag(item,
+ '{http://purl.org/rss/1.0/modules/content/}encoded', '')
+
+ tags = []
+ if status != 'publish':
+ tags.append('draft')
+ for tag in item.findall('category'):
+ text = tag.text
+ if text == 'Uncategorized':
+ continue
+ tags.append(text)
+
+ if post_type == 'attachment':
+ return
+ elif post_type == 'post':
+ out_folder = 'posts'
+ else:
+ out_folder = 'stories'
+ # Write metadata
+ with codecs.open(os.path.join('new_site', out_folder, slug + '.meta'),
+ "w+", "utf8") as fd:
+ fd.write(u'%s\n' % title)
+ fd.write(u'%s\n' % slug)
+ fd.write(u'%s\n' % post_date)
+ fd.write(u'%s\n' % ','.join(tags))
+ fd.write(u'\n')
+ fd.write(u'%s\n' % description)
+ with open(os.path.join(
+ 'new_site', out_folder, slug + '.wp'), "wb+") as fd:
+ if content.strip():
+ try:
+ doc = html.document_fromstring(content)
+ doc.rewrite_links(replacer)
+ fd.write(html.tostring(doc, encoding='utf8'))
+ except:
+ import pdb
+ pdb.set_trace()
diff --git a/nikola/plugins/command_init.plugin b/nikola/plugins/command_init.plugin
new file mode 100644
index 0000000..3c6bd21
--- /dev/null
+++ b/nikola/plugins/command_init.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = init
+Module = command_init
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create a new site.
+
diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command_init.py
new file mode 100644
index 0000000..a032370
--- /dev/null
+++ b/nikola/plugins/command_init.py
@@ -0,0 +1,34 @@
+from optparse import OptionParser
+import os
+import shutil
+
+import nikola
+from nikola.plugin_categories import Command
+
+
+class CommandInit(Command):
+ """Create a new site."""
+
+ name = "init"
+
+ usage = """Usage: nikola init folder [options].
+
+That will create a sample site in the specified folder.
+The destination folder must not exist.
+"""
+
+ def run(self, *args):
+ """Create a new site."""
+ parser = OptionParser(usage=self.usage)
+ (options, args) = parser.parse_args(list(args))
+
+ target = args[0]
+ if target is None:
+ print self.usage
+ else:
+ src = os.path.join(os.path.dirname(nikola.__file__),
+ 'data', 'samplesite')
+ shutil.copytree(src, target)
+ print "A new site with some sample data has been created at %s."\
+ % target
+ print "See README.txt in that folder for more information."
diff --git a/nikola/plugins/command_install_theme.plugin b/nikola/plugins/command_install_theme.plugin
new file mode 100644
index 0000000..f010074
--- /dev/null
+++ b/nikola/plugins/command_install_theme.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = install_theme
+Module = command_install_theme
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Install a theme into the current site.
+
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
new file mode 100644
index 0000000..293ce97
--- /dev/null
+++ b/nikola/plugins/command_install_theme.py
@@ -0,0 +1,62 @@
+from optparse import OptionParser
+import os
+import urllib2
+import json
+from io import StringIO
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+
+class CommandInstallTheme(Command):
+ """Start test server."""
+
+ name = "install_theme"
+
+ def run(self, *args):
+ """Install theme into current site."""
+
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ parser.add_option("-l", "--list", dest="list",
+ action="store_true",
+ help="Show list of available themes.")
+ parser.add_option("-n", "--name", dest="name",
+ help="Theme name", default=None)
+ parser.add_option("-u", "--url", dest="url",
+ help="URL for the theme repository"
+ "(default: http://nikola.ralsina.com.ar/themes/index.json)",
+ default='http://nikola.ralsina.com.ar/themes/index.json')
+ (options, args) = parser.parse_args(list(args))
+
+ listing = options.list
+ name = options.name
+ url = options.url
+
+ if name is None and not listing:
+ print "This command needs either the -n or the -l option."
+ return False
+ data = urllib2.urlopen(url).read()
+ data = json.loads(data)
+ if listing:
+ print "Themes:"
+ print "-------"
+ for theme in sorted(data.keys()):
+ print theme
+ return True
+ else:
+ if name in data:
+ if os.path.isfile("themes"):
+ raise IOError("'themes' isn't a directory!")
+ elif not os.path.isdir("themes"):
+ try:
+ os.makedirs("themes")
+ except:
+ raise OSError("mkdir 'theme' error!")
+ print 'Downloading: %s' % data[name]
+ zip_file = StringIO()
+ zip_file.write(urllib2.urlopen(data[name]).read())
+ print 'Extracting: %s into themes' % name
+ utils.extract_all(zip_file)
+ else:
+ print "Can't find theme %s" % name
+ return False
diff --git a/nikola/plugins/command_new_post.plugin b/nikola/plugins/command_new_post.plugin
new file mode 100644
index 0000000..6d70aff
--- /dev/null
+++ b/nikola/plugins/command_new_post.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = new_post
+Module = command_new_post
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create a new post.
+
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py
new file mode 100644
index 0000000..574df5f
--- /dev/null
+++ b/nikola/plugins/command_new_post.py
@@ -0,0 +1,100 @@
+import codecs
+import datetime
+from optparse import OptionParser
+import os
+import sys
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+
+class CommandNewPost(Command):
+ """Create a new post."""
+
+ name = "new_post"
+
+ def run(self, *args):
+ """Create a new post."""
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ parser.add_option('-p', '--page', dest='is_post',
+ action='store_false',
+ help='Create a page instead of a blog post.')
+ parser.add_option('-t', '--title', dest='title',
+ help='Title for the page/post.', default=None)
+ parser.add_option('--tags', dest='tags',
+ help='Comma-separated tags for the page/post.',
+ default='')
+ parser.add_option('-1', dest='onefile',
+ action='store_true',
+ help='Create post with embedded metadata (single file format).',
+ default=self.site.config.get('ONE_FILE_POSTS', True))
+ parser.add_option('-f', '--format',
+ dest='post_format',
+ default='rest',
+ help='Format for post (rest or markdown)')
+ (options, args) = parser.parse_args(list(args))
+
+ is_post = options.is_post
+ title = options.title
+ tags = options.tags
+ onefile = options.onefile
+ post_format = options.post_format
+
+ # Guess where we should put this
+ for path, _, _, use_in_rss in self.site.config['post_pages']:
+ if use_in_rss == is_post:
+ break
+ else:
+ path = self.site.config['post_pages'][0][0]
+
+ print "Creating New Post"
+ print "-----------------\n"
+ if title is None:
+ title = raw_input("Enter title: ").decode(sys.stdin.encoding)
+ else:
+ print "Title: ", title
+ slug = utils.slugify(title)
+ date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
+ data = [
+ title,
+ slug,
+ date,
+ tags
+ ]
+ output_path = os.path.dirname(path)
+ meta_path = os.path.join(output_path, slug + ".meta")
+ pattern = os.path.basename(path)
+ if pattern.startswith("*."):
+ suffix = pattern[1:]
+ else:
+ suffix = ".txt"
+ txt_path = os.path.join(output_path, slug + suffix)
+
+ if (not onefile and os.path.isfile(meta_path)) or \
+ os.path.isfile(txt_path):
+ print "The title already exists!"
+ exit()
+
+ if onefile:
+ if post_format not in ('rest', 'markdown'):
+ print "ERROR: Unknown post format %s" % post_format
+ return
+ with codecs.open(txt_path, "wb+", "utf8") as fd:
+ if post_format == 'markdown':
+ fd.write('<!-- \n')
+ fd.write('.. title: %s\n' % title)
+ fd.write('.. slug: %s\n' % slug)
+ fd.write('.. date: %s\n' % date)
+ fd.write('.. tags: %s\n' % tags)
+ fd.write('.. link: \n')
+ fd.write('.. description: \n')
+ if post_format == 'markdown':
+ fd.write('-->\n')
+ fd.write(u"Write your post here.")
+ else:
+ with codecs.open(meta_path, "wb+", "utf8") as fd:
+ fd.write(data)
+ with codecs.open(txt_path, "wb+", "utf8") as fd:
+ fd.write(u"Write your post here.")
+ print "Your post's metadata is at: ", meta_path
+ print "Your post's text is at: ", txt_path
diff --git a/nikola/plugins/command_serve.plugin b/nikola/plugins/command_serve.plugin
new file mode 100644
index 0000000..684935d
--- /dev/null
+++ b/nikola/plugins/command_serve.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = serve
+Module = command_serve
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Start test server.
+
diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py
new file mode 100644
index 0000000..626b117
--- /dev/null
+++ b/nikola/plugins/command_serve.py
@@ -0,0 +1,40 @@
+from optparse import OptionParser
+import os
+from BaseHTTPServer import HTTPServer
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+
+from nikola.plugin_categories import Command
+
+
+class CommandBuild(Command):
+ """Start test server."""
+
+ name = "serve"
+
+ def run(self, *args):
+ """Start test server."""
+
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ parser.add_option("-p", "--port", dest="port",
+ help="Port numer (default: 8000)", default=8000,
+ type="int")
+ parser.add_option("-a", "--address", dest="address",
+ help="Address to bind (default: 127.0.0.1)",
+ default='127.0.0.1')
+ (options, args) = parser.parse_args(list(args))
+
+ out_dir = self.site.config['OUTPUT_FOLDER']
+ if not os.path.isdir(out_dir):
+ print "Error: Missing '%s' folder?" % out_dir
+ else:
+ os.chdir(out_dir)
+ httpd = HTTPServer((options.address, options.port),
+ OurHTTPRequestHandler)
+ sa = httpd.socket.getsockname()
+ print "Serving HTTP on", sa[0], "port", sa[1], "..."
+ httpd.serve_forever()
+
+
+class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
+ extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
+ extensions_map[""] = "text/plain"
diff --git a/nikola/plugins/compile_html.plugin b/nikola/plugins/compile_html.plugin
new file mode 100644
index 0000000..f6cdfbc
--- /dev/null
+++ b/nikola/plugins/compile_html.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = html
+Module = compile_html
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Compile HTML into HTML (just copy)
+
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile_html.py
new file mode 100644
index 0000000..8241030
--- /dev/null
+++ b/nikola/plugins/compile_html.py
@@ -0,0 +1,20 @@
+"""Implementation of compile_html based on markdown."""
+
+import os
+import shutil
+
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileHtml(PageCompiler):
+ """Compile HTML into HTML."""
+
+ name = "html"
+
+ def compile_html(self, source, dest):
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except:
+ pass
+ shutil.copyfile(source, dest)
diff --git a/nikola/plugins/compile_markdown.plugin b/nikola/plugins/compile_markdown.plugin
new file mode 100644
index 0000000..f3e119b
--- /dev/null
+++ b/nikola/plugins/compile_markdown.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = markdown
+Module = compile_markdown
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Compile Markdown into HTML
+
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile_markdown/__init__.py
new file mode 100644
index 0000000..958cfa3
--- /dev/null
+++ b/nikola/plugins/compile_markdown/__init__.py
@@ -0,0 +1,33 @@
+"""Implementation of compile_html based on markdown."""
+
+import codecs
+import os
+import re
+
+from markdown import markdown
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileMarkdown(PageCompiler):
+ """Compile reSt into HTML."""
+
+ name = "markdown"
+
+ def compile_html(self, source, dest):
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except:
+ pass
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ output = markdown(data, ['fenced_code', 'codehilite'])
+ # remove the H1 because there is "title" h1.
+ output = re.sub(r'<h1>.*</h1>', '', output)
+ # python-markdown's highlighter uses the class 'codehilite' to wrap
+ # code, # instead of the standard 'code'. None of the standard
+ # pygments stylesheets use this class, so swap it to be 'code'
+ output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
+ r'\1code\2', output)
+ out_file.write(output)
diff --git a/nikola/plugins/compile_rest.plugin b/nikola/plugins/compile_rest.plugin
new file mode 100644
index 0000000..67eb562
--- /dev/null
+++ b/nikola/plugins/compile_rest.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest
+Module = compile_rest
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Compile reSt into HTML
+
diff --git a/nikola/rest.py b/nikola/plugins/compile_rest/__init__.py
index 071c6c8..0a25a06 100644
--- a/nikola/rest.py
+++ b/nikola/plugins/compile_rest/__init__.py
@@ -1,13 +1,6 @@
-"""Implementation of compile_html based on reStructuredText and docutils."""
-
-__all__ = ['compile_html']
-
import codecs
import os
-########################################
-# custom rst directives and renderer
-########################################
import docutils.core
import docutils.io
from docutils.parsers.rst import directives
@@ -24,23 +17,31 @@ pygments_code_block_directive
from youtube import youtube
directives.register_directive('youtube', youtube)
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileRest(PageCompiler):
+ """Compile reSt into HTML."""
+
+ name = "rest"
-def compile_html(source, dest):
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- error_level = 100
- with codecs.open(dest, "w+", "utf8") as out_file:
- with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
- output, error_level = rst2html(data,
- settings_overrides={'initial_header_level': 2})
- out_file.write(output)
- if error_level < 3:
- return True
- else:
- return False
+ def compile_html(self, source, dest):
+ """Compile reSt into HTML."""
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except:
+ pass
+ error_level = 100
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ output, error_level = rst2html(data,
+ settings_overrides={'initial_header_level': 2})
+ out_file.write(output)
+ if error_level < 3:
+ return True
+ else:
+ return False
def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
diff --git a/nikola/pygments_code_block_directive.py b/nikola/plugins/compile_rest/pygments_code_block_directive.py
index ac91f3c..ac91f3c 100644
--- a/nikola/pygments_code_block_directive.py
+++ b/nikola/plugins/compile_rest/pygments_code_block_directive.py
diff --git a/nikola/youtube.py b/nikola/plugins/compile_rest/youtube.py
index 584160b..584160b 100644
--- a/nikola/youtube.py
+++ b/nikola/plugins/compile_rest/youtube.py
diff --git a/nikola/plugins/task_archive.plugin b/nikola/plugins/task_archive.plugin
new file mode 100644
index 0000000..23f93ed
--- /dev/null
+++ b/nikola/plugins/task_archive.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_archive
+Module = task_archive
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Generates the blog's archive pages.
+
diff --git a/nikola/plugins/task_archive.py b/nikola/plugins/task_archive.py
new file mode 100644
index 0000000..4c97101
--- /dev/null
+++ b/nikola/plugins/task_archive.py
@@ -0,0 +1,77 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed
+
+
+class Archive(Task):
+ """Render the post archives."""
+
+ name = "render_archive"
+
+ def gen_tasks(self):
+ kw = {
+ "messages": self.site.MESSAGES,
+ "translations": self.site.config['TRANSLATIONS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ }
+ self.site.scan_posts()
+ # TODO add next/prev links for years
+ template_name = "list.tmpl"
+ # TODO: posts_per_year is global, kill it
+ for year, posts in self.site.posts_per_year.items():
+ for lang in kw["translations"]:
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", year, lang))
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
+ post_list.reverse()
+ context = {}
+ context["lang"] = lang
+ context["items"] = [("[%s] %s" %
+ (post.date, post.title(lang)), post.permalink(lang))
+ for post in post_list]
+ context["permalink"] = self.site.link("archive", year, lang)
+ context["title"] = kw["messages"][lang]["Posts for year %s"]\
+ % year
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ yield task
+
+ # And global "all your years" page
+ years = self.site.posts_per_year.keys()
+ years.sort(reverse=True)
+ template_name = "list.tmpl"
+ kw['years'] = years
+ for lang in kw["translations"]:
+ context = {}
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", None, lang))
+ context["title"] = kw["messages"][lang]["Archive"]
+ context["items"] = [(year, self.site.link("archive", year, lang))
+ for year in years]
+ context["permalink"] = self.site.link("archive", None, lang)
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ yield task
diff --git a/nikola/plugins/task_copy_assets.plugin b/nikola/plugins/task_copy_assets.plugin
new file mode 100644
index 0000000..b11133f
--- /dev/null
+++ b/nikola/plugins/task_copy_assets.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = copy_assets
+Module = task_copy_assets
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Copy theme assets into output.
+
diff --git a/nikola/plugins/task_copy_assets.py b/nikola/plugins/task_copy_assets.py
new file mode 100644
index 0000000..ac31fd7
--- /dev/null
+++ b/nikola/plugins/task_copy_assets.py
@@ -0,0 +1,35 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class CopyAssets(Task):
+ """Copy theme assets into output."""
+
+ name = "copy_assets"
+
+ def gen_tasks(self):
+ """Create tasks to copy the assets of the whole theme chain.
+
+ If a file is present on two themes, use the version
+ from the "youngest" theme.
+ """
+
+ kw = {
+ "themes": self.site.THEMES,
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ }
+
+ tasks = {}
+ for theme_name in kw['themes']:
+ src = os.path.join(utils.get_theme_path(theme_name), 'assets')
+ dst = os.path.join(kw['output_folder'], 'assets')
+ for task in utils.copy_tree(src, dst):
+ if task['name'] in tasks:
+ continue
+ tasks[task['name']] = task
+ task['uptodate'] = [utils.config_changed(kw)]
+ task['basename'] = self.name
+ yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task_copy_files.plugin b/nikola/plugins/task_copy_files.plugin
new file mode 100644
index 0000000..0bfc5be
--- /dev/null
+++ b/nikola/plugins/task_copy_files.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = copy_files
+Module = task_copy_files
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Copy static files into the output.
+
diff --git a/nikola/plugins/task_copy_files.py b/nikola/plugins/task_copy_files.py
new file mode 100644
index 0000000..a053905
--- /dev/null
+++ b/nikola/plugins/task_copy_files.py
@@ -0,0 +1,35 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class CopyFiles(Task):
+ """Copy static files into the output folder."""
+
+ name = "copy_files"
+
+ def gen_tasks(self):
+ """Copy static files into the output folder."""
+
+ kw = {
+ 'files_folders': self.site.config['FILES_FOLDERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
+ }
+
+ flag = False
+ for src in kw['files_folders']:
+ dst = kw['output_folder']
+ filters = kw['filters']
+ real_dst = os.path.join(dst, kw['files_folders'][src])
+ for task in utils.copy_tree(src, real_dst, link_cutoff=dst):
+ flag = True
+ task['basename'] = self.name
+ task['uptodate'] = [utils.config_changed(kw)]
+ yield utils.apply_filters(task, filters)
+ if not flag:
+ yield {
+ 'basename': self.name,
+ 'actions': (),
+ }
diff --git a/nikola/plugins/task_create_bundles.plugin b/nikola/plugins/task_create_bundles.plugin
new file mode 100644
index 0000000..5d4f6d3
--- /dev/null
+++ b/nikola/plugins/task_create_bundles.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = create_bundles
+Module = task_create_bundles
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Theme bundles using WebAssets
+
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task_create_bundles.py
new file mode 100644
index 0000000..ebca0b7
--- /dev/null
+++ b/nikola/plugins/task_create_bundles.py
@@ -0,0 +1,85 @@
+import os
+
+try:
+ import webassets
+except ImportError:
+ webassets = None # NOQA
+
+from nikola.plugin_categories import LateTask
+from nikola import utils
+
+
+class BuildBundles(LateTask):
+ """Bundle assets using WebAssets."""
+
+ name = "build_bundles"
+
+ def set_site(self, site):
+ super(BuildBundles, self).set_site(site)
+ if webassets is None:
+ self.site.config['USE_BUNDLES'] = False
+
+ def gen_tasks(self):
+ """Bundle assets using WebAssets."""
+
+ kw = {
+ 'filters': self.site.config['FILTERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'theme_bundles': get_theme_bundles(self.site.THEMES),
+ }
+
+ def build_bundle(output, inputs):
+ out_dir = os.path.join(kw['output_folder'], os.path.dirname(output))
+ inputs = [i for i in inputs if os.path.isfile(
+ os.path.join(out_dir, i))]
+ cache_dir = os.path.join('cache', 'webassets')
+ if not os.path.isdir(cache_dir):
+ os.makedirs(cache_dir)
+ env = webassets.Environment(out_dir, os.path.dirname(output),
+ cache=cache_dir)
+ bundle = webassets.Bundle(*inputs,
+ output=os.path.basename(output))
+ env.register(output, bundle)
+ # This generates the file
+ env[output].urls()
+
+ flag = False
+ if webassets is not None and self.site.config['USE_BUNDLES'] is not False:
+ for name, files in kw['theme_bundles'].items():
+ output_path = os.path.join(kw['output_folder'], name)
+ dname = os.path.dirname(name)
+ file_dep = [os.path.join('output', dname, fname)
+ for fname in files]
+ task = {
+ 'file_dep': file_dep,
+ 'basename': self.name,
+ 'name': output_path,
+ 'actions': [(build_bundle, (name, files))],
+ 'targets': [output_path],
+ 'uptodate': [utils.config_changed(kw)]
+ }
+ flag = True
+ yield utils.apply_filters(task, kw['filters'])
+ if flag is False: # No page rendered, yield a dummy task
+ yield {
+ 'basename': self.name,
+ 'uptodate': [True],
+ 'name': 'None',
+ 'actions': [],
+ }
+
+
+def get_theme_bundles(themes):
+ """Given a theme chain, return the bundle definitions."""
+ bundles = {}
+ for theme_name in themes:
+ bundles_path = os.path.join(
+ utils.get_theme_path(theme_name), 'bundles')
+ if os.path.isfile(bundles_path):
+ with open(bundles_path) as fd:
+ for line in fd:
+ name, files = line.split('=')
+ files = [f.strip() for f in files.split(',')]
+ bundles[name.strip()] = files
+ break
+ return bundles
diff --git a/nikola/plugins/task_indexes.plugin b/nikola/plugins/task_indexes.plugin
new file mode 100644
index 0000000..1536006
--- /dev/null
+++ b/nikola/plugins/task_indexes.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_index
+Module = task_indexes
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Generates the blog's index pages.
+
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task_indexes.py
new file mode 100644
index 0000000..2311ef3
--- /dev/null
+++ b/nikola/plugins/task_indexes.py
@@ -0,0 +1,81 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed
+
+
+class Indexes(Task):
+ """Render the blog indexes."""
+
+ name = "render_indexes"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "messages": self.site.MESSAGES,
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ }
+
+ template_name = "index.tmpl"
+ # TODO: timeline is global, get rid of it
+ posts = [x for x in self.site.timeline if x.use_in_feeds]
+ # Split in smaller lists
+ lists = []
+ while posts:
+ lists.append(posts[:kw["index_display_post_count"]])
+ posts = posts[kw["index_display_post_count"]:]
+ num_pages = len(lists)
+ if not lists:
+ yield {
+ 'basename': 'render_indexes',
+ 'actions': [],
+ }
+ for lang in kw["translations"]:
+ for i, post_list in enumerate(lists):
+ context = {}
+ if self.site.config.get("INDEXES_TITLE", ""):
+ indexes_title = self.site.config['INDEXES_TITLE']
+ else:
+ indexes_title = self.site.config["BLOG_TITLE"]
+ if not i:
+ output_name = "index.html"
+ context["title"] = indexes_title
+ else:
+ output_name = "index-%s.html" % i
+ if self.site.config.get("INDEXES_PAGES", ""):
+ indexes_pages = self.site.config["INDEXES_PAGES"] % i
+ else:
+ indexes_pages = " (" + \
+ kw["messages"][lang]["old posts page %d"] % i + ")"
+ context["title"] = indexes_title + indexes_pages
+ context["prevlink"] = None
+ context["nextlink"] = None
+ context['index_teasers'] = kw['index_teasers']
+ if i > 1:
+ context["prevlink"] = "index-%s.html" % (i - 1)
+ if i == 1:
+ context["prevlink"] = "index.html"
+ if i < num_pages - 1:
+ context["nextlink"] = "index-%s.html" % (i + 1)
+ context["permalink"] = self.site.link("index", i, lang)
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("index", i, lang))
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = 'render_indexes'
+ yield task
diff --git a/nikola/plugins/task_redirect.plugin b/nikola/plugins/task_redirect.plugin
new file mode 100644
index 0000000..285720b
--- /dev/null
+++ b/nikola/plugins/task_redirect.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = redirect
+Module = task_redirect
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create redirect pages.
+
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task_redirect.py
new file mode 100644
index 0000000..7c2ccb1
--- /dev/null
+++ b/nikola/plugins/task_redirect.py
@@ -0,0 +1,48 @@
+import codecs
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Redirect(Task):
+ """Copy theme assets into output."""
+
+ name = "redirect"
+
+ def gen_tasks(self):
+ """Generate redirections tasks."""
+
+ kw = {
+ 'redirections': self.site.config['REDIRECTIONS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ }
+
+ if not kw['redirections']:
+ # If there are no redirections, still needs to create a
+ # dummy action so dependencies don't fail
+ yield {
+ 'basename': self.name,
+ 'name': 'None',
+ 'uptodate': [True],
+ 'actions': [],
+ }
+
+ else:
+ for src, dst in kw["redirections"]:
+ src_path = os.path.join(kw["output_folder"], src)
+ yield {
+ 'basename': self.name,
+ 'name': src_path,
+ 'targets': [src_path],
+ 'actions': [(create_redirect, (src_path, dst))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+
+
+def create_redirect(src, dst):
+ with codecs.open(src, "wb+", "utf8") as fd:
+ fd.write(('<head>' +
+ '<meta HTTP-EQUIV="REFRESH" content="0; url=%s">' +
+ '</head>') % dst)
diff --git a/nikola/plugins/task_render_galleries.plugin b/nikola/plugins/task_render_galleries.plugin
new file mode 100644
index 0000000..e0a86c0
--- /dev/null
+++ b/nikola/plugins/task_render_galleries.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_galleries
+Module = task_render_galleries
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create image galleries automatically.
+
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
new file mode 100644
index 0000000..27e13ea
--- /dev/null
+++ b/nikola/plugins/task_render_galleries.py
@@ -0,0 +1,305 @@
+import codecs
+import datetime
+import glob
+import os
+import uuid
+
+Image = None
+try:
+ import Image as _Image
+ import ExifTags
+ Image = _Image
+except ImportError:
+ try:
+ from PIL import Image, ExifTags # NOQA
+ except ImportError:
+ pass
+
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Galleries(Task):
+ """Copy theme assets into output."""
+
+ name = "render_galleries"
+ dates = {}
+
+ def gen_tasks(self):
+ """Render image galleries."""
+
+ kw = {
+ 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'default_lang': self.site.config['DEFAULT_LANG'],
+ 'blog_description': self.site.config['BLOG_DESCRIPTION'],
+ 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
+ }
+
+ # FIXME: lots of work is done even when images don't change,
+ # which should be moved into the task.
+
+ template_name = "gallery.tmpl"
+
+ gallery_list = []
+ for root, dirs, files in os.walk('galleries'):
+ gallery_list.append(root)
+ if not gallery_list:
+ yield {
+ 'basename': 'render_galleries',
+ 'actions': [],
+ }
+ return
+
+ # gallery_path is "gallery/name"
+ for gallery_path in gallery_list:
+ # gallery_name is "name"
+ splitted = gallery_path.split(os.sep)[1:]
+ if not splitted:
+ gallery_name = ''
+ else:
+ gallery_name = os.path.join(*splitted)
+ # output_gallery is "output/GALLERY_PATH/name"
+ output_gallery = os.path.dirname(os.path.join(kw["output_folder"],
+ self.site.path("gallery", gallery_name, None)))
+ if not os.path.isdir(output_gallery):
+ yield {
+ 'basename': 'render_galleries',
+ 'name': output_gallery,
+ 'actions': [(os.makedirs, (output_gallery,))],
+ 'targets': [output_gallery],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+ # image_list contains "gallery/name/image_name.jpg"
+ image_list = glob.glob(gallery_path + "/*jpg") +\
+ glob.glob(gallery_path + "/*JPG") +\
+ glob.glob(gallery_path + "/*PNG") +\
+ glob.glob(gallery_path + "/*png")
+
+ # Filter ignore images
+ try:
+ def add_gallery_path(index):
+ return "{0}/{1}".format(gallery_path, index)
+
+ exclude_path = os.path.join(gallery_path, "exclude.meta")
+ try:
+ f = open(exclude_path, 'r')
+ excluded_image_name_list = f.read().split()
+ except IOError:
+ excluded_image_name_list = []
+
+ excluded_image_list = map(add_gallery_path,
+ excluded_image_name_list)
+ image_set = set(image_list) - set(excluded_image_list)
+ image_list = list(image_set)
+ except IOError:
+ pass
+
+ # List of sub-galleries
+ folder_list = [x.split(os.sep)[-2] for x in
+ glob.glob(os.path.join(gallery_path, '*') + os.sep)]
+
+ crumbs = gallery_path.split(os.sep)[:-1]
+ crumbs.append(os.path.basename(gallery_name))
+ # TODO: write this in human
+ paths = ['/'.join(['..'] * (len(crumbs) - 1 - i)) for i in
+ range(len(crumbs[:-1]))] + ['#']
+ crumbs = zip(paths, crumbs)
+
+ image_list = [x for x in image_list if "thumbnail" not in x]
+ # Sort by date
+ image_list.sort(cmp=lambda a, b: cmp(
+ self.image_date(a), self.image_date(b)))
+ image_name_list = [os.path.basename(x) for x in image_list]
+
+ thumbs = []
+ # Do thumbnails and copy originals
+ for img, img_name in zip(image_list, image_name_list):
+ # img is "galleries/name/image_name.jpg"
+ # img_name is "image_name.jpg"
+ # fname, ext are "image_name", ".jpg"
+ fname, ext = os.path.splitext(img_name)
+ # thumb_path is
+ # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
+ thumb_path = os.path.join(output_gallery,
+ fname + ".thumbnail" + ext)
+ # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
+ orig_dest_path = os.path.join(output_gallery, img_name)
+ thumbs.append(os.path.basename(thumb_path))
+ yield {
+ 'basename': 'render_galleries',
+ 'name': thumb_path,
+ 'file_dep': [img],
+ 'targets': [thumb_path],
+ 'actions': [
+ (self.resize_image,
+ (img, thumb_path, kw['thumbnail_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+ yield {
+ 'basename': 'render_galleries',
+ 'name': orig_dest_path,
+ 'file_dep': [img],
+ 'targets': [orig_dest_path],
+ 'actions': [
+ (self.resize_image,
+ (img, orig_dest_path, kw['max_image_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+
+ # Remove excluded images
+ if excluded_image_name_list:
+ for img, img_name in zip(excluded_image_list,
+ excluded_image_name_list):
+ # img_name is "image_name.jpg"
+ # fname, ext are "image_name", ".jpg"
+ fname, ext = os.path.splitext(img_name)
+ excluded_thumb_dest_path = os.path.join(output_gallery,
+ fname + ".thumbnail" + ext)
+ excluded_dest_path = os.path.join(output_gallery, img_name)
+ yield {
+ 'basename': 'render_galleries',
+ 'name': excluded_thumb_dest_path,
+ 'file_dep': [exclude_path],
+ #'targets': [excluded_thumb_dest_path],
+ 'actions': [
+ (utils.remove_file, (excluded_thumb_dest_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+ yield {
+ 'basename': 'render_galleries',
+ 'name': excluded_dest_path,
+ 'file_dep': [exclude_path],
+ #'targets': [excluded_dest_path],
+ 'actions': [
+ (utils.remove_file, (excluded_dest_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+
+ output_name = os.path.join(output_gallery, "index.html")
+ context = {}
+ context["lang"] = kw["default_lang"]
+ context["title"] = os.path.basename(gallery_path)
+ context["description"] = kw["blog_description"]
+ if kw['use_filename_as_title']:
+ img_titles = ['title="%s"' % utils.unslugify(fn[:-4])
+ for fn in image_name_list]
+ else:
+ img_titles = [''] * len(image_name_list)
+ context["images"] = zip(image_name_list, thumbs, img_titles)
+ context["folders"] = folder_list
+ context["crumbs"] = crumbs
+ context["permalink"] = self.site.link(
+ "gallery", gallery_name, None)
+
+ # Use galleries/name/index.txt to generate a blurb for
+ # the gallery, if it exists
+ index_path = os.path.join(gallery_path, "index.txt")
+ cache_dir = os.path.join('cache', 'galleries')
+ if not os.path.isdir(cache_dir):
+ os.makedirs(cache_dir)
+ index_dst_path = os.path.join(cache_dir, unicode(uuid.uuid1())+'.html')
+ if os.path.exists(index_path):
+ compile_html = self.site.get_compiler(index_path)
+ yield {
+ 'basename': 'render_galleries',
+ 'name': index_dst_path.encode('utf-8'),
+ 'file_dep': [index_path],
+ 'targets': [index_dst_path],
+ 'actions': [(compile_html,
+ [index_path, index_dst_path])],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+
+ file_dep = self.site.template_system.template_deps(
+ template_name) + image_list
+
+ def render_gallery(output_name, context, index_dst_path):
+ if os.path.exists(index_dst_path):
+ with codecs.open(index_dst_path, "rb", "utf8") as fd:
+ context['text'] = fd.read()
+ file_dep.append(index_dst_path)
+ else:
+ context['text'] = ''
+ self.site.render_template(template_name, output_name, context)
+
+ yield {
+ 'basename': 'render_galleries',
+ 'name': output_name,
+ 'file_dep': file_dep,
+ 'targets': [output_name],
+ 'actions': [(render_gallery,
+ (output_name, context, index_dst_path))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: kw,
+ 2: self.site.config['GLOBAL_CONTEXT']})],
+ }
+
+ def resize_image(self, src, dst, max_size):
+ """Make a copy of the image in the requested size."""
+ if not Image:
+ utils.copy_file(src, dst)
+ return
+ im = Image.open(src)
+ w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in exif.items():
+ decoded = ExifTags.TAGS.get(tag, tag)
+
+ if decoded == 'Orientation':
+ if value == 3:
+ im = im.rotate(180)
+ elif value == 6:
+ im = im.rotate(270)
+ elif value == 8:
+ im = im.rotate(90)
+
+ break
+
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(dst)
+
+ else:
+ utils.copy_file(src, dst)
+
+ def image_date(self, src):
+ """Try to figure out the date of the image."""
+ if src not in self.dates:
+ im = Image.open(src)
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in exif.items():
+ decoded = ExifTags.TAGS.get(tag, tag)
+ if decoded == 'DateTimeOriginal':
+ try:
+ self.dates[src] = datetime.datetime.strptime(
+ value, r'%Y:%m:%d %H:%M:%S')
+ break
+ except ValueError: # Invalid EXIF date.
+ pass
+ if src not in self.dates:
+ self.dates[src] = datetime.datetime.fromtimestamp(
+ os.stat(src).st_mtime)
+ return self.dates[src]
diff --git a/nikola/plugins/task_render_listings.plugin b/nikola/plugins/task_render_listings.plugin
new file mode 100644
index 0000000..1f897b9
--- /dev/null
+++ b/nikola/plugins/task_render_listings.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_listings
+Module = task_render_listings
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Render code listings into output
+
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task_render_listings.py
new file mode 100644
index 0000000..7ec6e42
--- /dev/null
+++ b/nikola/plugins/task_render_listings.py
@@ -0,0 +1,81 @@
+import os
+
+from pygments import highlight
+from pygments.lexers import get_lexer_for_filename, TextLexer
+from pygments.formatters import HtmlFormatter
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Listings(Task):
+ """Render pretty listings."""
+
+ name = "render_listings"
+
+ def gen_tasks(self):
+ """Render pretty code listings."""
+ kw = {
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ "listings_folder": self.site.config["LISTINGS_FOLDER"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ }
+
+ # Things to ignore in listings
+ ignored_extensions = (".pyc",)
+
+ def render_listing(in_name, out_name):
+ with open(in_name, 'r') as fd:
+ try:
+ lexer = get_lexer_for_filename(in_name)
+ except:
+ lexer = TextLexer()
+ code = highlight(fd.read(), lexer,
+ HtmlFormatter(cssclass='code',
+ linenos="table",
+ nowrap=False,
+ lineanchors=utils.slugify(f),
+ anchorlinenos=True))
+ title = os.path.basename(in_name)
+ crumbs = out_name.split(os.sep)[1:-1] + [title]
+ # TODO: write this in human
+ paths = ['/'.join(['..'] * (len(crumbs) - 2 - i)) for i in
+ range(len(crumbs[:-2]))] + ['.', '#']
+ context = {
+ 'code': code,
+ 'title': title,
+ 'crumbs': zip(paths, crumbs),
+ 'lang': kw['default_lang'],
+ 'description': title,
+ }
+ self.site.render_template('listing.tmpl', out_name, context)
+ flag = True
+ template_deps = self.site.template_system.template_deps('listing.tmpl')
+ for root, dirs, files in os.walk(kw['listings_folder']):
+ # Render all files
+ for f in files:
+ ext = os.path.splitext(f)[-1]
+ if ext in ignored_extensions:
+ continue
+ flag = False
+ in_name = os.path.join(root, f)
+ out_name = os.path.join(
+ kw['output_folder'],
+ root,
+ f) + '.html'
+ yield {
+ 'basename': self.name,
+ 'name': out_name.encode('utf8'),
+ 'file_dep': template_deps + [in_name],
+ 'targets': [out_name],
+ 'actions': [(render_listing, [in_name, out_name])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(
+ self.site.config['GLOBAL_CONTEXT'])]
+ }
+ if flag:
+ yield {
+ 'basename': self.name,
+ 'actions': [],
+ }
diff --git a/nikola/plugins/task_render_pages.plugin b/nikola/plugins/task_render_pages.plugin
new file mode 100644
index 0000000..e2a358c
--- /dev/null
+++ b/nikola/plugins/task_render_pages.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_pages
+Module = task_render_pages
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create pages in the output.
+
diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task_render_pages.py
new file mode 100644
index 0000000..954dc47
--- /dev/null
+++ b/nikola/plugins/task_render_pages.py
@@ -0,0 +1,35 @@
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed
+
+
+class RenderPages(Task):
+ """Render pages into output."""
+
+ name = "render_pages"
+
+ def gen_tasks(self):
+ """Build final pages from metadata and HTML fragments."""
+ kw = {
+ "post_pages": self.site.config["post_pages"],
+ "translations": self.site.config["TRANSLATIONS"],
+ "filters": self.site.config["FILTERS"],
+ }
+ self.site.scan_posts()
+ flag = False
+ for lang in kw["translations"]:
+ for wildcard, destination, template_name, _ in kw["post_pages"]:
+ for task in self.site.generic_page_renderer(lang,
+ wildcard, template_name, destination, kw["filters"]):
+ task['uptodate'] = [config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ flag = True
+ yield task
+ if flag is False: # No page rendered, yield a dummy task
+ yield {
+ 'basename': self.name,
+ 'name': 'None',
+ 'uptodate': [True],
+ 'actions': [],
+ }
diff --git a/nikola/plugins/task_render_posts.plugin b/nikola/plugins/task_render_posts.plugin
new file mode 100644
index 0000000..0d19ea9
--- /dev/null
+++ b/nikola/plugins/task_render_posts.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_posts
+Module = task_render_posts
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create HTML fragments out of posts.
+
diff --git a/nikola/plugins/task_render_posts.py b/nikola/plugins/task_render_posts.py
new file mode 100644
index 0000000..44888f2
--- /dev/null
+++ b/nikola/plugins/task_render_posts.py
@@ -0,0 +1,52 @@
+from copy import copy
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class RenderPosts(Task):
+ """Build HTML fragments from metadata and text."""
+
+ name = "render_posts"
+
+ def gen_tasks(self):
+ """Build HTML fragments from metadata and text."""
+ self.site.scan_posts()
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "timeline": self.site.timeline,
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ }
+
+ flag = False
+ for lang in kw["translations"]:
+ # TODO: timeline is global, get rid of it
+ deps_dict = copy(kw)
+ deps_dict.pop('timeline')
+ for post in kw['timeline']:
+ source = post.source_path
+ dest = post.base_path
+ if lang != kw["default_lang"]:
+ dest += '.' + lang
+ source_lang = source + '.' + lang
+ if os.path.exists(source_lang):
+ source = source_lang
+ flag = True
+ yield {
+ 'basename': self.name,
+ 'name': dest.encode('utf-8'),
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [dest],
+ 'actions': [(self.site.get_compiler(post.source_path),
+ [source, dest])],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(deps_dict)],
+ }
+ if flag is False: # Return a dummy task
+ yield {
+ 'basename': self.name,
+ 'name': 'None',
+ 'uptodate': [True],
+ 'actions': [],
+ }
diff --git a/nikola/plugins/task_render_rss.plugin b/nikola/plugins/task_render_rss.plugin
new file mode 100644
index 0000000..20caf15
--- /dev/null
+++ b/nikola/plugins/task_render_rss.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_rss
+Module = task_render_rss
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Generate RSS feeds.
+
diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task_render_rss.py
new file mode 100644
index 0000000..bee1192
--- /dev/null
+++ b/nikola/plugins/task_render_rss.py
@@ -0,0 +1,41 @@
+import os
+
+from nikola import utils
+from nikola.plugin_categories import Task
+
+
+class RenderRSS(Task):
+ """Generate RSS feeds."""
+
+ name = "render_rss"
+
+ def gen_tasks(self):
+ """Generate RSS feeds."""
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "filters": self.site.config["FILTERS"],
+ "blog_title": self.site.config["BLOG_TITLE"],
+ "blog_url": self.site.config["BLOG_URL"],
+ "blog_description": self.site.config["BLOG_DESCRIPTION"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ }
+ self.site.scan_posts()
+ # TODO: timeline is global, kill it
+ for lang in kw["translations"]:
+ output_name = os.path.join(kw['output_folder'],
+ self.site.path("rss", None, lang))
+ deps = []
+ posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
+ for post in posts:
+ deps += post.deps(lang)
+ yield {
+ 'basename': 'render_rss',
+ 'name': output_name,
+ 'file_dep': deps,
+ 'targets': [output_name],
+ 'actions': [(utils.generic_rss_renderer,
+ (lang, kw["blog_title"], kw["blog_url"],
+ kw["blog_description"], posts, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
diff --git a/nikola/plugins/task_render_sources.plugin b/nikola/plugins/task_render_sources.plugin
new file mode 100644
index 0000000..5b59598
--- /dev/null
+++ b/nikola/plugins/task_render_sources.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_sources
+Module = task_render_sources
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Copy page sources into the output.
+
diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task_render_sources.py
new file mode 100644
index 0000000..ae5ce23
--- /dev/null
+++ b/nikola/plugins/task_render_sources.py
@@ -0,0 +1,54 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Sources(Task):
+ """Copy page sources into the output."""
+
+ name = "render_sources"
+
+ def gen_tasks(self):
+ """Publish the page sources into the output.
+
+ Required keyword arguments:
+
+ translations
+ default_lang
+ post_pages
+ output_folder
+ """
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ }
+
+ self.site.scan_posts()
+ flag = False
+ for lang in kw["translations"]:
+ for post in self.site.timeline:
+ output_name = os.path.join(kw['output_folder'],
+ post.destination_path(lang, post.source_ext()))
+ source = post.source_path
+ if lang != kw["default_lang"]:
+ source_lang = source + '.' + lang
+ if os.path.exists(source_lang):
+ source = source_lang
+ yield {
+ 'basename': 'render_sources',
+ 'name': output_name.encode('utf8'),
+ 'file_dep': [source],
+ 'targets': [output_name],
+ 'actions': [(utils.copy_file, (source, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+ if flag is False: # No page rendered, yield a dummy task
+ yield {
+ 'basename': 'render_sources',
+ 'name': 'None',
+ 'uptodate': [True],
+ 'actions': [],
+ }
diff --git a/nikola/plugins/task_render_tags.plugin b/nikola/plugins/task_render_tags.plugin
new file mode 100644
index 0000000..b826e87
--- /dev/null
+++ b/nikola/plugins/task_render_tags.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_tags
+Module = task_render_tags
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Render the tag pages and feeds.
+
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task_render_tags.py
new file mode 100644
index 0000000..61629ec
--- /dev/null
+++ b/nikola/plugins/task_render_tags.py
@@ -0,0 +1,180 @@
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class RenderTags(Task):
+ """Render the tag pages and feeds."""
+
+ name = "render_tags"
+
+ def gen_tasks(self):
+ """Render the tag pages and feeds."""
+
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "blog_title": self.site.config["BLOG_TITLE"],
+ "blog_url": self.site.config["BLOG_URL"],
+ "blog_description": self.site.config["BLOG_DESCRIPTION"],
+ "messages": self.site.MESSAGES,
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ }
+
+ self.site.scan_posts()
+
+ if not self.site.posts_per_tag:
+ yield {
+ 'basename': self.name,
+ 'actions': [],
+ }
+ return
+
+ def page_name(tagname, i, lang):
+ """Given tag, n, returns a page name."""
+ name = self.site.path("tag", tag, lang)
+ if i:
+ name = name.replace('.html', '-%s.html' % i)
+ return name
+
+ for tag, posts in self.site.posts_per_tag.items():
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
+ post_list.reverse()
+ for lang in kw["translations"]:
+ #Render RSS
+ output_name = os.path.join(kw['output_folder'],
+ self.site.path("tag_rss", tag, lang))
+ deps = []
+ post_list = [self.site.global_data[post] for post in posts
+ if self.site.global_data[post].use_in_feeds]
+ post_list.sort(cmp=lambda a, b: cmp(a.date, b.date))
+ post_list.reverse()
+ for post in post_list:
+ deps += post.deps(lang)
+ yield {
+ 'name': output_name.encode('utf8'),
+ 'file_dep': deps,
+ 'targets': [output_name],
+ 'actions': [(utils.generic_rss_renderer,
+ (lang, "%s (%s)" % (kw["blog_title"], tag),
+ kw["blog_url"], kw["blog_description"],
+ post_list, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ 'basename': self.name
+ }
+
+ # Render HTML
+ if kw['tag_pages_are_indexes']:
+ # We render a sort of index page collection using only
+ # this tag's posts.
+
+ # FIXME: deduplicate this with render_indexes
+ template_name = "index.tmpl"
+ # Split in smaller lists
+ lists = []
+ while post_list:
+ lists.append(post_list[
+ :kw["index_display_post_count"]])
+ post_list = post_list[
+ kw["index_display_post_count"]:]
+ num_pages = len(lists)
+ for i, post_list in enumerate(lists):
+ context = {}
+ # On a tag page, the feeds include the tag's feeds
+ rss_link = \
+ """<link rel="alternate" type="application/rss+xml" """\
+ """type="application/rss+xml" title="RSS for tag """\
+ """%s (%s)" href="%s">""" % \
+ (tag, lang, self.site.link("tag_rss", tag, lang))
+ context['rss_link'] = rss_link
+ output_name = os.path.join(kw['output_folder'],
+ page_name(tag, i, lang))
+ context["title"] = kw["messages"][lang][
+ u"Posts about %s"] % tag
+ context["prevlink"] = None
+ context["nextlink"] = None
+ context['index_teasers'] = kw['index_teasers']
+ if i > 1:
+ context["prevlink"] = os.path.basename(
+ page_name(tag, i - 1, lang))
+ if i == 1:
+ context["prevlink"] = os.path.basename(
+ page_name(tag, 0, lang))
+ if i < num_pages - 1:
+ context["nextlink"] = os.path.basename(
+ page_name(tag, i + 1, lang))
+ context["permalink"] = self.site.link("tag", tag, lang)
+ context["tag"] = tag
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [utils.config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ yield task
+ else:
+ # We render a single flat link list with this tag's posts
+ template_name = "tag.tmpl"
+ output_name = os.path.join(kw['output_folder'],
+ self.site.path("tag", tag, lang))
+ context = {}
+ context["lang"] = lang
+ context["title"] = kw["messages"][lang][
+ u"Posts about %s"] % tag
+ context["items"] = [("[%s] %s" % (post.date,
+ post.title(lang)),
+ post.permalink(lang)) for post in post_list]
+ context["permalink"] = self.site.link("tag", tag, lang)
+ context["tag"] = tag
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [utils.config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ yield task
+
+ # And global "all your tags" page
+ tags = self.site.posts_per_tag.keys()
+ tags.sort()
+ template_name = "tags.tmpl"
+ kw['tags'] = tags
+ for lang in kw["translations"]:
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path('tag_index', None, lang))
+ context = {}
+ context["title"] = kw["messages"][lang][u"Tags"]
+ context["items"] = [(tag, self.site.link("tag", tag, lang))
+ for tag in tags]
+ context["permalink"] = self.site.link("tag_index", None, lang)
+ for task in self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ ):
+ task['uptodate'] = [utils.config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ yield task
diff --git a/nikola/plugins/task_sitemap.plugin b/nikola/plugins/task_sitemap.plugin
new file mode 100644
index 0000000..f6b01d7
--- /dev/null
+++ b/nikola/plugins/task_sitemap.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = sitemap
+Module = task_sitemap
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Generate google sitemap.
+
diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
new file mode 100644
index 0000000..87b72bf
--- /dev/null
+++ b/nikola/plugins/task_sitemap/__init__.py
@@ -0,0 +1,62 @@
+import os
+import tempfile
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed
+
+import sitemap_gen as smap
+
+
+class Sitemap(LateTask):
+ """Copy theme assets into output."""
+
+ name = "sitemap"
+
+ def gen_tasks(self):
+ """Generate Google sitemap."""
+ kw = {
+ "blog_url": self.site.config["BLOG_URL"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ }
+ output_path = os.path.abspath(kw['output_folder'])
+ sitemap_path = os.path.join(output_path, "sitemap.xml.gz")
+
+ def sitemap():
+ # Generate config
+ config_data = """<?xml version="1.0" encoding="UTF-8"?>
+ <site
+ base_url="%s"
+ store_into="%s"
+ verbose="1" >
+ <directory path="%s" url="%s" />
+ <filter action="drop" type="wildcard" pattern="*~" />
+ <filter action="drop" type="regexp" pattern="/\.[^/]*" />
+ </site>""" % (
+ kw["blog_url"],
+ sitemap_path,
+ output_path,
+ kw["blog_url"],
+ )
+ config_file = tempfile.NamedTemporaryFile(delete=False)
+ config_file.write(config_data)
+ config_file.close()
+
+ # Generate sitemap
+ sitemap = smap.CreateSitemapFromFile(config_file.name, True)
+ if not sitemap:
+ smap.output.Log('Configuration file errors -- exiting.', 0)
+ else:
+ sitemap.Generate()
+ smap.output.Log('Number of errors: %d' %
+ smap.output.num_errors, 1)
+ smap.output.Log('Number of warnings: %d' %
+ smap.output.num_warns, 1)
+ os.unlink(config_file.name)
+
+ yield {
+ "basename": "sitemap",
+ "targets": [sitemap_path],
+ "actions": [(sitemap,)],
+ "uptodate": [config_changed(kw)],
+ "clean": True,
+ }
diff --git a/nikola/sitemap_gen.py b/nikola/plugins/task_sitemap/sitemap_gen.py
index e5d28b4..43e7c32 100755
--- a/nikola/sitemap_gen.py
+++ b/nikola/plugins/task_sitemap/sitemap_gen.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+# flake8: noqa
#
# Copyright (c) 2004, 2005 Google Inc.
# All rights reserved.
diff --git a/nikola/plugins/template_jinja.plugin b/nikola/plugins/template_jinja.plugin
new file mode 100644
index 0000000..01e6d8c
--- /dev/null
+++ b/nikola/plugins/template_jinja.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = jinja
+Module = template_jinja
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Support for Jinja2 templates.
diff --git a/nikola/plugins/template_jinja.py b/nikola/plugins/template_jinja.py
new file mode 100644
index 0000000..0893cf7
--- /dev/null
+++ b/nikola/plugins/template_jinja.py
@@ -0,0 +1,38 @@
+"""Jinja template handlers"""
+
+import os
+import jinja2
+
+from nikola.plugin_categories import TemplateSystem
+
+
+class JinjaTemplates(TemplateSystem):
+ """Wrapper for Jinja2 templates."""
+
+ name = "jinja"
+ lookup = None
+
+ def set_directories(self, directories):
+ """Createa template lookup."""
+ self.lookup = jinja2.Environment(loader=jinja2.FileSystemLoader(
+ directories,
+ encoding='utf-8',
+ ))
+
+ def render_template(self, template_name, output_name, context):
+ """Render the template into output_name using context."""
+
+ template = self.lookup.get_template(template_name)
+ output = template.render(**context)
+ if output_name is not None:
+ try:
+ os.makedirs(os.path.dirname(output_name))
+ except:
+ pass
+ with open(output_name, 'w+') as output:
+ output.write(output.encode('utf8'))
+ return output
+
+ def template_deps(self, template_name):
+ # FIXME: unimplemented
+ return []
diff --git a/nikola/plugins/template_mako.plugin b/nikola/plugins/template_mako.plugin
new file mode 100644
index 0000000..3fdc354
--- /dev/null
+++ b/nikola/plugins/template_mako.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = mako
+Module = template_mako
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Support for Mako templates.
diff --git a/nikola/plugins/template_mako.py b/nikola/plugins/template_mako.py
new file mode 100644
index 0000000..7ab5c43
--- /dev/null
+++ b/nikola/plugins/template_mako.py
@@ -0,0 +1,68 @@
+"""Mako template handlers"""
+
+import os
+import shutil
+
+from mako import util, lexer
+from mako.lookup import TemplateLookup
+
+from nikola.plugin_categories import TemplateSystem
+
+
+class MakoTemplates(TemplateSystem):
+ """Wrapper for Mako templates."""
+
+ name = "mako"
+
+ lookup = None
+ cache = {}
+
+ def get_deps(self, filename):
+ text = util.read_file(filename)
+ lex = lexer.Lexer(text=text, filename=filename)
+ lex.parse()
+
+ deps = []
+ for n in lex.template.nodes:
+ if getattr(n, 'keyword', None) == "inherit":
+ deps.append(n.attributes['file'])
+ # TODO: include tags are not handled
+ return deps
+
+ def set_directories(self, directories):
+ """Createa template lookup."""
+ cache_dir = os.path.join('cache', '.mako.tmp')
+ if os.path.exists(cache_dir):
+ shutil.rmtree(cache_dir)
+ self.lookup = TemplateLookup(
+ directories=directories,
+ module_directory=cache_dir,
+ output_encoding='utf-8',
+ )
+
+ def render_template(self, template_name, output_name, context):
+ """Render the template into output_name using context."""
+
+ template = self.lookup.get_template(template_name)
+ data = template.render_unicode(**context)
+ if output_name is not None:
+ try:
+ os.makedirs(os.path.dirname(output_name))
+ except:
+ pass
+ with open(output_name, 'w+') as output:
+ output.write(data)
+ return data
+
+ def template_deps(self, template_name):
+ """Returns filenames which are dependencies for a template."""
+ # We can cache here because depedencies should
+ # not change between runs
+ if self.cache.get(template_name, None) is None:
+ template = self.lookup.get_template(template_name)
+ dep_filenames = self.get_deps(template.filename)
+ deps = [template.filename]
+ for fname in dep_filenames:
+ deps += self.template_deps(fname)
+ self.cache[template_name] = tuple(deps)
+ return list(self.cache[template_name])
diff --git a/nikola/post.py b/nikola/post.py
index 9b2d73f..f4b0a0e 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -15,7 +15,7 @@ class Post(object):
"""Represents a blog post or web page."""
def __init__(self, source_path, destination, use_in_feeds,
- translations, default_lang, blog_url, compile_html, messages):
+ translations, default_lang, blog_url, messages):
"""Initialize post.
The base path is the .txt post file. From it we calculate
@@ -63,8 +63,6 @@ class Post(object):
self.is_draft = 'draft' in self.tags
self.tags = [t for t in self.tags if t != 'draft']
- self.compile_html = compile_html
-
self.pagenames = {}
self.titles = {}
self.descriptions = {}
diff --git a/nikola/utils.py b/nikola/utils.py
index 42e0c05..e319a6d 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -1,53 +1,59 @@
"""Utility functions."""
from collections import defaultdict
-import cPickle
import datetime
import hashlib
import os
import re
import codecs
+import json
import shutil
import string
import subprocess
import sys
from zipfile import ZipFile as zip
+from doit import tools
from unidecode import unidecode
import PyRSS2Gen as rss
__all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree',
- 'get_compile_html', 'get_template_module', 'generic_rss_renderer',
+ 'generic_rss_renderer',
'copy_file', 'slugify', 'unslugify', 'get_meta', 'to_datetime',
'apply_filters', 'config_changed']
-class config_changed(object):
- """ A copy of doit's but using pickle instead of serializing manually."""
+class CustomEncoder(json.JSONEncoder):
+ def default(self, obj):
+ try:
+ return json.JSONEncoder.default(self, obj)
+ except TypeError:
+ s = repr(obj).split('0x', 1)[0]
+ return s
- def __init__(self, config):
- self.config = config
- def __call__(self, task, values):
- config_digest = None
+class config_changed(tools.config_changed):
+ """ A copy of doit's but using pickle instead of serializing manually."""
+
+ def _calc_digest(self):
if isinstance(self.config, basestring):
- config_digest = self.config
+ return self.config
elif isinstance(self.config, dict):
- data = cPickle.dumps(self.config)
- config_digest = hashlib.md5(data).hexdigest()
+ data = json.dumps(self.config, cls=CustomEncoder)
+ if isinstance(data, unicode): # pragma: no cover # python3
+ byte_data = data.encode("utf-8")
+ else:
+ byte_data = data
+ return hashlib.md5(byte_data).hexdigest()
else:
- raise Exception(('Invalid type of config_changed parameter got %s'
- + ', must be string or dict') % (type(self.config),))
-
- def _save_config():
- return {'_config_changed': config_digest}
+ raise Exception(
+ ('Invalid type of config_changed parameter got %s' +
+ ', must be string or dict') % (type(self.config),))
- task.insert_action(_save_config)
- last_success = values.get('_config_changed')
- if last_success is None:
- return False
- return (last_success == config_digest)
+ def __repr__(self):
+ return "Change with config: %s" % json.dumps(
+ self.config, cls=CustomEncoder)
def get_theme_path(theme):
@@ -66,7 +72,7 @@ def get_theme_path(theme):
def re_meta(line, match):
- """ re.compile for meta"""
+ """re.compile for meta"""
reStr = re.compile('^%s(.*)' % re.escape(match))
result = reStr.findall(line)
if result:
@@ -123,19 +129,6 @@ def get_template_engine(themes):
# default
return 'mako'
-def get_theme_bundles(themes):
- """Given a theme chain, return the bundle definitions."""
- bundles = {}
- for theme_name in themes:
- bundles_path = os.path.join(get_theme_path(theme_name), 'bundles')
- if os.path.isfile(bundles_path):
- with open(bundles_path) as fd:
- for line in fd:
- name, files = line.split('=')
- files = [f.strip() for f in files.split(',')]
- bundles[name.strip()] = files
- break
- return bundles
def get_theme_chain(theme):
"""Create the full theme inheritance chain."""
@@ -164,14 +157,23 @@ def load_messages(themes, translations):
and "younger" themes have priority.
"""
messages = defaultdict(dict)
+ warned = []
for theme_name in themes[::-1]:
msg_folder = os.path.join(get_theme_path(theme_name), 'messages')
oldpath = sys.path
sys.path.insert(0, msg_folder)
+ english = __import__('en')
for lang in translations.keys():
# If we don't do the reload, the module is cached
translation = __import__(lang)
reload(translation)
+ if sorted(translation.MESSAGES.keys()) !=\
+ sorted(english.MESSAGES.keys()) and \
+ lang not in warned:
+ # FIXME: get real logging in place
+ print "Warning: Incomplete translation for language '%s'." % lang
+ warned.append(lang)
+ messages[lang].update(english.MESSAGES)
messages[lang].update(translation.MESSAGES)
del(translation)
sys.path = oldpath
@@ -216,84 +218,6 @@ def copy_tree(src, dst, link_cutoff=None):
}
-def get_compile_html(input_format):
- """Setup input format library."""
- if input_format == "rest":
- import rest
- compile_html = rest.compile_html
- elif input_format == "markdown":
- import md
- compile_html = md.compile_html
- elif input_format == "html":
- compile_html = copy_file
- return compile_html
-
-
-class CompileHtmlGetter(object):
- """Get the correct compile_html for a file, based on file extension.
-
- This class exists to provide a closure for its `__call__` method.
- """
- def __init__(self, post_compilers):
- """Store post_compilers for use by `__call__`.
-
- See the structure of `post_compilers` in conf.py
- """
- self.post_compilers = post_compilers
- self.inverse_post_compilers = {}
-
- def __call__(self, source_name):
- """Get the correct compiler for a post from `conf.post_compilers`
-
- To make things easier for users, the mapping in conf.py is
- compiler->[extensions], although this is less convenient for us. The
- majority of this function is reversing that dictionary and error
- checking.
- """
- ext = os.path.splitext(source_name)[1]
- try:
- compile_html = self.inverse_post_compilers[ext]
- except KeyError:
- # Find the correct compiler for this files extension
- langs = [lang for lang, exts in
- self.post_compilers.items()
- if ext in exts]
- if len(langs) != 1:
- if len(set(langs)) > 1:
- exit("Your file extension->compiler definition is"
- "ambiguous.\nPlease remove one of the file extensions"
- "from 'post_compilers' in conf.py\n(The error is in"
- "one of %s)" % ', '.join(langs))
- elif len(langs) > 1:
- langs = langs[:1]
- else:
- exit("post_compilers in conf.py does not tell me how to "
- "handle '%s' extensions." % ext)
-
- lang = langs[0]
- compile_html = get_compile_html(lang)
-
- self.inverse_post_compilers[ext] = compile_html
-
- return compile_html
-
-
-def get_template_module(template_engine, themes):
- """Setup templating library."""
- templates_module = None
- if template_engine == "mako":
- import mako_templates
- templates_module = mako_templates
- elif template_engine == "jinja":
- import jinja_templates
- templates_module = jinja_templates
- templates_module.lookup = \
- templates_module.get_template_lookup(
- [os.path.join(get_theme_path(name), "templates")
- for name in themes])
- return templates_module
-
-
def generic_rss_renderer(lang, title, link, description,
timeline, output_path):
"""Takes all necessary data, and renders a RSS feed in output_path."""
diff --git a/nikola/wordpress.py b/nikola/wordpress.py
deleted file mode 100644
index a04f19d..0000000
--- a/nikola/wordpress.py
+++ /dev/null
@@ -1,134 +0,0 @@
-import codecs
-import os
-import sys
-from urlparse import urlparse
-from urllib import urlopen
-
-from lxml import etree, html
-from mako.template import Template
-
-from nikola import utils
-
-links = {}
-
-def replacer(dst):
- return links.get(dst, dst)
-
-def get_text_tag(tag, name, default):
- t = tag.find(name)
- if t is not None:
- return t.text
- else:
- return default
-
-def import_attachment(item):
- post_type = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_type', 'post')
- if post_type == 'attachment':
- url = get_text_tag(item, '{http://wordpress.org/export/1.2/}attachment_url', 'foo')
- link = get_text_tag(item, '{http://wordpress.org/export/1.2/}link', 'foo')
- path = urlparse(url).path
- dst_path = os.path.join(*(['new_site', 'files']+list(path.split('/'))))
- dst_dir = os.path.dirname(dst_path)
- if not os.path.isdir(dst_dir):
- os.makedirs(dst_dir)
- print "Downloading %s => %s" % (url, dst_path)
- with open(dst_path, 'wb+') as fd:
- fd.write(urlopen(url).read())
- dst_url = '/'.join(dst_path.split(os.sep)[2:])
- links[link] = '/'+dst_url
- links[url] = '/'+dst_url
- return
-
-
-def import_item(item):
- """Takes an item from the feed and creates a post file."""
- title = get_text_tag(item, 'title', 'NO TITLE')
- # link is something like http://foo.com/2012/09/01/hello-world/
- # So, take the path, utils.slugify it, and that's our slug
- slug = utils.slugify(urlparse(get_text_tag(item, 'link', None)).path)
- description = get_text_tag(item, 'description', '')
- post_date = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_date', None)
- post_type = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_type', 'post')
- status = get_text_tag(item, '{http://wordpress.org/export/1.2/}status', 'publish')
- content = get_text_tag(item, '{http://purl.org/rss/1.0/modules/content/}encoded', '')
-
- tags = []
- if status != 'publish':
- tags.append('draft')
- for tag in item.findall('category'):
- text = tag.text
- if text == 'Uncategorized':
- continue
- tags.append(text)
-
- if post_type == 'attachment':
- return
- elif post_type == 'post':
- out_folder = 'posts'
- else:
- out_folder = 'stories'
- # Write metadata
- with codecs.open(os.path.join('new_site', out_folder, slug+'.meta'), "w+", "utf8") as fd:
- fd.write(u'%s\n' % title)
- fd.write(u'%s\n' % slug)
- fd.write(u'%s\n' % post_date)
- fd.write(u'%s\n' % ','.join(tags))
- fd.write(u'\n')
- fd.write(u'%s\n' % description)
- with open(os.path.join('new_site', out_folder, slug+'.wp'), "wb+") as fd:
- if content.strip():
- try:
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
- fd.write(html.tostring(doc, encoding='utf8'))
- except:
- import pdb; pdb.set_trace()
-
-
-def process(fname):
- # Parse the data
- context = {}
- with open(fname) as fd:
- xml = []
- for line in fd:
- # These explode etree and are useless
- if '<atom:link rel=' in line:
- continue
- xml.append(line)
- xml = '\n'.join(xml)
-
- tree = etree.fromstring(xml)
- channel = tree.find('channel')
-
- context['DEFAULT_LANG'] = get_text_tag(channel, 'language', 'en')
- context['BLOG_TITLE'] = get_text_tag(channel, 'title', 'PUT TITLE HERE')
- context['BLOG_DESCRIPTION'] = get_text_tag(channel, 'description', 'PUT DESCRIPTION HERE')
- context['BLOG_URL'] = get_text_tag(channel, 'link', '#')
- author = channel.find('{http://wordpress.org/export/1.2/}author')
- context['BLOG_EMAIL'] = get_text_tag(author,
- '{http://wordpress.org/export/1.2/}author_email', "joe@example.com")
- context['BLOG_AUTHOR'] = get_text_tag(author,
- '{http://wordpress.org/export/1.2/}author_display_name', "Joe Example")
- context['POST_PAGES'] = '''(
- ("posts/*.wp", "posts", "post.tmpl", True),
- ("stories/*.wp", "stories", "story.tmpl", False),
- )'''
- context['POST_COMPILERS'] = '''{
- "rest": ('.txt', '.rst'),
- "markdown": ('.md', '.mdown', '.markdown', '.wp'),
- "html": ('.html', '.htm')
- }
- '''
-
- # Generate base site
- os.system('nikola init new_site')
- conf_template = Template(filename = os.path.join(
- os.path.dirname(__file__), 'data', 'samplesite', 'conf.py.in'))
- with codecs.open(os.path.join('new_site', 'conf.py'), 'w+', 'utf8') as fd:
- fd.write(conf_template.render(**context))
-
- # Import posts
- for item in channel.findall('item'):
- import_attachment(item)
- for item in channel.findall('item'):
- import_item(item)