diff options
| author | 2012-12-12 20:15:50 -0300 | |
|---|---|---|
| committer | 2012-12-12 20:15:50 -0300 | |
| commit | 540e6aa6fcd86a98e270715d1d4e49493eacc270 (patch) | |
| tree | ad1b1fcca82d9638f5edcf7a4d9149ca113ba2d2 | |
| parent | 25744f4bf462020e353c503db3ec558604c19137 (diff) | |
| parent | 0f2c04e70a0ffdd0892d6970cafbcd952d221db5 (diff) | |
Merge tag 'upstream/5'
Upstream version 5
99 files changed, 3145 insertions, 3047 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 7897c64..f9a822d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,25 @@ +New in Master +============= + +Features +-------- + +* Templates now know their name from template_name in their context. +* Made most of Nikola into Yapsy plugins. +* Recurse galleries/ and render each folder as a gallery. +* Breadcrumbs and folder links in image galleries. + +Bugfixes +-------- + +* Fixed doc installation +* Put webassets cache in cache/webassets +* Update to doit 0.17 +* Don't crash on incomplete translations +* Fixed Issue 160: render_tags didn't call scan_posts() +* Fixed Issue 161: webassets setting USE_BUNDLES was ignored (fix by schettino72) +* Fixed Issue 153: index.txt was being ignored in galleries. + New in Version 4.0.3 ==================== diff --git a/docs/extending.txt b/docs/extending.txt new file mode 100644 index 0000000..4bbc2ea --- /dev/null +++ b/docs/extending.txt @@ -0,0 +1,301 @@ +Extending Nikola +================ + +:Version: 5 +:Author: Roberto Alsina <ralsina@netmanagers.com.ar> + +.. class:: alert alert-info pull-right + +.. contents:: + + +.. note:: This is a draft + + I am not sure of the best way to do some things, including how + to document this. Suggestions are welcome. + +Nikola is extensible. Almost all its functionality is based on plugins, +and you can add your own or replace the provided ones. + +Plugins consist of a metadata file (with ``.plugin`` extension) and +a python module (a ``.py`` file) or package (a folder containing +a ``__init__.py`` file. + +To use a plugin in your site, you just have to put it in a ``plugins`` +folder in your site. + +Plugins come in various flavours, aimed at extending different aspects +of Nikola. + +Command Plugins +--------------- + +When you run ``nikola --help`` you will see something like this:: + + $ nikola --help + Usage: nikola command [options] + + Available commands: + + nikola bootswatch_theme: Given a swatch name and a parent theme, creates a custom theme. + nikola build: Build the site. + nikola import_wordpress: Import a wordpress site from a XML dump. + nikola init: Create a new site. + nikola install_theme: Install a theme into the current site. + nikola new_post: Create a new post. + nikola serve: Start test server. + + For detailed help for a command, use nikola command --help + +That will give you a list of all available commands in your version of Nikola. +Each and every one of those is a plugin. Let's look at a typical example: + +First, the ``command_serve.plugin`` file: + +.. code_block:: init + + [Core] + Name = serve + Module = command_serve + + [Documentation] + Author = Roberto Alsina + Version = 0.1 + Website = http://nikola.ralsina.com.ar + Description = Start test server. + +For your own plugin, just change the values in a sensible way. The +``Module`` will be used to find the matching python module, in this case +``command_serve.py``, from which this is the interesting bit: + +.. code_block:: python + + from nikola.plugin_categories import Command + + # You have to inherit Command for this to be a + # command plugin: + + class CommandBuild(Command): + """Start test server.""" + + # This has to match the Name option in the .plugin file + + name = "serve" + + # This is the function that does stuff + + def run(self, *args): + """Start test server.""" + + # use OptionParser if you want your command to have options + + 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)) + + # You can use self.site.config to access your + # configuration options. self.site is an instance + # of the Nikola class and contains all your site's + # data. + + out_dir = self.site.config['OUTPUT_FOLDER'] + + # Then do something interesting. In this case, + # it starts a webserver + + 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() + +As mentioned above, a plugin can have options, which the user can see by doing +``nikola command --help`` and can later use as ``nikola command --option``:: + + $ nikola serve --help + Usage: nikola serve [options] + + Options: + -h, --help show this help message and exit + -p PORT, --port=PORT Port numer (default: 8000) + -a ADDRESS, --address=ADDRESS + Address to bind (default: 127.0.0.1) + + $ nikola serve -p 9000 + Serving HTTP on 127.0.0.1 port 9000 ... + +So, what can you do with commands? Well, anything you want, really. I have implemented +a sort of planet using it. So, be creative, and if you do something interesting, +let me know ;-) + +TemplateSystem Plugins +---------------------- + +Nikola supports Mako and Jinja2. If you prefer some other templating +system, then you will have to write a TemplateSystem plugin. Here's how they work. +First, you have to create a .plugin file. Here's the one for the Mako plugin: + +.. code_block:: ini + + [Core] + Name = mako + Module = template_mako + + [Documentation] + Author = Roberto Alsina + Version = 0.1 + Website = http://nikola.ralsina.com.ar + Description = Support for Mako templates. + +You will have to replace "mako" with your template system's name, and other data +in the obvious ways. + +The "Module" option is the name of the module, which has to look something like this, +a stub for a hypothetical system called "Templater": + +.. code_block:: python + + from nikola.plugin_categories import TemplateSystem + + # You have to inherit TemplateSystem + + class TemplaterTemplates(TemplateSystem): + """Wrapper for Templater templates.""" + + # name has to match Name in the .plugin file + name = "templater" + + # You *must* implement this, even if to return [] + # It should return a list of all the files that, + # when changed, may affect the template's output. + # usually this involves template inheritance and + # inclusion. + def get_deps(self, filename): + return [] + + # A list of directories where the templates will be + # located. Most template systems have some sort of + # template loading tool that can use this. + + def set_directories(self, directories): + """Createa template lookup.""" + pass + + # The method that does the actual rendering. + # template_name is the name of the template file, + # output_name is the file for the output, context + # is a dictionary containing the data the template + # uses for rendering. + + def render_template(self, template_name, output_name, + context, global_context): + """Render the template into output_name using context.""" + pass + + +Task Plugins +------------ + +If you want to do something that depends on the data in your site, you +probably want to do a Task plugin, which will make it be part of the +``nikola build`` command. There are the currently available tasks, all +provided by plugins:: + + $ nikola build list + + build_bundles + copy_assets + copy_files + deploy + redirect + render_archive + render_galleries + render_indexes + render_listings + render_pages + render_posts + render_rss + render_site + render_sources + render_tags + sitemap + +These have access to the ``site`` object which contains your timeline and +your configuration. + +The critical bit of Task plugins is their ``gen_tasks`` method, which ``yields`` +`doit tasks <http://python-doit.sourceforge.net/tasks.html>`_ + +The details of how to handle dependencies, etc. are a bit too much for this +document, so I'll just leave you with an example, the ``copy_assets`` task. +First the ``task_copy_assets.plugin`` file, which you should copy and edit +in the logical ways: + +.. code_block:: ini + + [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. + +And the ``task_copy_assets.py`` file, in its entirety: + +.. code_block:: python + + import os + + from nikola.plugin_categories import Task + from nikola import utils + + # Have to inherit Task to be a task plugin + class CopyAssets(Task): + """Copy theme assets into output.""" + + name = "copy_assets" + + # This yields the tasks + 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. + """ + + # I put all the configurations and data the plugin uses + # in a dictionary because utils.config_changed will + # make it so that if these change, this task will be + # marked out of date, and run again. + + 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'] = task.get('uptodate', []) + \ + [utils.config_changed(kw)] + task['basename'] = self.name + # If your task generates files, please do this. + yield utils.apply_filters(task, kw['filters']) diff --git a/docs/man/nikola.1 b/docs/man/nikola.1 new file mode 100644 index 0000000..8f77039 --- /dev/null +++ b/docs/man/nikola.1 @@ -0,0 +1,60 @@ +====== +nikola +====== + +-------------------------------- +A Static Site and Blog Generator +-------------------------------- + +:Manual section: 1 +:Manual group: nikola + +SYNOPSIS +======== + +**nikola** command [*options*] + + +DESCRIPTION +=========== + +nikola + is a simple yet powerful and flexible static website and blog generator + +OPTIONS +======= + +bootswatch_theme + Given a swatch name and a parent theme, creates a custom theme. + +build + Build the site. + +check + Check the generated site + +deploy + Deploy the site + +import_wordpress + Import a wordpress site from a XML dump (requires markdown). + +init + Create a new site. + +install_theme + Install a theme into the current site. + +new_post + Create a new post. + +serve + Start test server. + +command --help + Get detailed help for a command + +AUTHOR +====== + +Roberto Alsina <http://ralsina.com.ar/> diff --git a/docs/manual.txt b/docs/manual.txt index f8804e6..202afc1 100644 --- a/docs/manual.txt +++ b/docs/manual.txt @@ -1,7 +1,7 @@ The Nikola Handbook =================== -:Version: 2.1+svn +:Version: 5 :Author: Roberto Alsina <ralsina@netmanagers.com.ar> .. class:: alert alert-info pull-right @@ -18,16 +18,16 @@ Create a site: ``nikola init mysite`` Create a post: - ``doit new_post`` + ``nikola new_post`` Edit the post: The filename should be in the output of the previous command. Build the site: - ``doit`` + ``nikola build`` Start the test server: - ``doit serve`` + ``nikola serve`` See the site: http://127.0.0.1:8000 @@ -168,13 +168,14 @@ If you want to create a blog or a site, Nikola provides: * The input format is light markup (`reStructuredText <quickstart.html>`_ or `Markdown <http://daringfireball.net/projects/markdown/>`_) * Easy-to-create image galleries +* Support for displaying source code 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. +* "Smart" builds: only what changed gets rebuilt (usually in seconds) +* Easy to extend with minimal Python knowledge. Installing Nikola ----------------- @@ -187,24 +188,35 @@ The short version is: ``pip install https://github.com/ralsina/nikola/zipball/ma 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/>`_ +#. Get `Nikola <http://nikola.ralsina.com.ar/>`_ +#. Install dependencies. To do that, either: -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. + #. ``pip install -r requirements.txt`` or... + #. Install your distribution's packages for all the things + mentioned below, if they exist, or... + #. Get all of these manually: -Then get Nikola itself (<http://nikola.ralsina.com.ar/>), unzip it, and -run ``python setup.py install``. + #. 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/>`_ + #. Get `yapsy <http://yapsy.sourceforge.com>`_ + #. Get `configparser <http://pypi.python.org/pypi/configparser/3.2.0r3>`_ + +#. run ``python setup.py install`` After that, run ``nikola init sitename`` and that will create a folder called ``sitename`` containing a functional demo site. +.. note:: Are you using Ubuntu? + + Then you can try using `my PPA <https://launchpad.net/~ralsina/+archive/nikola>`_ + and installing python-nikola + Getting Started --------------- @@ -216,10 +228,10 @@ First, let's see how you "build" your site. Nikola comes with a minimal site to 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":: +first build, just run "nikola build":: - $ doit - Parsing metadata + $ nikola build + Scanning posts . . done! . render_posts:stories/manual.html . render_posts:posts/1.html . render_posts:stories/1.html @@ -238,66 +250,75 @@ first build, just run "doit":: 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 + $ nikola build + Scanning posts . . done! 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``:: +Nikola is mostly a series of doit *tasks*, and you can see them by doing ``nikola build list``:: - $ doit list + $ nikola build 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 + build_bundles + copy_assets + copy_files + deploy + redirect + render_archive + render_galleries + render_indexes + render_listings + render_pages + render_posts + render_rss + render_site + render_sources + render_tags + sitemap + +You can make Nikola redo everything by calling ``nikola build forget``, you can make it do just a specific +part of the site using task names, for example ``nikola build render_pages``, and even individual files like +``nikola build render_indexes:output/index.html`` + +Nikola also has other commands besides ``build``:: + + $ nikola help + Usage: nikola command [options] + + Available commands: + + nikola bootswatch_theme: Given a swatch name and a parent theme, creates a custom theme. + nikola build: Build the site. + nikola check: Check the generated site + nikola deploy: Deploy the site + nikola import_wordpress: Import a wordpress site from a XML dump (requires markdown). + nikola init: Create a new site. + nikola install_theme: Install a theme into the current site. + nikola new_post: Create a new post. + nikola serve: Start test server. + + For detailed help for a command, use nikola command --help + +The ``serve`` command starts a web server so you can see the site you are creating:: + + $ nikola 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! +the sample site. This is useful as a "preview" of your work. -By default, the ``serve`` task runs the web server on port 8000 on the IP address 127.0.0.1. +By default, the ``serve`` command 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 + $ nikola serve --address 0.0.0.0 --port 8080 Serving HTTP on 0.0.0.0 port 8080 ... -The ``deploy`` task is discussed in the Deployment_ section. - Creating a Blog Post -------------------- @@ -321,21 +342,22 @@ 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 + $ nikola new_post Creating New Post ----------------- - Enter title: How to Make Money - Your post's metadata is at: posts/how-to-make-money.meta + Enter title: How to make money Your post's text is at: posts/how-to-make-money.txt -The format for the ``.meta`` file is as follows:: +The content of that file is as follows:: - How to Make Money - how-to-make-money - 2012/04/09 13:59 + .. title: How to make money + .. slug: how-to-make-money + .. date: 2012/09/15 19:52:05 + .. tags: + .. link: + .. description: + Write your post here. 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. @@ -346,12 +368,22 @@ 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. +A fifth line that's a URL for an original source of the post, and a sixth line +that's the page description. + +.. note:: The Two-File Format + + Nikola originally used a separate ``.meta`` file. That will still work! + The format of the meta files is the same as shown above, but without the + explanations:: -And a sixth line that's the page description. + How to make money + how-to-make-money + 2012/09/15 19:52:05 If you are writing a multilingual site, you can also create a per-language -metadata file. This one can have two lines: +post file (for example: ``how-to-make-money.txt.es``). This one can have two +lines of metadata: 1) The translated title for the post or page 2) A translated version of the pagename @@ -404,22 +436,22 @@ configuration option:: 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. +The ``new_post`` command supports some options:: -In restructured text:: + $ nikola new_post --help + Usage: nikola new_post [options] - .. tags: test,demo - .. slug: demo-test - .. date: 2012/04/09 13:59 - .. link: http://foo.bar/baz + Options: + -h, --help show this help message and exit + -p, --page Create a page instead of a blog post. + -t TITLE, --title=TITLE + Title for the page/post. + --tags=TAGS Comma-separated tags for the page/post. + -1 Create post with embedded metadata (single file + format). + -f POST_FORMAT, --format=POST_FORMAT + Format for post (rest or markdown) -In Markdown: - <!-- - .. tags: test,demo - .. slug: demo-test - .. date: 2012/04/09 13:59 - .. link: http://foo.bar/baz - --> Teasers ~~~~~~~ @@ -458,8 +490,8 @@ Pages are the same as posts, except that: 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 +You can create the page's files manually or use the ``new_post`` command +with the ``-p`` option, qhich will place the files in the folder that has ``use_in_feed`` set to False. Redirections @@ -540,7 +572,8 @@ 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! +There are lots of things you can do to personalize your website, but let's see +the easy ones! Basics You can assume this needs to be changed:: @@ -588,19 +621,15 @@ 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:: +``install_theme`` command:: - $ doit install_theme -l - Scanning posts . . done! - . install_theme + $ nikola install_theme -l Themes: ------- blogtxt readable - $ doit install_theme -n blogtxt - Scanning posts . . done! - . install_theme + $ nikola install_theme -n blogtxt Downloading: http://nikola.ralsina.com.ar/themes/blogtxt.zip Extracting: blogtxt into themes @@ -615,10 +644,8 @@ 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 + $ nikola bootswatch_theme -n custom_theme -s spruce -p site + 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. @@ -634,7 +661,7 @@ 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``. +option, and run them with ``nikola deploy``. One caveat is that if any command has a % in it, you should double them. diff --git a/docs/theming.txt b/docs/theming.txt index 339ecd4..93c7824 100644 --- a/docs/theming.txt +++ b/docs/theming.txt @@ -1,7 +1,7 @@ Theming Nikola ============== -:Version: 2.1+svn +:Version: 5 :Author: Roberto Alsina <ralsina@netmanagers.com.ar> .. class:: alert alert-info pull-right 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äge aus dem Jahr %s", u"Archive": u"Archiv", - u"Posts about %s:": u"Einträge über %s", + u"Posts about %s": u"Einträge über %s", u"Tags": u"Tags", - u"Also available in: ": u"Auch verfügbar in: ", + u"Also available in": u"Auch verfügbar in", u"More posts about": u"Weitere Einträge über", - u"Posted:": u"Veröffentlicht:", + u"Posted": u"Veröffentlicht", u"Original site": u"Original-Seite", u"Read in English": u"Auf Deutsch lesen", - u"Older posts →": u"Ältere Einträge →", - u"← Newer posts": u"← Neuere Einträge", - u"← Previous post": u"← Vorheriger Eintrag", - u"Next post →": u"Nächster Eintrag →", + u"Older posts": u"Ältere Einträge", + u"Newer posts": u"Neuere Einträge", + u"Previous post": u"Vorheriger Eintrag", + u"Next post": u"Nächster Eintrag", u"Source": u"Source", + u"Read more": u"Weiterlesen", + u"old posts page %d": u'Vorherige Einträ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"← Newer posts", - u"Older posts →", - u"← Previous post", - u"Next post →", + 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 →": u"Posts anteriores →", - u"← Newer posts": u"← Posts posteriores", - u"← Previous post": u"← Post anterior", - u"Next post →": u"Siguiente post →", + 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"← Newer posts": u"← Billets récents", - u"Older posts →": u"Anciens billets →", + 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"← Newer posts": u"← Νεότερες αναρτήσεις", - u"Older posts →": u"Παλαιότερες αναρτήσεις →", - u"← Previous post": u"← Προηγούμενη ανάρτηση", - u"Next post →": u"Επόμενη ανάρτηση →", + 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"← 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"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 →": u"Старые записи →", - u"← Newer posts": u"← Новые записи", + 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"])}: %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> ${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> - ${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]["← Newer posts"]}</a> + <a href="${prevlink}">← ${messages[lang]["Newer posts"]}</a> </li> %endif %if nextlink: <li class="next"> - <a href="${nextlink}">${messages[lang]["Older posts →"]}</a> + <a href="${nextlink}">${messages[lang]["Older posts"]} →</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} | + ${messages[lang]["Posted"]}: ${post.date} | %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]["← Previous post"]}</a> + <a href="${post.prev_post.permalink(lang)}">← ${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 →"]}</a> + <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} →</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"] }}: {% 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> {{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> - {{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]["← Newer posts"]}</a> + <a href="{{prevlink}}">← {{messages[lang]["Newer posts"]}}</a> </li> {% endif %} {% if nextlink %} <li class="next"> - <a href="{{nextlink}}">${messages[lang]["Older posts →"]}</a> + <a href="{{nextlink}}">${messages[lang]["Older posts"]} →</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}} | + {{messages[lang]["Posted"]}}: {{post.date}} | {% 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]["← Previous post"]}}</a> + <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">← {{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 →"]}}</a> + <a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post"]}} →</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]["← Previous post"]}</a> + <a href="${post.prev_post.permalink(lang)}">← ${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 →"]}</a> + <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} →</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/scripts/nikola_check b/nikola/plugins/command_check.py index 797c29b..ce1e2e3 100644 --- a/scripts/nikola_check +++ b/nikola/plugins/command_check.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +from optparse import OptionParser import os import sys import urllib @@ -6,8 +6,33 @@ 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] @@ -21,7 +46,9 @@ def analize(task): continue if parsed.fragment: target = target.split('#')[0] - target_filename = os.path.abspath(os.path.join(os.path.dirname(filename), urllib.unquote(target))) + 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) @@ -29,14 +56,17 @@ def analize(task): print "In %s broken link: " % filename, target if '--find-sources' in sys.argv: print "Possible sources:" - print os.popen('doit list --deps %s' % task, 'r').read() + 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(): - for task in os.popen('doit list --all', 'r').readlines(): + 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', @@ -47,11 +77,13 @@ def scan_links(): '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('doit list --all', 'r').readlines(): + 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] @@ -75,13 +107,3 @@ def scan_files(): print "\nFiles not generated:\n" for f in only_on_input: print f - - -if __name__ == '__main__': - if '--help' in sys.argv or len(sys.argv) == 1: - print "Usage: nikola_check [--check-links [--find-sources]] [--check-files]" - sys.exit() - elif '--check-links' in sys.argv: - scan_links() - elif '--check-files' in sys.argv: - scan_files() 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) diff --git a/requirements.txt b/requirements.txt index ecce1b0..50482e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ -doit>=0.16.1 +doit>=0.17 pygments pillow docutils mako>=0.6 unidecode lxml +configparser +yapsy diff --git a/scripts/nikola b/scripts/nikola index 4b895d2..a20ac43 100755 --- a/scripts/nikola +++ b/scripts/nikola @@ -2,34 +2,68 @@ """Nikola main script.""" +from __future__ import print_function import os -import shutil import sys -import nikola +# LIBDIR trick start (marker for removal on platforms that don't need it) +libdir = '@LIBDIR@' -USAGE = """To create a new site in a folder, run "nikola init foldername [src]". +# Two cases: +if libdir != '@' 'LIBDIR' '@': + # Changed by our distutils hook, then use the given path. -The destination folder must not exist. + if not os.path.isabs(libdir): + libdir = os.path.join(os.path.dirname( + os.path.realpath(__file__)), libdir) + libdir = os.path.abspath(libdir) +else: + # Unchanged, running from checkout, + # use the parent directory, the nikola package ought be there. + libdir = os.path.join(os.path.dirname(__file__), "..") -If you pass the src argument, that folder will be used as a template for -the new site instead of Nikola's sample site. -""" +sys.path.insert(0, libdir) +if "PYTHONPATH" not in os.environ: + os.environ["PYTHONPATH"] = libdir +else: + os.environ["PYTHONPATH"] = os.environ["PYTHONPATH"] + ":" + libdir + +# LIBDIR trick end (marker for removal on platforms that don't need it) + +import nikola -def init(dst): - """Create a copy of demosite in the current folder.""" - if len(sys.argv) > 3: - src = sys.argv[3] - else: - src = os.path.join(os.path.dirname(nikola.__file__),'data','samplesite') - shutil.copytree(src, dst) - print "A new site with some sample data has been created at %s." % dst - print "See README.txt in that folder for more information." + +def print_help(site): + print("Usage: nikola command [options]") + print() + print("Available commands:") + print() + keys = sorted(site.commands.keys()) + for name in keys: + print("nikola %s: %s" % (name, site.commands[name].short_help)) + print() + print("For detailed help for a command, use nikola command --help") if __name__ == "__main__": - if len(sys.argv)>=3 and sys.argv[1] == "init": - print "Doing init" - init(sys.argv[2]) + + try: + sys.path.append('') + import conf + config = conf.__dict__ + except ImportError: + config = {} + + site = nikola.Nikola(**config) + + if len(sys.argv) < 2: + sys.argv[1:] = 'help' + cmd_name = sys.argv[1] + + if cmd_name in ("help", "--help", "-h"): + print_help(site) + elif cmd_name in site.commands: + site.commands[cmd_name].run(*sys.argv[2:]) else: - print USAGE + print("Unknown command: %s" % cmd_name) + print_help(site) diff --git a/scripts/nikola_import_wordpress b/scripts/nikola_import_wordpress deleted file mode 100644 index 015d6a0..0000000 --- a/scripts/nikola_import_wordpress +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -import sys -from nikola import wordpress - -if __name__ == "__main__": - fname = sys.argv[-1] - wordpress.process(fname) @@ -7,18 +7,23 @@ # http://www.opensource.org/licenses/mit-license.php import os +import subprocess import sys from fnmatch import fnmatchcase from distutils.util import convert_path +from distutils.command.install import install dependencies = [ - 'doit>=0.16.1', + 'doit>=0.18.1', 'pygments', 'pillow', 'docutils', 'mako>=0.6', 'unidecode', - 'lxml'] + 'lxml', + 'yapsy', + 'configparser', +] # Provided as an attribute, so you can append to these instead # of replicating them: @@ -27,6 +32,34 @@ standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', './dist', 'EGG-INFO', '*.egg-info') +def install_manpages(prefix): + man_pages = [ + ('docs/man/nikola.1', 'share/man/man1/nikola.1'), + ] + join = os.path.join + normpath = os.path.normpath + for src, dst in man_pages: + path_dst = join(normpath(prefix), normpath(dst)) + try: + os.makedirs(os.path.dirname(path_dst)) + except OSError: + pass + rst2man_cmd = ['rst2man.py', 'rst2man'] + for rst2man in rst2man_cmd: + try: + subprocess.call([rst2man, src, path_dst]) + except OSError: + continue + else: + break + + +class nikola_install(install): + def run(self): + install.run(self) + install_manpages(self.prefix) + + def find_package_data( where='.', package='', exclude=standard_exclude, @@ -109,15 +142,20 @@ def find_package_data( from distutils.core import setup setup(name='Nikola', - version='4.0.3', + version='5', description='Static blog/website generator', author='Roberto Alsina and others', author_email='ralsina@netmanagers.com.ar', url='http://nikola.ralsina.com.ar/', packages=['nikola'], - scripts=['scripts/nikola', 'scripts/nikola_check', 'scripts/nikola_import_wordpress'], + scripts=['scripts/nikola'], install_requires=dependencies, package_data=find_package_data(), - data_files=['docs/manual.txt', - 'docs/theming.txt'], + cmdclass={'install': nikola_install}, + data_files=[ + ('share/doc/nikola', [ + 'docs/manual.txt', + 'docs/theming.txt', + 'docs/extending.txt']), + ], ) |
