aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-03-13 20:58:39 -0300
commit1004b9f3c61574acbdb3ec2f303d35307949fb7e (patch)
tree3ae41121a82650e6889fda82a4316f989dfc0b4b
parent1c7c74d71f5dc9d13d029c9df8d46f27907a7503 (diff)
parent8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (diff)
Merge tag 'upstream/5.4.2'
Upstream version 5.4.2
-rw-r--r--.travis.yml7
-rw-r--r--AUTHORS.txt2
-rw-r--r--CHANGES.txt48
-rw-r--r--README.md2
-rw-r--r--docs/creating-a-site.txt181
-rw-r--r--docs/manual.txt276
-rw-r--r--docs/theming.txt4
-rw-r--r--extra_plugins/command_planetoid/__init__.py22
-rw-r--r--extra_plugins/task_mustache/__init__.py3
-rw-r--r--nikola/conf.py.in77
-rw-r--r--nikola/console.py11
-rw-r--r--nikola/data/samplesite/files/images/biohazard.pngbin0 -> 179 bytes
-rw-r--r--nikola/data/samplesite/listings/hello.py3
-rw-r--r--nikola/data/samplesite/stories/creating-a-theme.meta3
l---------nikola/data/samplesite/stories/creating-a-theme.txt1
-rw-r--r--nikola/data/samplesite/stories/quickref.txt60
-rw-r--r--nikola/data/samplesite/stories/quickstart.txt18
-rw-r--r--nikola/data/themes/default/assets/css/theme.css7
-rw-r--r--nikola/data/themes/default/assets/js/mathjax.js12
-rw-r--r--nikola/data/themes/default/bundles2
-rw-r--r--nikola/data/themes/default/templates/base.tmpl4
-rw-r--r--nikola/data/themes/default/templates/base_helper.tmpl13
-rw-r--r--nikola/data/themes/default/templates/gallery.tmpl6
-rw-r--r--nikola/data/themes/default/templates/index.tmpl3
-rw-r--r--nikola/data/themes/default/templates/index_helper.tmpl8
-rw-r--r--nikola/data/themes/default/templates/listing.tmpl14
-rw-r--r--nikola/data/themes/default/templates/post.tmpl14
-rw-r--r--nikola/data/themes/default/templates/post_helper.tmpl31
-rw-r--r--nikola/data/themes/jinja-default/templates/base.tmpl12
-rw-r--r--nikola/data/themes/jinja-default/templates/gallery.tmpl8
-rw-r--r--nikola/data/themes/jinja-default/templates/index.tmpl4
-rw-r--r--nikola/data/themes/jinja-default/templates/listing.tmpl2
-rw-r--r--nikola/data/themes/jinja-default/templates/post.tmpl2
-rw-r--r--nikola/data/themes/site/README2
-rw-r--r--nikola/data/themes/site/assets/css/theme.css9
-rw-r--r--nikola/data/themes/site/templates/post.tmpl25
-rw-r--r--nikola/filters.py17
-rw-r--r--nikola/main.py125
-rw-r--r--nikola/nikola.py249
-rw-r--r--nikola/plugin_categories.py66
-rw-r--r--nikola/plugins/__init__.py1
-rw-r--r--nikola/plugins/command_bootswatch_theme.py57
-rw-r--r--nikola/plugins/command_build.plugin10
-rw-r--r--nikola/plugins/command_build.py67
-rw-r--r--nikola/plugins/command_check.py67
-rw-r--r--nikola/plugins/command_console.py32
-rw-r--r--nikola/plugins/command_deploy.py8
-rw-r--r--nikola/plugins/command_import_blogger.py139
-rw-r--r--nikola/plugins/command_import_wordpress.py240
-rw-r--r--nikola/plugins/command_init.py44
-rw-r--r--nikola/plugins/command_install_theme.py57
-rw-r--r--nikola/plugins/command_new_post.py127
-rw-r--r--nikola/plugins/command_serve.py38
-rw-r--r--nikola/plugins/compile_bbcode.py8
-rw-r--r--nikola/plugins/compile_html.py8
-rw-r--r--nikola/plugins/compile_markdown/__init__.py14
-rw-r--r--nikola/plugins/compile_rest/__init__.py10
-rw-r--r--nikola/plugins/compile_rest/gist_directive.py12
-rw-r--r--nikola/plugins/compile_rest/pygments_code_block_directive.py39
-rw-r--r--nikola/plugins/compile_rest/slides.py7
-rw-r--r--nikola/plugins/compile_rest/soundcloud.py32
-rw-r--r--nikola/plugins/compile_rest/vimeo.py10
-rw-r--r--nikola/plugins/compile_rest/youtube.py8
-rw-r--r--nikola/plugins/compile_textile.py8
-rw-r--r--nikola/plugins/compile_txt2tags.py8
-rw-r--r--nikola/plugins/task_create_bundles.py12
-rw-r--r--nikola/plugins/task_indexes.py4
-rw-r--r--nikola/plugins/task_redirect.py5
-rw-r--r--nikola/plugins/task_render_galleries.py11
-rw-r--r--nikola/plugins/task_render_listings.py56
-rw-r--r--nikola/plugins/task_render_rss.py4
-rw-r--r--nikola/plugins/task_render_tags.py22
-rw-r--r--nikola/plugins/task_sitemap/__init__.py26
-rw-r--r--nikola/plugins/task_sitemap/sitemap_gen.py2
-rw-r--r--nikola/post.py288
-rw-r--r--nikola/utils.py161
-rw-r--r--requirements-3.txt11
-rw-r--r--requirements.txt11
-rwxr-xr-xscripts/nikola36
-rwxr-xr-x[-rw-r--r--]setup.py4
-rw-r--r--tests/data/translated_titles/conf.py397
-rw-r--r--tests/data/translated_titles/stories/1.txt5
-rw-r--r--tests/data/translated_titles/stories/1.txt.es4
-rw-r--r--tests/test_command_import_wordpress.py142
-rw-r--r--tests/test_command_init.py15
-rw-r--r--tests/test_integration.py157
-rw-r--r--tests/test_rss_feeds.py12
-rw-r--r--tests/test_utils.py121
-rw-r--r--tests/wordpress_export_example.xml52
89 files changed, 2753 insertions, 1209 deletions
diff --git a/.travis.yml b/.travis.yml
index b0f9231..c30c430 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,12 +3,15 @@ python:
- "2.6"
- "2.7"
- "3.2"
+ - "3.3"
# command to install dependencies
# pip is run inside the script because we need have different requirements for Python 2 / 3.
install:
- "python install_requirements.py"
+ - "pip install flake8 --use-mirrors"
- "pip install . --use-mirrors"
# We run tests and afterwards nikola to see if the command is executable.
-script:
+script:
- nosetests --with-doctest
- - nikola
+ - nikola help
+ - "flake8 --exit-zero nikola --ignore=E501"
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 31bc7b7..b241433 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -2,4 +2,6 @@ Roberto Alsina <ralsina@kde.org>
Eduardo Schettino <https://github.com/schettino72>
Niko Wenselowski
Roman Imankulov <https://github.com/imankulov>
+Kay Hayen <kay.hayen@gmail.com>
Zhaojun Meng <https://github.com/zhaojunmeng>
+Chris “Kwpolska” Warrick <kwpolska@gmail.com>
diff --git a/CHANGES.txt b/CHANGES.txt
index fe55892..376bcdc 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,51 @@
+New in 5.4.2
+============
+
+Bugfixes
+--------
+
+* Fix relative paths when stories is "dropped to root" (Issue #362)
+* Pick translated titles in 1-file posts (Issue #365)
+
+New in 5.4
+==========
+
+Features
+--------
+
+* Twitter Card / Open Graph support.
+* Smart math support
+* New soundcould directive
+* Custom "read more" links
+* Better time display, timezone support
+* Better doit integration (Issue #151)
+* Make the whole listings folder browsable (Issue #128)
+* New GZIP_FILES/GZIP_EXTENSIONS options to create gzipped copies of some files (Issue #348)
+* New optional path parameter to new_post command.
+* Wordpress importer: option to not download files
+* Wordpress importer: option to squash newlines
+* Separated BLOG_URL into SITE_URL and BASE_URL
+* Added DISABLED_PLUGINS option (Issue #354)
+
+Bugfixes
+--------
+
+* Added missing </div> in default theme templates.
+* Wordpress import: Description is left empty if no description is found.
+* When running the build command it is now possible to get help.
+* Load jQuery before bootstrap in JS bundle (Issue #327)
+* Generate valid HTML5 for redirects (Issue #276)
+* Fixed "nikola check"
+* Fixed internationalized RSS links
+* Make commands that need to be run in a site fail gracefully (Issue #342)
+* Use localized links on lxml fixer
+* Redirections created during the import from wordpress are now written to conf.py
+* Stop parsing metadata in post file on first blank line
+* Metadata handling cleanups by Tordek
+* Fixed blockquote font size inconsistency
+* Wordpress Import: Do not break indentation (issue #189)
+* Make things work even if SITE_URL has a path (Fix #307)
+
New in 5.3
==========
diff --git a/README.md b/README.md
index 94bba0f..943128f 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,8 @@ Nikola, a Static Site and Blog Generator
In goes content, out comes a website, ready to deploy.
+[![Build Status](https://travis-ci.org/ralsina/nikola.png)](https://travis-ci.org/ralsina/nikola)
+
Why Static Websites?
--------------------
diff --git a/docs/creating-a-site.txt b/docs/creating-a-site.txt
new file mode 100644
index 0000000..41985c9
--- /dev/null
+++ b/docs/creating-a-site.txt
@@ -0,0 +1,181 @@
+.. slug: creating-a-site-not-a-blog-with-nikola
+.. date: 2013/03/01 12:49:41
+.. tags: nikola, python
+.. link:
+.. description:
+
+Creating a Site (Not a Blog) with Nikola
+========================================
+
+One of the most frequent questions I get about Nikola is "but how do
+I create a site that's not a blog?". And of course, that's because the
+documentation is heavily blog-oriented. This document will change that ;-)
+
+Since it started, Nikola has had the capabilities to create generic sites. For example,
+Nikola's `own site <http://nikola.ralsina.com.ar>`_ is a fairly generic one. Let's go
+step by step on how you can do something like that.
+
+As usual when starting a nikola site, you start with ``nikola init`` which creates a
+empty semi-configured site::
+
+ $ nikola init mysite
+ Created empty site at mysite.
+
+Then we go into the new ``mysite`` folder, and make the needed changes in the ``conf.py``
+configuration file:
+
+.. code-block:: python
+
+
+ ##############################################
+ # Configuration, please edit
+ ##############################################
+
+
+ # Data about this site
+ BLOG_AUTHOR = "Roberto Alsina"
+ BLOG_TITLE = "Not a Blog"
+ # This is the main URL for your site. It will be used
+ # in a prominent link
+ SITE_URL = "http://notablog.ralsina.com.ar"
+ BLOG_EMAIL = "ralsina@kde.org"
+ BLOG_DESCRIPTION = "This is a demo site (not a blog) for Nikola."
+
+ #
+ # Some things in the middle you don't really need to change...
+ #
+
+ post_pages = (
+ ("pages/*.txt", "", "story.tmpl", False),
+ )
+
+And now we are ready to create our first page::
+
+ $ nikola new_post -p
+ Creating New Post
+ -----------------
+
+ Enter title: index
+ Your post's text is at: pages/index.txt
+
+.. note:: The ``-p`` option in the ``nikola new_post`` command means we are creating a page and not a blog post.
+
+We can now build and preview our site::
+
+ $ nikola build
+ Scanning posts.done!
+ . render_site:output/categories/index.html
+ . render_sources:output/index.txt
+ . render_rss:output/rss.xml
+ :
+ :
+ : [Much more of the same]
+
+ $ nikola serve
+ Serving HTTP on 127.0.0.1 port 8000 ...
+
+And you can see your (very empty) site in http://localhost:8000
+
+So, what's in that ``pages/index.txt`` file?
+
+.. code-block:: rest
+
+ .. title: index
+ .. slug: index
+ .. date: 2013/03/01 10:26:17
+ .. tags:
+ .. link:
+ .. description:
+
+
+ Write your post here.
+
+Title is the page title, slug is the name of the generated HTML file
+(in this case it would be ``index.html``) the date doesn't matter much in
+not-blogs, same for tags and link. Description is useful for SEO purposes
+if you care for that.
+
+And below, the content. By default you are expected to use
+`reStructured text <http://nikola.ralsina.com.ar/quickstart.html>`_ but
+Nikola supports a ton of formats, including Markdown, plain HTML, BBCode,
+Wiki, and Textile.
+
+So, let's give the page a nicer title, and some fake content. Since the default
+Nikola theme (called "site") is based on `bootstrap <http://twitter.github.com/bootstrap/>`_
+you can use anything you like from it:
+
+.. code-block:: rest
+
+ .. title: Welcome To The Fake Site
+ .. slug: index
+ .. date: 2013/03/01 10:26:17
+ .. tags:
+ .. link:
+ .. description: Fake Site version 1, welcome page!
+
+
+ .. class:: hero-unit span6
+
+ .. admonition:: This is a Fake Site
+
+ It pretends to be about things, but is really just an example.
+ So, don't click this button, it leads nowhere.
+
+ .. class:: btn
+
+ Click Me!
+
+
+ .. class:: span5
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris non nunc turpis.
+ Phasellus a ullamcorper leo. Sed fringilla dapibus orci eu ornare. Quisque
+ gravida quam a mi dignissim consequat. Morbi sed iaculis mi. Vivamus ultrices
+ mattis euismod. Mauris aliquet magna eget mauris volutpat a egestas leo rhoncus.
+ In hac habitasse platea dictumst. Ut sed mi arcu. Nullam id massa eu orci
+ convallis accumsan. Nunc faucibus sodales justo ac ornare. In eu congue eros.
+ Pellentesque iaculis risus urna. Proin est lorem, scelerisque non elementum at,
+ semper vel velit. Phasellus consectetur orci vel tortor tempus imperdiet. Class
+ aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
+ himenaeos.
+
+ [And more in the same vein]
+
+.. admonition:: TIP: Nice URLs
+
+ If you like your URLs without the ".html" then you want to create folders and
+ put the pages in ``index.html`` inside them. Example::
+
+ nikola new_post -p pages/foo/index.txt
+
+ which will create a page you could access as "http://yoursite.com/foo"
+
+And that's it. You will want to change the SIDEBAR_LINKS option to create a reasonable
+"menu" for your site, you will want to hack the theme (check ``nikola help bootswatch_theme``
+for a quick & dirty solution), and you may want to add a blog later on, for company news
+or whatever.
+
+.. admonition:: TIP: So, how do I add a blog now?
+
+ First, change the ``post_pages`` option like this:
+
+ .. code-block:: python
+
+ post_pages = (
+ ("pages/*.txt", "", "story.tmpl", False),
+ ("posts/*.txt", "blog", "post.tmpl", True),
+ )
+
+ And to avoid a conflict (because blogs try to generate ``/index.html``:
+
+ .. code-block:: python
+
+ INDEX_PATH = "blog"
+
+ Create a post with ``nikola new_post`` and that's it, you now have a blog
+ in http://yoursite.com/blog (you may want to add links to it in SIDEBAR_LINKS of course).
+
+You can see the finished site in http://notablog.ralsina.com.ar and its full configuration in
+http://ralsina.com.ar/listings/notablog/conf.py.html
+
+I hope this was helpful!
diff --git a/docs/manual.txt b/docs/manual.txt
index caf9780..b2290c2 100644
--- a/docs/manual.txt
+++ b/docs/manual.txt
@@ -1,7 +1,7 @@
The Nikola Handbook
===================
-:Version: 5.3
+:Version: 5.4.2
:Author: Roberto Alsina <ralsina@netmanagers.com.ar>
.. class:: alert alert-info pull-right
@@ -226,7 +226,7 @@ libxml/xmlversion.h: No such file or directory`` when running ``pip install -r r
Debian systems:
sudo apt-get install libxml2-dev
- sudo apt-get install libxslt1-dev
+ sudo apt-get install libxslt1-dev
RHEL systems:
@@ -235,7 +235,7 @@ RHEL systems:
Getting Started
---------------
-To create posts and pages in Nikola, you write them in one of the supported input formats.
+To create posts and pages in Nikola, you write them in one of the supported input formats.
Those source files are later converted to HTML
The recommended formats are restructured text and Markdown, but there is also support
for textile and WikiCreole and even for just writing HTML.
@@ -304,21 +304,32 @@ part of the site using task names, for example ``nikola build render_pages``, an
Nikola also has other commands besides ``build``::
$ nikola help
- Usage: nikola command [options]
-
+ Nikola
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
+ nikola auto automatically execute tasks when a dependency changes
+ nikola bootswatch_theme Given a swatch name and a parent theme, creates a custom theme.
+ nikola build run tasks
+ nikola check Check links and files in the generated site.
+ nikola clean clean action / remove targets
+ nikola console A short explanation.
+ nikola deploy Deploy the site.
+ nikola dumpdb dump dependency DB
+ nikola forget clear successful run status from internal DB
+ nikola help show help
+ nikola ignore ignore task (skip) on subsequent runs
+ nikola import_blogger Import a blogger dump.
+ nikola import_wordpress Import a wordpress dump.
+ nikola init Create a Nikola site in the specified folder.
+ nikola install_theme Install theme into current site.
+ nikola list list tasks from dodo file
+ nikola new_post Create a new blog post or site page.
+ nikola run run tasks
+ nikola serve Start the test webserver.
+ nikola strace use strace to list file_deps and targets
+
+ nikola help show help / reference
+ nikola help <command> show command usage
+ nikola help <task-name> show task usage
The ``serve`` command starts a web server so you can see the site you are creating::
@@ -340,25 +351,24 @@ Example usage::
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
+To create a new post, the easiest way is to run ``nikola new_post``. You will
+be asked for a title for your post, and it will tell you where the post's file
+is located.
-.md .markdown .mdown
- Markdown
+By default, that file will contain also some extra information about your post ("the metadata").
+It can be placed in a separate file by using the ``-2`` option, but it's generally
+easier to keep it in a single location.
-.htm .html
- HTML
+The contents of your post have to be written (by default) in `restructured text <http://docutils.sf.net>`_
+but you can use a lot of different markups using the ``-f`` option. Currently
+Nikola supports bbcode, wiki, markdown, html, txt2tags and textile in addition
+to restructured text.
-The default configuration expects them to be placed in ``posts`` but that can be
+You can control what markup compiler is used for each file extension with the ``post_compilers``
+option. 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::
+This is how it works::
$ nikola new_post
Creating New Post
@@ -375,19 +385,19 @@ The content of that file is as follows::
.. tags:
.. link:
.. description:
+
Write your post here.
-The first line is the title. The second one is the pagename. Since often titles will have
+The ``slug`` 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)::
+The other lines are optional. Tags are comma-separated. The ``link`` is an original
+source for the content, and ``description`` is mostly useful for SEO.
- programming, python, fame, fortune
+You can add your own metadata fields in the same manner, if you use a theme that
+supports them (for example: ``.. author: John Doe``)
-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
@@ -460,20 +470,21 @@ one in the list if all of them have it set to False.
The ``new_post`` command supports some options::
- $ nikola new_post --help
- Usage: nikola new_post [options]
-
+ $ nikola help new_post
+ Purpose: Create a new blog post or site page.
+ Usage: nikola new_post [options] [path]
+
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)
+ -p, --page Create a page instead of a blog post.
+ -t ARG, --title=ARG Title for the page/post.
+ --tags=ARG Comma-separated tags for the page/post.
+ -1 Create post with embedded metadata (single file format)
+ -2 Create post with separate metadata (two file format)
+ -f ARG, --format=ARG Markup format for post, one of rest, markdown, wiki, bbcode, html, textile, txt2tags
+The optional ``path`` parameter tells nikola exactly where to put it instead of guessing from your config.
+So, if you do ``nikola new_post posts/random/foo.txt`` you will have a post in that path, with
+"foo" as its slug.
Teasers
~~~~~~~
@@ -497,6 +508,11 @@ this behaviour with your ``conf.py``: ``INDEX_TEASERS`` defines whether index
page should display the whole contents or only teasers. ``RSS_TEASERS``
works the same way for your RSS feeds.
+By default, teasers will include a "read more" link at the end. If you want to
+change that text, you can use a custom teaser::
+
+ .. TEASER_END: click to read the rest of the article
+
Drafts
~~~~~~
@@ -552,49 +568,14 @@ 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 surely want to edit these options::
-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.
+ # 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."
-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
---------------------
@@ -602,15 +583,6 @@ Customizing Your Site
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::
-
- # 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.
@@ -643,6 +615,29 @@ Analytics
a Google analytics snippet or something similar, but you can really put anything
there.
+
+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'
+
Getting More Themes
-------------------
@@ -765,6 +760,28 @@ be stripped. For example ``03_an_amazing_sunrise.jpg`` will be render as *An ama
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>`_.
+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.
+
Optimizing Your Website
-----------------------
@@ -783,6 +800,9 @@ different ones, or about other webservers, please share!
AddType text/css .css
+#. Optionally you can greate static compressed copies and save some CPU on your server
+ with the GZIP_FILES option in Nikola.
+
#. The webassets Nikola plugin can drastically decrease the number of CSS and JS files your site fetches.
#. Through the filters feature, you can run your files through arbitrary commands, so that images
@@ -809,7 +829,7 @@ Once you have that, all you need to do is::
.. youtube:: 8N_tupPBtWQ
Vimeo
-~~~~~~~
+~~~~~
To link to a vimeo video, you need the id of the video. For example, if the
URL of the video is http://www.vimeo.com/20241459 then the id is **20241459**
@@ -827,6 +847,18 @@ You can override this if you wish::
height=240
width=320
+Soundcloud
+~~~~~~~~~~
+
+This directive lets you share music from http://soundcloud.com You first need to get the
+ID for the piece, which you can find in the "share" link. For example, if the
+Wordpress code starts like this::
+
+ [soundcloud url="http://api.soundcloud.com/tracks/78131362"
+
+The ID is 78131362 and you can embed the audio with this::
+
+ .. soundcloud:: 78131362
code-block
~~~~~~~~~~
@@ -892,22 +924,22 @@ Slideshows
To create an image slideshow, you can use the ``slides`` directive. For example::
- .. slides::
+ .. slides::
:preload:
:play: 350
-
+
/galleries/demo/tesla_conducts_lg.jpg
/galleries/demo/tesla_lightning2_lg.jpg
/galleries/demo/tesla4_lg.jpg
/galleries/demo/tesla_lightning1_lg.jpg
/galleries/demo/tesla_tower1_lg.jpg
-This is based on `slidejs <http://slidesjs.com/>`_ and it supports
+This is based on `slidejs <http://slidesjs.com/>`_ and it supports
`the options described there <http://slidesjs.com/#options>`_ with one minor tweak to make them
fit in docutils convention: If the option takes a boolean value, you just have to add it or not. For example,
to enable preloading, just use the ``:preload:`` option.
-If the option takes any other kind of argument, just use it after the option, like ``play`` in the
+If the option takes any other kind of argument, just use it after the option, like ``play`` in the
above example.
Importing Your Wordpress Site Into Nikola
@@ -930,14 +962,14 @@ the following:
* Will give you a url_map so you know where each old post was
This is also useful for Disqus thread migration!
-
+
* Will try to convert the content of your posts. This is *not* error free, because
wordpress uses some unholy mix of HTML and strange things. Currently we are treating it
as markdown, which does a reasonabe job of it.
-
- You will find your old posts in ``new_site/posts/post-title.wp`` in case you need to fix
+
+ You will find your old posts in ``new_site/posts/post-title.wp`` in case you need to fix
any of them.
-
+
This feature is a work in progress, and the only way to improve it is to have it used for
as many sites as possible and make it work better each time, so I am happy to get requests
about it.
@@ -962,6 +994,34 @@ With this command Nikola will import into the folder ``import_location``.
If the folder already exists Nikola will not overwrite an existing ``conf.py``.
Instead a new file with a timestamp at the end of the filename will be created.
+Using Twitter Cards
+-------------------
+
+Twitter Cards enable you to show additional information in Tweets that link
+to you content.
+Nikola supports `Twitter Cards <https://dev.twitter.com/docs/cards>`_.
+They are implemented to use *Open Graph* tags whenever possible.
+
+.. admonition:: Important
+
+ To use Twitter Cards you need to opt-in on Twitter.
+ To do so please use the form that can be found at https://dev.twitter.com/form/participate-twitter-cards
+
+To enable and configure your use of Twitter Cards please modify the
+corresponding lines in your ``conf.py``.
+An example configuration that uses the Twitter nickname of the website
+and the authors Twitter user ID is found below.
+
+.. code-block:: Python
+
+ TWITTER_CARD = {
+ 'use_twitter_cards': True, # enable Twitter Cards / Open Graph
+ 'site': '@website', # twitter nick for the website
+ # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead.
+ # 'creator': '@username', # Username for the content creator / author.
+ 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
+ }
+
License
-------
diff --git a/docs/theming.txt b/docs/theming.txt
index 33884e9..6c0c0e4 100644
--- a/docs/theming.txt
+++ b/docs/theming.txt
@@ -76,8 +76,8 @@ 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>`_
-or `Jinja2 <jinja.pocoo.org>`_ template languages. If you want to do a theme, you
+theme's page templates. They are done using the `Mako <http://makotemplates.org>`_
+or `Jinja2 <http://jinja.pocoo.org>`_ template languages. If you want to do a theme, you
should learn one first. What engine is used by the theme is declared in the ``engine`` file.
The rest of this document explains Mako templates, but Jinja2 is fairly similar.
diff --git a/extra_plugins/command_planetoid/__init__.py b/extra_plugins/command_planetoid/__init__.py
index 7ccb19e..2428f10 100644
--- a/extra_plugins/command_planetoid/__init__.py
+++ b/extra_plugins/command_planetoid/__init__.py
@@ -23,7 +23,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
import codecs
import datetime
import hashlib
@@ -86,7 +86,7 @@ class Planetoid(Command, Task):
line = line.strip()
if line.startswith("#"):
continue
- elif line.startswith(u'http'):
+ elif line.startswith('http'):
feed = line
elif line:
name = line
@@ -119,7 +119,7 @@ class Planetoid(Command, Task):
elif list(f)[0].url != feed:
yield {
'basename': self.name,
- 'name': (u'updating_' + name).encode('utf8'),
+ 'name': ('updating_' + name).encode('utf8'),
'actions': ((update_feed_url, (list(f)[0], feed)), ),
}
@@ -224,18 +224,18 @@ class Planetoid(Command, Task):
meta_path = os.path.join('posts', unique_id + '.meta')
post_path = os.path.join('posts', unique_id + '.txt')
with codecs.open(meta_path, 'wb+', 'utf8') as fd:
- fd.write(u'%s\n' % entry.title.replace('\n', ' '))
- fd.write(u'%s\n' % unique_id)
- fd.write(u'%s\n' % entry.date.strftime('%Y/%m/%d %H:%M'))
- fd.write(u'\n')
- fd.write(u'%s\n' % entry.link)
+ fd.write('%s\n' % entry.title.replace('\n', ' '))
+ fd.write('%s\n' % unique_id)
+ fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M'))
+ fd.write('\n')
+ fd.write('%s\n' % entry.link)
with codecs.open(post_path, 'wb+', 'utf8') as fd:
- fd.write(u'.. raw:: html\n\n')
+ fd.write('.. raw:: html\n\n')
content = entry.content
if not content:
- content = u'Sin contenido'
+ content = 'Sin contenido'
for line in content.splitlines():
- fd.write(u' %s\n' % line)
+ fd.write(' %s\n' % line)
for entry in Entry.select().order_by(Entry.date.desc()):
entry_id = gen_id(entry)
diff --git a/extra_plugins/task_mustache/__init__.py b/extra_plugins/task_mustache/__init__.py
index 0d0e87d..8b5ec13 100644
--- a/extra_plugins/task_mustache/__init__.py
+++ b/extra_plugins/task_mustache/__init__.py
@@ -73,7 +73,8 @@ class Mustache(Task):
# Configuration
for k, v in self.site.config.items():
- if isinstance(v, (str, unicode)):
+ # FIXME: not py3 ready
+ if isinstance(v, (str, unicode)): # NOQA
data[k] = v
# Tag data
diff --git a/nikola/conf.py.in b/nikola/conf.py.in
index 40e1996..7d75295 100644
--- a/nikola/conf.py.in
+++ b/nikola/conf.py.in
@@ -2,7 +2,6 @@
<%text>
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-import os
import time
##############################################
@@ -13,7 +12,12 @@ import time
# Data about this site
BLOG_AUTHOR = "${BLOG_AUTHOR}"
BLOG_TITLE = "${BLOG_TITLE}"
-BLOG_URL = "${BLOG_URL}"
+# This is the main URL for your site. It will be used
+# in a prominent link
+SITE_URL = "${SITE_URL}"
+# This is the URL where nikola's output will be deployed.
+# If not set, defaults to SITE_URL
+# BASE_URL = "${SITE_URL}
BLOG_EMAIL = "${BLOG_EMAIL}"
BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}"
@@ -46,7 +50,7 @@ TRANSLATIONS = {
"${DEFAULT_LANG}": "",
# Example for another language:
# "es": "./es",
- }
+}
# Links for the sidebar / navigation bar.
# You should provide a key-value pair for each used language.
@@ -101,6 +105,10 @@ post_pages = ${POST_PAGES}
# 'html' assumes the file is html and just copies it
post_compilers = ${POST_COMPILERS}
+# Create by default posts in one file format?
+# Set to False for two-file posts, with separate metadata.
+# ONE_FILE_POSTS = True
+
# Paths for different autogenerated bits. These are combined with the
# translation paths.
@@ -176,6 +184,11 @@ post_compilers = ${POST_COMPILERS}
# ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
# }
+# Create a gzipped copy of each generated file. Cheap server-side optimization.
+# GZIP_FILES = False
+# File extensions that will be compressed
+# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json')
+
# #############################################################################
# Image Gallery Options
# #############################################################################
@@ -198,6 +211,12 @@ post_compilers = ${POST_COMPILERS}
# Name of the theme to use. Themes are located in themes/theme_name
# THEME = 'site'
+# If you use 'site-reveal' theme you can select several subthemes
+# THEME_REVEAL_CONGIF_SUBTHEME = 'sky' # You can also use: beige/serif/simple/night/default
+
+# Again, if you use 'site-reveal' theme you can select several transitions between the slides
+# THEME_REVEAL_CONGIF_TRANSITION = 'cube' # You can also use: page/concave/linear/none/default
+
# date format used to display post dates. (str used by datetime.datetime.strftime)
# DATE_FORMAT = '%Y-%m-%d %H:%M'
@@ -242,6 +261,25 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# Enable comments on picture gallery pages?
# COMMENTS_IN_GALLERIES = False
+# Do you want a add a Mathjax config file?
+# MATHJAX_CONFIG = ""
+
+# If you are using the compile-ipynb plugin, just add this one:
+#MATHJAX_CONFIG = """
+#<script type="text/x-mathjax-config">
+#MathJax.Hub.Config({
+# tex2jax: {
+# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ],
+# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ]
+# },
+# displayAlign: 'left', // Change this to 'center' to center equations.
+# "HTML-CSS": {
+# styles: {'.MathJax_Display': {"margin": 0}}
+# }
+#});
+#</script>
+#"""
+
# Enable Addthis social buttons?
# Defaults to true
# ADD_THIS_BUTTONS = True
@@ -309,6 +347,39 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md'
# FILE_METADATA_REGEXP = None
+# Nikola supports Twitter Card summaries / Open Graph.
+# Twitter cards make it possible for you to attach media to Tweets
+# that link to your content.
+#
+# IMPORTANT:
+# Please note, that you need to opt-in for using Twitter Cards!
+# To do this please visit https://dev.twitter.com/form/participate-twitter-cards
+#
+# Uncomment and modify to following lines to match your accounts.
+# Specifying the id for either 'site' or 'creator' will be preferred
+# over the cleartext username. Specifying an ID is not necessary.
+# Displaying images is currently not supported.
+# TWITTER_CARD = {
+# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph
+# # 'site': '@website', # twitter nick for the website
+# # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead.
+# # 'creator': '@username', # Username for the content creator / author.
+# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
+# }
+
+
+# If you want to use formatted post time in W3C-DTF Format(ex. 2012-03-30T23:00:00+02:00),
+# set timzone if you want a localized posted date.
+#
+# TIMEZONE = 'Europe/Zurich'
+
+# If webassets is installed, bundle JS and CSS to make site loading faster
+# USE_BUNDLES = True
+
+# Plugins you don't want to use. Be careful :-)
+# DISABLED_PLUGINS = ["render_galleries"]
+
# Put in global_context things you want available on all your templates.
# It can be anything, data, functions, modules, etc.
+
GLOBAL_CONTEXT = {}
diff --git a/nikola/console.py b/nikola/console.py
deleted file mode 100644
index fae0ecd..0000000
--- a/nikola/console.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from __future__ import print_function, unicode_literals
-
-from nikola import Nikola
-try:
- import conf
- SITE = Nikola(**conf.__dict__)
- SITE.scan_posts()
- print("You can now access your configuration as conf and your site engine "
- "as SITE")
-except ImportError:
- print("No configuration found.")
diff --git a/nikola/data/samplesite/files/images/biohazard.png b/nikola/data/samplesite/files/images/biohazard.png
new file mode 100644
index 0000000..ae4629d
--- /dev/null
+++ b/nikola/data/samplesite/files/images/biohazard.png
Binary files differ
diff --git a/nikola/data/samplesite/listings/hello.py b/nikola/data/samplesite/listings/hello.py
index 695c212..885acde 100644
--- a/nikola/data/samplesite/listings/hello.py
+++ b/nikola/data/samplesite/listings/hello.py
@@ -4,7 +4,8 @@ import sys
def hello(name='world'):
- print "hello", name
+ greeting = "hello " + name
+ print(greeting)
if __name__ == "__main__":
hello(*sys.argv[1:])
diff --git a/nikola/data/samplesite/stories/creating-a-theme.meta b/nikola/data/samplesite/stories/creating-a-theme.meta
new file mode 100644
index 0000000..fe9b69a
--- /dev/null
+++ b/nikola/data/samplesite/stories/creating-a-theme.meta
@@ -0,0 +1,3 @@
+Creating a Theme
+creating-a-theme
+2012/03/13 12:00
diff --git a/nikola/data/samplesite/stories/creating-a-theme.txt b/nikola/data/samplesite/stories/creating-a-theme.txt
new file mode 120000
index 0000000..108a192
--- /dev/null
+++ b/nikola/data/samplesite/stories/creating-a-theme.txt
@@ -0,0 +1 @@
+../../../../docs/creating-a-theme.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/stories/quickref.txt b/nikola/data/samplesite/stories/quickref.txt
index 13ebc9b..9a06d74 100644
--- a/nikola/data/samplesite/stories/quickref.txt
+++ b/nikola/data/samplesite/stories/quickref.txt
@@ -55,7 +55,7 @@
<p>Links that look like "(<a href="#details">details</a>)" point
into the HTML version of the full <a
- href="../../ref/rst/restructuredtext.html">reStructuredText
+ href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html">reStructuredText
specification</a> document. These are relative links; if they
don't work, please use the <a
href="http://docutils.sourceforge.net/docs/user/rst/quickref.html"
@@ -65,7 +65,7 @@
<h2><a href="#contents" name="inline-markup" class="backref"
>Inline Markup</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#inline-markup">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup">details</a>)
<p>Inline markup allows words and phrases within text to have
character styles (like italics and boldface) and functionality
@@ -198,7 +198,7 @@
>Escaping with Backslashes</a></h2>
<p>(<a
- href="../../ref/rst/restructuredtext.html#escaping-mechanism">details</a>)
+ href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#escaping-mechanism">details</a>)
<p>reStructuredText uses backslashes ("\") to override the special
meaning given to markup characters and get the literal characters
@@ -246,7 +246,7 @@
<h2><a href="#contents" name="section-structure" class="backref"
>Section Structure</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#sections">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#sections">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -292,7 +292,7 @@
<h2><a href="#contents" name="paragraphs" class="backref"
>Paragraphs</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#paragraphs">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#paragraphs">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -320,7 +320,7 @@
<h2><a href="#contents" name="bullet-lists" class="backref"
>Bullet Lists</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#bullet-lists">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -358,7 +358,7 @@
<h2><a href="#contents" name="enumerated-lists" class="backref"
>Enumerated Lists</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#enumerated-lists">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#enumerated-lists">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -396,7 +396,7 @@
<h2><a href="#contents" name="definition-lists" class="backref"
>Definition Lists</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#definition-lists">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#definition-lists">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -437,7 +437,7 @@
<h2><a href="#contents" name="field-lists" class="backref"
>Field Lists</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#field-lists">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#field-lists">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -476,7 +476,7 @@
<h2><a href="#contents" name="option-lists" class="backref"
>Option Lists</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#option-lists">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#option-lists">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -524,7 +524,7 @@
<h2><a href="#contents" name="literal-blocks" class="backref"
>Literal Blocks</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#literal-blocks">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#literal-blocks">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -613,7 +613,7 @@
<h2><a href="#contents" name="line-blocks" class="backref"
>Line Blocks</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#line-blocks">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#line-blocks">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -655,7 +655,7 @@
<h2><a href="#contents" name="block-quotes" class="backref"
>Block Quotes</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#block-quotes">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#block-quotes">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -687,7 +687,7 @@
<h2><a href="#contents" name="doctest-blocks" class="backref"
>Doctest Blocks</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#doctest-blocks">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#doctest-blocks">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -723,7 +723,7 @@
<h2><a href="#contents" name="tables" class="backref"
>Tables</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#tables">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#tables">details</a>)
<p>There are two syntaxes for tables in reStructuredText. Grid
tables are complete but cumbersome to create. Simple tables are
@@ -840,7 +840,7 @@
<h2><a href="#contents" name="transitions" class="backref"
>Transitions</a></h2>
- <p>(<a href="../../ref/rst/restructuredtext.html#transitions">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#transitions">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -890,7 +890,7 @@
<h3><a href="#contents" name="footnotes" class="backref"
>Footnotes</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#footnotes">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#footnotes">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -983,7 +983,7 @@
<h3><a href="#contents" name="citations" class="backref"
>Citations</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#citations">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#citations">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -1036,7 +1036,7 @@
<h3><a href="#contents" name="hyperlink-targets" class="backref"
>Hyperlink Targets</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#hyperlink-targets">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#hyperlink-targets">details</a>)
<h4><a href="#contents" name="external-hyperlink-targets" class="backref"
>External Hyperlink Targets</a></h4>
@@ -1081,11 +1081,11 @@
printed documents, where the link needs to be presented explicitly, for
example as a footnote. You can force usage of the call-out form by
using the
- "<a href="../../ref/rst/directives.html#target-notes">target-notes</a>"
+ "<a href="http://docutils.sourceforge.net/docs/ref/rst/directives.html#target-notes">target-notes</a>"
directive.
<p>reStructuredText also provides for <b>embedded URIs</b> (<a
- href="../../ref/rst/restructuredtext.html#embedded-uris">details</a>),
+ href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#embedded-uris">details</a>),
a convenience at the expense of readability. A hyperlink
reference may directly embed a target URI inline, within angle
brackets. The following is exactly equivalent to the example above:
@@ -1149,7 +1149,7 @@
<h4><a href="#contents" name="indirect-hyperlink-targets" class="backref"
>Indirect Hyperlink Targets</a></h4>
- <p>(<a href="../../ref/rst/restructuredtext.html#indirect-hyperlink-targets">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#indirect-hyperlink-targets">details</a>)
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
<thead>
@@ -1189,7 +1189,7 @@
<h4><a href="#contents" name="implicit-hyperlink-targets" class="backref"
>Implicit Hyperlink Targets</a></h4>
- <p>(<a href="../../ref/rst/restructuredtext.html#implicit-hyperlink-targets">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#implicit-hyperlink-targets">details</a>)
<p>Section titles, footnotes, and citations automatically generate
hyperlink targets (the title text or footnote/citation label is
@@ -1217,12 +1217,12 @@
<h3><a href="#contents" name="directives" class="backref"
>Directives</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#directives">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#directives">details</a>)
<p>Directives are a general-purpose extension mechanism, a way of
adding support for new constructs without adding new syntax. For
a description of all standard directives, see <a
- href="../../ref/rst/directives.html" >reStructuredText
+ href="http://docutils.sourceforge.net/docs/ref/rst/directives.html" >reStructuredText
Directives</a>.
<p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3">
@@ -1235,17 +1235,17 @@
<tr valign="top">
<td><samp>For&nbsp;instance:</samp>
- <p><samp>..&nbsp;image::&nbsp;images/ball1.gif</samp>
+ <p><samp>..&nbsp;image::&nbsp;images/biohazard.png</samp>
<td>
For instance:
- <p><img src="images/ball1.gif" alt="ball1">
+ <p><img src="/images/biohazard.png" alt="ball1">
</table>
<h3><a href="#contents" name="substitution-references-and-definitions"
class="backref" >Substitution References and Definitions</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#substitution-definitions">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#substitution-definitions">details</a>)
<p>Substitutions are like inline directives, allowing graphics and
arbitrary constructs within text.
@@ -1268,7 +1268,7 @@
<td>
- <p>The <img src="images/biohazard.png" align="bottom" alt="biohazard"> symbol
+ <p>The <img src="/images/biohazard.png" align="bottom" alt="biohazard"> symbol
must be used on containers used to dispose of medical waste.
</table>
@@ -1276,7 +1276,7 @@
<h3><a href="#contents" name="comments" class="backref"
>Comments</a></h3>
- <p>(<a href="../../ref/rst/restructuredtext.html#comments">details</a>)
+ <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#comments">details</a>)
<p>Any text which begins with an explicit markup start but doesn't
use the syntax of any of the constructs above, is a comment.
diff --git a/nikola/data/samplesite/stories/quickstart.txt b/nikola/data/samplesite/stories/quickstart.txt
index a74d8fa..1a0f330 100644
--- a/nikola/data/samplesite/stories/quickstart.txt
+++ b/nikola/data/samplesite/stories/quickstart.txt
@@ -96,7 +96,7 @@ __ quickref.html#escaping
middle of a word won't be recognized. See the `markup spec`__ for
full details.
-__ ../../ref/rst/restructuredtext.html#inline-markup
+__ http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
Lists
@@ -362,18 +362,18 @@ __ quickref.html#directives
To include an image in your document, you use the the ``image`` directive__.
For example::
- .. image:: images/biohazard.png
+ .. image:: /images/biohazard.png
results in:
-.. image:: images/biohazard.png
+.. image:: /images/biohazard.png
-The ``images/biohazard.png`` part indicates the filename of the image
+The ``/images/biohazard.png`` part indicates the filename of the image
you wish to appear in the document. There's no restriction placed on
the image (format, size etc). If the image is to appear in HTML and
you wish to supply additional information, you may::
- .. image:: images/biohazard.png
+ .. image:: /images/biohazard.png
:height: 100
:width: 200
:scale: 50
@@ -381,8 +381,8 @@ you wish to supply additional information, you may::
See the full `image directive documentation`__ for more info.
-__ ../../ref/rst/directives.html
-__ ../../ref/rst/directives.html#images
+__ http://docutils.sourceforge.net/docs/ref/rst/directives.html
+__ http://docutils.sourceforge.net/docs/ref/rst/directives.html#images
What Next?
@@ -401,6 +401,6 @@ list.
http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html.
.. _reStructuredText Markup Specification:
- ../../ref/rst/restructuredtext.html
-.. _Docutils-users: ../mailing-lists.html#docutils-users
+ http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html
+.. _Docutils-users: http://docutils.sourceforge.net/docs/user/mailing-lists.html#docutils-users
.. _Docutils project web site: http://docutils.sourceforge.net/
diff --git a/nikola/data/themes/default/assets/css/theme.css b/nikola/data/themes/default/assets/css/theme.css
index 6f3d4cb..0523ce9 100644
--- a/nikola/data/themes/default/assets/css/theme.css
+++ b/nikola/data/themes/default/assets/css/theme.css
@@ -49,9 +49,14 @@ div.figure > a > img {
margin-right: auto;
}
-div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
+div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
/* Issue 277 */
border: 1px solid #aaa;
border-radius: 5px;
}
+blockquote p, blockquote {
+ font-size: 17.5px;
+ font-weight: 300;
+ line-height: 1.25;
+}
diff --git a/nikola/data/themes/default/assets/js/mathjax.js b/nikola/data/themes/default/assets/js/mathjax.js
new file mode 100644
index 0000000..2f4e773
--- /dev/null
+++ b/nikola/data/themes/default/assets/js/mathjax.js
@@ -0,0 +1,12 @@
+// We wait for the onload function to load MathJax after the page is completely loaded.
+// MathJax is loaded 1 unit of time after the page is ready.
+// This hack prevent problems when you use social button from addthis.
+//
+window.onload = function () {
+ setTimeout(function () {
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+ script.src = "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML";
+ document.getElementsByTagName("body")[0].appendChild(script);
+ },1)
+} \ No newline at end of file
diff --git a/nikola/data/themes/default/bundles b/nikola/data/themes/default/bundles
index 10f44e7..35af9c0 100644
--- a/nikola/data/themes/default/bundles
+++ b/nikola/data/themes/default/bundles
@@ -1,4 +1,4 @@
assets/css/all-nocdn.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,slides.css,theme.css,custom.css
assets/css/all.css=rst.css,code.css,colorbox.css,slides.css,theme.css,custom.css
-assets/js/all-nocdn.js=bootstrap.min.js,jquery-1.7.2.min.js,jquery.colorbox-min.js,slides.min.jquery.js
+assets/js/all-nocdn.js=jquery-1.7.2.min.js,bootstrap.min.js,jquery.colorbox-min.js,slides.min.jquery.js
assets/js/all.js=jquery.colorbox-min.js,slides.min.jquery.js
diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl
index 7af8497..c0935a2 100644
--- a/nikola/data/themes/default/templates/base.tmpl
+++ b/nikola/data/themes/default/templates/base.tmpl
@@ -48,6 +48,10 @@
</ul>
<!--End of sidebar content-->
</div>
+ </div>
+ </div>
+ </div>
+</div>
${analytics}
${late_load_js()}
<script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
diff --git a/nikola/data/themes/default/templates/base_helper.tmpl b/nikola/data/themes/default/templates/base_helper.tmpl
index 51969c9..eb22905 100644
--- a/nikola/data/themes/default/templates/base_helper.tmpl
+++ b/nikola/data/themes/default/templates/base_helper.tmpl
@@ -4,7 +4,7 @@
<meta name="description" content="${description}" >
<meta name="author" content="${blog_author}">
<title>${title} | ${blog_title}</title>
- <!-- Le styles -->
+ ${mathjax_config}
%if use_bundles:
%if use_cdn:
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
@@ -28,7 +28,6 @@
<link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
%endif
%endif
- <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
<![endif]-->
@@ -36,7 +35,7 @@
${rss_link}
%else:
%for language in translations:
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, lang)}">
+ <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
%endfor
%endif
%if favicons:
@@ -73,10 +72,10 @@
<!-- Social buttons -->
<div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
<a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a></li>
- <li><a class="addthis_button_google_plusone_share"></a></li>
- <li><a class="addthis_button_linkedin"></a></li>
- <li><a class="addthis_button_twitter"></a></li>
+ <ul><li><a class="addthis_button_facebook"></a>
+ <li><a class="addthis_button_google_plusone_share"></a>
+ <li><a class="addthis_button_linkedin"></a>
+ <li><a class="addthis_button_twitter"></a>
</ul>
</div>
<script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
diff --git a/nikola/data/themes/default/templates/gallery.tmpl b/nikola/data/themes/default/templates/gallery.tmpl
index 3186cc8..09c25cc 100644
--- a/nikola/data/themes/default/templates/gallery.tmpl
+++ b/nikola/data/themes/default/templates/gallery.tmpl
@@ -6,7 +6,7 @@
<%block name="content">
<ul class="breadcrumb">
% for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
+ <li><a href="${link}">/ ${crumb}</a>
% endfor
</ul>
%if text:
@@ -16,13 +16,13 @@
%endif
<ul>
% for folder in folders:
- <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
+ <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a>
% endfor
</ul>
<ul class="thumbnails">
%for image in images:
<li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}>
- <img src="${image[1]}" /></a></li>
+ <img src="${image[1]}" /></a>
%endfor
</ul>
%if enable_comments:
diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl
index 1a436e2..4f66867 100644
--- a/nikola/data/themes/default/templates/index.tmpl
+++ b/nikola/data/themes/default/templates/index.tmpl
@@ -7,7 +7,7 @@
<div class="postbox">
<h1><a href="${post.permalink(lang)}">${post.title(lang)}</a>
<small>&nbsp;&nbsp;
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
+ ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time>
</small></h1>
<hr>
${post.text(lang, index_teasers)}
@@ -16,4 +16,5 @@
% endfor
${helper.html_pager()}
${disqus.html_disqus_script()}
+ ${helper.mathjax_script(post)}
</%block>
diff --git a/nikola/data/themes/default/templates/index_helper.tmpl b/nikola/data/themes/default/templates/index_helper.tmpl
index 114a730..151b4d2 100644
--- a/nikola/data/themes/default/templates/index_helper.tmpl
+++ b/nikola/data/themes/default/templates/index_helper.tmpl
@@ -5,13 +5,17 @@
%if prevlink:
<li class="previous">
<a href="${prevlink}">&larr; ${messages[lang]["Newer posts"]}</a>
- </li>
%endif
%if nextlink:
<li class="next">
<a href="${nextlink}">${messages[lang]["Older posts"]} &rarr;</a>
- </li>
%endif
</ul>
</div>
</%def>
+
+<%def name="mathjax_script(post)">
+ %if any(post.is_mathjax for post in posts):
+ <script src="/assets/js/mathjax.js" type="text/javascript"></script>
+ %endif
+</%def>
diff --git a/nikola/data/themes/default/templates/listing.tmpl b/nikola/data/themes/default/templates/listing.tmpl
index 596a704..f279af0 100644
--- a/nikola/data/themes/default/templates/listing.tmpl
+++ b/nikola/data/themes/default/templates/listing.tmpl
@@ -3,8 +3,18 @@
<%block name="content">
<ul class="breadcrumb">
% for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
+ <li><a href="${link}">/${crumb}</a>
% endfor
</ul>
-${code}
+<ul class="unstyled">
+% for name in folders:
+ <li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a>
+% endfor
+% for name in files:
+ <li><a href="${name}.html"><i class="icon-file"></i> ${name}</a>
+% endfor
+</ul>
+% if code:
+ ${code}
+% endif
</%block>
diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl
index 672d4f6..22d8a58 100644
--- a/nikola/data/themes/default/templates/post.tmpl
+++ b/nikola/data/themes/default/templates/post.tmpl
@@ -2,20 +2,28 @@
<%namespace name="helper" file="post_helper.tmpl"/>
<%namespace name="disqus" file="disqus_helper.tmpl"/>
<%inherit file="base.tmpl"/>
+<%block name="extra_head">
+${helper.twitter_card_information(post)}
+</%block>
<%block name="content">
<div class="postbox">
${helper.html_title()}
<hr>
<small>
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
+ ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time>
${helper.html_translations(post)}
- &nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.pagenames[lang]+'.txt'}" id="sourcelink">${messages[lang]["Source"]}</a>
${helper.html_tags(post)}
</small>
<hr>
${post.text(lang)}
${helper.html_pager(post)}
${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
+ ${helper.mathjax_script(post)}
</div>
</%block>
+
+<%block name="sourcelink">
+ <li>
+ <a href="${post.pagenames[lang]+post.source_ext()}" id="sourcelink">${messages[lang]["Source"]}</a>
+ </li>
+</%block>
diff --git a/nikola/data/themes/default/templates/post_helper.tmpl b/nikola/data/themes/default/templates/post_helper.tmpl
index ab08359..911a831 100644
--- a/nikola/data/themes/default/templates/post_helper.tmpl
+++ b/nikola/data/themes/default/templates/post_helper.tmpl
@@ -33,12 +33,39 @@
%if post.prev_post:
<li class="previous">
<a href="${post.prev_post.permalink(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
- </li>
%endif
%if post.next_post:
<li class="next">
<a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} &rarr;</a>
- </li>
%endif
</ul>
</%def>
+
+<%def name="twitter_card_information(post)">
+ %if twitter_card and twitter_card['use_twitter_cards']:
+ <meta name="twitter:card" content="${twitter_card.get('card', 'summary')}">
+ <meta name="og:url" content="${post.permalink(lang, absolute=True)}">
+ %if 'site:id' in twitter_card:
+ <meta name="twitter:site:id" content="${twitter_card['site:id']}">
+ %elif 'site' in twitter_card:
+ <meta name="twitter:site" content="${twitter_card['site']}">
+ %endif
+ %if 'creator:id' in twitter_card:
+ <meta name="twitter:creator:id" content="${twitter_card['creator:id']}">
+ %elif 'creator' in twitter_card:
+ <meta name="twitter:creator" content="${twitter_card['creator']}">
+ %endif
+ <meta name="og:title" content="${post.title(lang)[:70]}">
+ %if post.description(lang):
+ <meta name="og:description" content="${post.description(lang)[:200]}">
+ %else:
+ <meta name="og:description" content="${post.text(lang, strip_html=True)[:200]}">
+ %endif
+ %endif
+</%def>
+
+<%def name="mathjax_script(post)">
+ %if post.is_mathjax:
+ <script src="/assets/js/mathjax.js" type="text/javascript"></script>
+ %endif
+</%def>
diff --git a/nikola/data/themes/jinja-default/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl
index 32e5d48..97cddff 100644
--- a/nikola/data/themes/jinja-default/templates/base.tmpl
+++ b/nikola/data/themes/jinja-default/templates/base.tmpl
@@ -93,15 +93,19 @@
<!-- social buttons -->
<div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
<a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a></li>
- <li><a class="addthis_button_google_plusone_share"></a></li>
- <li><a class="addthis_button_linkedin"></a></li>
- <li><a class="addthis_button_twitter"></a></li>
+ <ul><li><a class="addthis_button_facebook"></a>
+ <li><a class="addthis_button_google_plusone_share"></a>
+ <li><a class="addthis_button_linkedin"></a>
+ <li><a class="addthis_button_twitter"></a>
</ul>
</div>
<script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
<!-- End of social buttons -->
{% endif %}
+ </div>
+ </div>
+ </div>
+</div>
{{analytics}}
<!-- late load javascript -->
{% if use_bundles %}
diff --git a/nikola/data/themes/jinja-default/templates/gallery.tmpl b/nikola/data/themes/jinja-default/templates/gallery.tmpl
index 34ff439..9b16df6 100644
--- a/nikola/data/themes/jinja-default/templates/gallery.tmpl
+++ b/nikola/data/themes/jinja-default/templates/gallery.tmpl
@@ -4,7 +4,7 @@
{% block content %}
<ul class="breadcrumb">
{% for link, crumb in crumbs %}
- <li><a href="{{link}}">/ {{crumb}}</a></li>
+ <li><a href="{{link}}">/ {{crumb}}</a>
{% endfor %}
</ul>
{% if text %}
@@ -14,13 +14,13 @@
{% endif %}
<ul>
{% for folder in folders %}
- <li><a href="{{folder}}"><i class="icon-folder-open"></i>&nbsp;{{folder}}</a></li>
+ <li><a href="{{folder}}"><i class="icon-folder-open"></i>&nbsp;{{folder}}</a>
{% 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>
+ <li><a href="{{image[0]}}" class="thumbnail image-reference" {{image[2]}}>
+ <img src="{{image[1]}}" /></a>
{% endfor %}
</ul>
{%if enable_comments %}
diff --git a/nikola/data/themes/jinja-default/templates/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl
index ad54c19..ab0392c 100644
--- a/nikola/data/themes/jinja-default/templates/index.tmpl
+++ b/nikola/data/themes/jinja-default/templates/index.tmpl
@@ -19,12 +19,10 @@
{%if prevlink %}
<li class="previous">
<a href="{{prevlink}}">&larr; {{messages[lang]["Newer posts"]}}</a>
- </li>
{% endif %}
{% if nextlink %}
<li class="next">
- <a href="{{nextlink}}">${messages[lang]["Older posts"]} &rarr;</a>
- </li>
+ <a href="{{nextlink}}">{{messages[lang]["Older posts"]}} &rarr;</a>
{% endif %}
</ul>
diff --git a/nikola/data/themes/jinja-default/templates/listing.tmpl b/nikola/data/themes/jinja-default/templates/listing.tmpl
index 8310635..493624a 100644
--- a/nikola/data/themes/jinja-default/templates/listing.tmpl
+++ b/nikola/data/themes/jinja-default/templates/listing.tmpl
@@ -2,7 +2,7 @@
{% block content %}
<ul class="breadcrumb">
{% for link, crumb in crumbs %}
- <li><a href="{{link}}">/ {{crumb}}</a></li>
+ <li><a href="{{link}}">/ {{crumb}}</a>
{% endfor %}
</ul>
{{code}}
diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl
index 2a356c5..d14e973 100644
--- a/nikola/data/themes/jinja-default/templates/post.tmpl
+++ b/nikola/data/themes/jinja-default/templates/post.tmpl
@@ -32,12 +32,10 @@
{%if post.prev_post %}
<li class="previous">
<a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">&larr; {{messages[lang]["Previous post"]}}</a>
- </li>
{% endif %}
{%if post.next_post %}
<li class="next">
<a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post"]}} &rarr;</a>
- </li>
{% endif %}
</ul>
{% if disqus_forum %}
diff --git a/nikola/data/themes/site/README b/nikola/data/themes/site/README
index 6cbc7dd..c89543c 100644
--- a/nikola/data/themes/site/README
+++ b/nikola/data/themes/site/README
@@ -29,4 +29,4 @@ default duckduckgo search form:
<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" />
</form>
<!-- End of custom search -->
- """ % BLOG_URL
+ """ % SITE_URL
diff --git a/nikola/data/themes/site/assets/css/theme.css b/nikola/data/themes/site/assets/css/theme.css
index 0183c69..aa0ee4a 100644
--- a/nikola/data/themes/site/assets/css/theme.css
+++ b/nikola/data/themes/site/assets/css/theme.css
@@ -52,8 +52,15 @@ div.figure > a > img {
margin-right: auto;
}
-div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
+div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
/* Issue 277 */
border: 1px solid #aaa;
border-radius: 5px;
}
+
+blockquote p, blockquote {
+ font-size: 17.5px;
+ font-weight: 300;
+ line-height: 1.25;
+}
+
diff --git a/nikola/data/themes/site/templates/post.tmpl b/nikola/data/themes/site/templates/post.tmpl
deleted file mode 100644
index 785385f..0000000
--- a/nikola/data/themes/site/templates/post.tmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <div class="postbox">
- ${helper.html_title()}
- <hr>
- <small>
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
- ${helper.html_translations(post)}
- ${helper.html_tags(post)}
- </small>
- <hr>
- ${post.text(lang)}
- ${helper.html_pager(post)}
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
- </div>
-</%block>
-
-<%block name="sourcelink">
- <li>
- <a href="${post.pagenames[lang]+post.source_ext()}" id="sourcelink">${messages[lang]["Source"]}</a>
- </li>
-</%block>
diff --git a/nikola/filters.py b/nikola/filters.py
index 4a63cb4..a3bff81 100644
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -80,9 +80,10 @@ def tidy(inplace):
# Tidy will give error exits, that we will ignore.
output = subprocess.check_output("tidy -m -w 90 --indent no --quote-marks"
- "no --keep-time yes --tidy-mark no '%s';"
- "exit 0" % inplace,
- stderr=subprocess.STDOUT, shell=True)
+ "no --keep-time yes --tidy-mark no "
+ "--force-output yes '{0}'; exit 0".format(
+ inplace), stderr=subprocess.STDOUT,
+ shell=True)
for line in output.split("\n"):
if "Warning:" in line:
@@ -101,7 +102,15 @@ def tidy(inplace):
elif '<table> lacks "summary" attribute' in line:
# Happens for tables, TODO: Check this is normal.
continue
+ elif 'proprietary attribute "data-toggle"' in line or \
+ 'proprietary attribute "data-target"':
+ # Some of our own tricks
+ continue
else:
assert False, (inplace, line)
elif "Error:" in line:
- assert False, line
+ if '<time> is not recognized' in line:
+ # False alarm, time is proper HTML5.
+ continue
+ else:
+ assert False, line
diff --git a/nikola/main.py b/nikola/main.py
new file mode 100644
index 0000000..b390387
--- /dev/null
+++ b/nikola/main.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function, unicode_literals
+import sys
+from operator import attrgetter
+
+from doit.loader import generate_tasks
+from doit.cmd_base import TaskLoader
+from doit.reporter import ExecutedOnlyReporter
+from doit.doit_cmd import DoitMain
+from doit.cmd_help import Help as DoitHelp
+from doit.cmd_run import Run as DoitRun
+
+from .nikola import Nikola
+
+
+def main(args):
+ sys.path.append('')
+ try:
+ import conf
+ if sys.version_info[0] > 2:
+ from imp import reload as _reload
+ else:
+ _reload = reload # NOQA
+ _reload(conf)
+ config = conf.__dict__
+ except ImportError:
+ config = {}
+
+ site = Nikola(**config)
+ return DoitNikola(site).run(args)
+
+
+class Help(DoitHelp):
+ """show Nikola usage instead of doit """
+
+ @staticmethod
+ def print_usage(cmds):
+ """print nikola "usage" (basic help) instructions"""
+ print("Nikola")
+ print("Available commands:")
+ for cmd in sorted(cmds.values(), key=attrgetter('name')):
+ print(" nikola %s \t\t %s" % (cmd.name, cmd.doc_purpose))
+ print("")
+ print(" nikola help show help / reference")
+ print(" nikola help <command> show command usage")
+ print(" nikola help <task-name> show task usage")
+
+
+class Build(DoitRun):
+ """expose "run" command as "build" for backward compatibility"""
+ pass
+
+
+class NikolaTaskLoader(TaskLoader):
+ """custom task loader to get tasks from Nikola instead of dodo.py file"""
+ def __init__(self, nikola):
+ self.nikola = nikola
+
+ def load_tasks(self, cmd, opt_values, pos_args):
+ DOIT_CONFIG = {
+ 'reporter': ExecutedOnlyReporter,
+ 'default_tasks': ['render_site'],
+ }
+ tasks = generate_tasks('render_site', self.nikola.gen_tasks())
+ return tasks, DOIT_CONFIG
+
+
+class DoitNikola(DoitMain):
+ # overwite help command
+ DOIT_CMDS = list(DoitMain.DOIT_CMDS) + [Help, Build]
+ TASK_LOADER = NikolaTaskLoader
+
+ def __init__(self, nikola):
+ self.nikola = nikola
+ self.task_loader = self.TASK_LOADER(nikola)
+
+ def get_commands(self):
+ # core doit commands
+ cmds = DoitMain.get_commands(self)
+
+ # load nikola commands
+ for name, cmd in self.nikola.commands.items():
+ cmds[name] = cmd
+ return cmds
+
+ def run(self, cmd_args):
+ sub_cmds = self.get_commands()
+ args = self.process_args(cmd_args)
+
+ if len(args) == 0 or args == ["--help"]:
+ cmd_args = ['help']
+ args = ['help']
+
+ if len(args) == 0 or args[0] not in sub_cmds.keys() or \
+ args[0] in ('run', 'build'):
+ # Check for conf.py before launching run
+ if not self.nikola.configured:
+ print("This command needs to run inside an "
+ "existing Nikola site.")
+ return False
+ super(DoitNikola, self).run(cmd_args)
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 88de88c..a1506e7 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -27,6 +27,7 @@ from __future__ import print_function, unicode_literals
from collections import defaultdict
from copy import copy
import glob
+import gzip
import os
import sys
try:
@@ -36,6 +37,7 @@ except ImportError:
import lxml.html
from yapsy.PluginManager import PluginManager
+import pytz
if os.getenv('DEBUG'):
import logging
@@ -75,6 +77,10 @@ class Nikola(object):
self.timeline = []
self.pages = []
self._scanned = False
+ if not config:
+ self.configured = False
+ else:
+ self.configured = True
# This is the default config
# TODO: fill it
@@ -90,12 +96,15 @@ class Nikola(object):
'DATE_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': [],
+ 'DISABLED_PLUGINS': (),
'DISQUS_FORUM': 'nikolademo',
'FAVICONS': {},
'FILE_METADATA_REGEXP': None,
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
'GALLERY_PATH': 'galleries',
+ 'GZIP_FILES': False,
+ 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json'),
'INDEX_DISPLAY_POST_COUNT': 10,
'INDEX_TEASERS': False,
'INDEXES_TITLE': "",
@@ -104,6 +113,7 @@ class Nikola(object):
'LICENSE': '',
'LISTINGS_FOLDER': 'listings',
'MAX_IMAGE_SIZE': 1280,
+ 'MATHJAX_CONFIG': '',
'OUTPUT_FOLDER': 'output',
'post_compilers': {
"rest": ('.txt', '.rst'),
@@ -129,10 +139,13 @@ class Nikola(object):
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
'THEME': 'site',
+ 'THEME_REVEAL_CONGIF_SUBTHEME': 'sky',
+ 'THEME_REVEAL_CONGIF_TRANSITION': 'cube',
'THUMBNAIL_SIZE': 180,
'USE_BUNDLES': True,
'USE_CDN': False,
'USE_FILENAME_AS_TITLE': True,
+ 'TIMEZONE': None,
}
self.config.update(config)
@@ -145,6 +158,18 @@ class Nikola(object):
self.MESSAGES = utils.load_messages(self.THEMES,
self.config['TRANSLATIONS'])
+ # SITE_URL is required, but if the deprecated BLOG_URL
+ # is available, use it and warn
+ if 'SITE_URL' not in self.config:
+ if 'BLOG_URL' in self.config:
+ print("WARNING: You should configure SITE_URL instead of BLOG_URL")
+ print("See docs at FIXME put URL")
+ self.config['SITE_URL'] = self.config['BLOG_URL']
+
+ # BASE_URL defaults to SITE_URL
+ if 'BASE_URL' not in self.config:
+ self.config['BASE_URL'] = self.config.get('SITE_URL')
+
self.plugin_manager = PluginManager(categories_filter={
"Command": Command,
"Task": Task,
@@ -157,24 +182,28 @@ class Nikola(object):
str(os.path.join(os.path.dirname(__file__), 'plugins')),
str(os.path.join(os.getcwd(), 'plugins')),
])
+
self.plugin_manager.collectPlugins()
self.commands = {}
# Activate all command plugins
for pluginInfo in self.plugin_manager.getPluginsOfCategory("Command"):
+ if pluginInfo.name in self.config['DISABLED_PLUGINS']:
+ self.plugin_manager.removePluginFromCategory(pluginInfo, "Command")
+ continue
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)
+ for task_type in ["Task", "LateTask"]:
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory(task_type):
+ if pluginInfo.name in self.config['DISABLED_PLUGINS']:
+ self.plugin_manager.removePluginFromCategory(pluginInfo, task_type)
+ continue
+ self.plugin_manager.activatePluginByName(pluginInfo.name)
+ pluginInfo.plugin_object.set_site(self)
# set global_context for template rendering
self.GLOBAL_CONTEXT = {
@@ -195,20 +224,29 @@ class Nikola(object):
self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
self.GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
self.GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
- self.GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT', '%Y-%m-%d %H:%M')
+ self.GLOBAL_CONTEXT['date_format'] = self.config.get(
+ 'DATE_FORMAT', '%Y-%m-%d %H:%M')
self.GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
self.GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE')
- self.GLOBAL_CONTEXT['blog_url'] = self.config.get('BLOG_URL')
+
+ self.GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL'))
self.GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
self.GLOBAL_CONTEXT['analytics'] = self.config.get('ANALYTICS')
self.GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS')
self.GLOBAL_CONTEXT['license'] = self.config.get('LICENSE')
self.GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM')
self.GLOBAL_CONTEXT['disqus_forum'] = self.config.get('DISQUS_FORUM')
- self.GLOBAL_CONTEXT['content_footer'] = self.config.get('CONTENT_FOOTER')
+ self.GLOBAL_CONTEXT['mathjax_config'] = self.config.get(
+ 'MATHJAX_CONFIG')
+ self.GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONGIF_SUBTHEME')
+ self.GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONGIF_TRANSITION')
+ self.GLOBAL_CONTEXT['content_footer'] = self.config.get(
+ 'CONTENT_FOOTER')
self.GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
self.GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
self.GLOBAL_CONTEXT['sidebar_links'] = self.config.get('SIDEBAR_LINKS')
+ self.GLOBAL_CONTEXT['twitter_card'] = self.config.get(
+ 'TWITTER_CARD', {})
self.GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
@@ -226,8 +264,8 @@ class Nikola(object):
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.stderr.write("Error loading {0} template system "
+ "plugin\n".format(template_sys_name))
sys.exit(1)
self.template_system = pi.plugin_object
lookup_dirs = [os.path.join(utils.get_theme_path(name), "templates")
@@ -265,12 +303,12 @@ class Nikola(object):
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))
+ "one of {0})".format(', '.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)
+ "handle '{0}' extensions.".format(ext))
lang = langs[0]
compile_html = self.compilers[lang]
@@ -292,10 +330,12 @@ class Nikola(object):
url_part = output_name.decode('utf8')[len(self.config["OUTPUT_FOLDER"])
+ 1:]
- # This is to support windows paths
- url_part = "/".join(url_part.split(os.sep))
-
- src = urljoin(self.config["BLOG_URL"], url_part)
+ # Treat our site as if output/ is "/" and then make all URLs relative,
+ # making the site "relocatable"
+ src = os.sep + url_part
+ src = os.path.normpath(src)
+ # The os.sep is because normpath will change "/" to "\" on windows
+ src = "/".join(src.split(os.sep))
parsed_src = urlsplit(src)
src_elems = parsed_src.path.split('/')[1:]
@@ -398,7 +438,7 @@ class Nikola(object):
if name not in [None, 0]:
path = [_f for _f in [self.config['TRANSLATIONS'][lang],
self.config['INDEX_PATH'],
- 'index-%s.html' % name] if _f]
+ 'index-{0}.html'.format(name)] if _f]
else:
path = [_f for _f in [self.config['TRANSLATIONS'][lang],
self.config['INDEX_PATH'], 'index.html']
@@ -434,13 +474,13 @@ class Nikola(object):
def abs_link(self, dst):
# Normalize
- dst = urljoin(self.config['BLOG_URL'], dst)
+ dst = urljoin(self.config['BASE_URL'], dst)
return urlparse(dst).path
def rel_link(self, src, dst):
# Normalize
- src = urljoin(self.config['BLOG_URL'], src)
+ src = urljoin(self.config['BASE_URL'], src)
dst = urljoin(src, dst)
# Avoid empty links.
if src == dst:
@@ -471,19 +511,67 @@ class Nikola(object):
return exists
def gen_tasks(self):
- task_dep = []
+
+ def create_gzipped_copy(in_path, out_path):
+ with gzip.GzipFile(out_path, 'wb+') as outf:
+ with open(in_path, 'rb') as inf:
+ outf.write(inf.read())
+
+ def flatten(task):
+ if isinstance(task, dict):
+ yield task
+ else:
+ for t in task:
+ for ft in flatten(t):
+ yield ft
+
+ def add_gzipped_copies(task):
+ if not self.config['GZIP_FILES']:
+ return None
+ gzip_task = {
+ 'file_dep': [],
+ 'targets': [],
+ 'actions': [],
+ 'basename': 'gzip',
+ 'name': task.get('name', 'unknown'),
+ 'clean': True,
+ }
+ targets = task.get('targets', [])
+ flag = False
+ for target in targets:
+ ext = os.path.splitext(target)[1]
+ if (ext.lower() in self.config['GZIP_EXTENSIONS'] and
+ target.startswith(self.config['OUTPUT_FOLDER'])):
+ flag = True
+ gzipped = target + '.gz'
+ gzip_task['file_dep'].append(target)
+ gzip_task['targets'].append(gzipped)
+ gzip_task['actions'].append((create_gzipped_copy, (target, gzipped)))
+ if not flag:
+ return None
+ return gzip_task
+
+ if self.config['GZIP_FILES']:
+ task_dep = ['gzip']
+ else:
+ task_dep = []
for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
- for task in pluginInfo.plugin_object.gen_tasks():
+ for task in flatten(pluginInfo.plugin_object.gen_tasks()):
+ gztask = add_gzipped_copies(task)
+ if gztask:
+ yield gztask
yield task
if pluginInfo.plugin_object.is_default:
task_dep.append(pluginInfo.plugin_object.name)
for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
for task in pluginInfo.plugin_object.gen_tasks():
+ gztask = add_gzipped_copies(task)
+ if gztask:
+ yield gztask
yield task
if pluginInfo.plugin_object.is_default:
task_dep.append(pluginInfo.plugin_object.name)
-
yield {
'name': b'all',
'actions': None,
@@ -493,60 +581,65 @@ class Nikola(object):
def scan_posts(self):
"""Scan all the posts."""
- if not self._scanned:
- print("Scanning posts", end='')
- targets = set([])
- for wildcard, destination, template_name, use_in_feeds in \
- self.config['post_pages']:
- print(".", end='')
- base_len = len(destination.split(os.sep))
- dirname = os.path.dirname(wildcard)
- for dirpath, _, _ in os.walk(dirname):
- dir_glob = os.path.join(dirpath,
- os.path.basename(wildcard))
- dest_dir = os.path.join(*([destination] +
- dirpath.split(
- os.sep)[base_len:]))
- for base_path in glob.glob(dir_glob):
- post = Post(
- base_path,
- self.config['CACHE_FOLDER'],
- dest_dir,
- use_in_feeds,
- self.config['TRANSLATIONS'],
- self.config['DEFAULT_LANG'],
- self.config['BLOG_URL'],
- self.MESSAGES,
- template_name,
- self.config['FILE_METADATA_REGEXP'])
- for lang, langpath in list(
- self.config['TRANSLATIONS'].items()):
- dest = (destination, langpath, dir_glob,
- post.pagenames[lang])
- if dest in targets:
- raise Exception(
- 'Duplicated output path %r in post %r' %
- (post.pagenames[lang], base_path))
- targets.add(dest)
- self.global_data[post.post_name] = post
- if post.use_in_feeds:
- self.posts_per_year[
- str(post.date.year)].append(post.post_name)
- for tag in post.tags:
- self.posts_per_tag[tag].append(post.post_name)
- else:
- self.pages.append(post)
- for name, post in list(self.global_data.items()):
- self.timeline.append(post)
- self.timeline.sort(key=lambda p: p.date)
- self.timeline.reverse()
- post_timeline = [p for p in self.timeline if p.use_in_feeds]
- for i, p in enumerate(post_timeline[1:]):
- p.next_post = post_timeline[i]
- for i, p in enumerate(post_timeline[:-1]):
- p.prev_post = post_timeline[i + 1]
- self._scanned = True
- print("done!")
+ if self._scanned:
+ return
+
+ print("Scanning posts", end='')
+ tzinfo = None
+ if self.config['TIMEZONE'] is not None:
+ tzinfo = pytz.timezone(self.config['TIMEZONE'])
+ targets = set([])
+ for wildcard, destination, template_name, use_in_feeds in \
+ self.config['post_pages']:
+ print(".", end='')
+ dirname = os.path.dirname(wildcard)
+ for dirpath, _, _ in os.walk(dirname):
+ dir_glob = os.path.join(dirpath, os.path.basename(wildcard))
+ dest_dir = os.path.normpath(os.path.join(destination,
+ os.path.relpath(dirpath, dirname)))
+ for base_path in glob.glob(dir_glob):
+ post = Post(
+ base_path,
+ self.config['CACHE_FOLDER'],
+ dest_dir,
+ use_in_feeds,
+ self.config['TRANSLATIONS'],
+ self.config['DEFAULT_LANG'],
+ self.config['BASE_URL'],
+ self.MESSAGES,
+ template_name,
+ self.config['FILE_METADATA_REGEXP'],
+ tzinfo,
+ )
+ for lang, langpath in list(
+ self.config['TRANSLATIONS'].items()):
+ dest = (destination, langpath, dir_glob,
+ post.pagenames[lang])
+ if dest in targets:
+ raise Exception('Duplicated output path {0!r} '
+ 'in post {1!r}'.format(
+ post.pagenames[lang],
+ base_path))
+ targets.add(dest)
+ self.global_data[post.post_name] = post
+ if post.use_in_feeds:
+ self.posts_per_year[
+ str(post.date.year)].append(post.post_name)
+ for tag in post.tags:
+ self.posts_per_tag[tag].append(post.post_name)
+ else:
+ self.pages.append(post)
+ for name, post in list(self.global_data.items()):
+ self.timeline.append(post)
+ self.timeline.sort(key=lambda p: p.date)
+ self.timeline.reverse()
+ post_timeline = [p for p in self.timeline if p.use_in_feeds]
+ for i, p in enumerate(post_timeline[1:]):
+ p.next_post = post_timeline[i]
+ for i, p in enumerate(post_timeline[:-1]):
+ p.prev_post = post_timeline[i + 1]
+ self._scanned = True
+ print("done!")
def generic_page_renderer(self, lang, post, filters):
"""Render post fragments to final HTML pages."""
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
index 8c69dec..cff9b65 100644
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -31,6 +31,7 @@ __all__ = [
]
from yapsy.IPlugin import IPlugin
+from doit.cmd_base import Command as DoitCommand
class BasePlugin(IPlugin):
@@ -41,16 +42,57 @@ class BasePlugin(IPlugin):
self.site = site
-class Command(BasePlugin):
- """These plugins are exposed via the command line."""
+class Command(BasePlugin, DoitCommand):
+ """These plugins are exposed via the command line.
+ They implement the doit Command interface."""
name = "dummy_command"
- short_help = "A short explanation."
+ doc_purpose = "A short explanation."
+ doc_usage = ""
+ doc_description = None # None value will completely ommit line from doc
+ # see http://python-doit.sourceforge.net/cmd_run.html#parameters
+ cmd_options = ()
+ needs_config = True
+
+ def __init__(self, *args, **kwargs):
+ BasePlugin.__init__(self, *args, **kwargs)
+ DoitCommand.__init__(self)
+
+ def execute(self, options={}, args=[]):
+ """Check if the command can run in the current environment,
+ fail if needed, or call _execute."""
+ if self.needs_config and not self.site.configured:
+ print("This command needs to run inside an existing Nikola site.")
+ return False
+ self._execute(options, args)
+
+ def _execute(self, options, args):
+ """Do whatever this command does.
+ @param options (dict) with values from cmd_options
+ @param args (list) list of positional arguments
+ """
+ raise NotImplementedError()
+
+
+def help(self):
+ """return help text"""
+ text = []
+ text.append("Purpose: %s" % self.doc_purpose)
+ text.append("Usage: nikola %s %s" % (self.name, self.doc_usage))
+ text.append('')
+
+ text.append("Options:")
+ for opt in self.options:
+ text.extend(opt.help_doc())
+
+ if self.doc_description is not None:
+ text.append("")
+ text.append("Description:")
+ text.append(self.doc_description)
+ return "\n".join(text)
- def run(self):
- """Do whatever this command does."""
- raise Exception("Implement Me First")
+DoitCommand.help = help
class BaseTask(BasePlugin):
@@ -64,7 +106,7 @@ class BaseTask(BasePlugin):
def gen_tasks(self):
"""Task generator."""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
class Task(BaseTask):
@@ -84,11 +126,11 @@ class TemplateSystem(object):
def set_directories(self, directories, cache_folder):
"""Sets the list of folders where templates are located and cache."""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
def template_deps(self, template_name):
"""Returns filenames which are dependencies for a template."""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
def render_template(name, output_name, context):
"""Renders template to a file using context.
@@ -96,7 +138,7 @@ class TemplateSystem(object):
This must save the data to output_name *and* return it
so that the caller may do additional processing.
"""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
class PageCompiler(object):
@@ -106,9 +148,9 @@ class PageCompiler(object):
def compile_html(self, source, dest):
"""Compile the source, save it on dest."""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
def create_post(self, path, onefile=False, title="", slug="", date="",
tags=""):
"""Create post file with optional metadata."""
- raise Exception("Implement Me First")
+ raise NotImplementedError()
diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py
index 2d9b1b2..b1de7f1 100644
--- a/nikola/plugins/__init__.py
+++ b/nikola/plugins/__init__.py
@@ -1,4 +1,3 @@
from __future__ import absolute_import
from . import command_import_wordpress # NOQA
-from . import command_build # NOQA
diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command_bootswatch_theme.py
index 6c1061f..8400c9f 100644
--- a/nikola/plugins/command_bootswatch_theme.py
+++ b/nikola/plugins/command_bootswatch_theme.py
@@ -23,7 +23,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser
import os
try:
@@ -38,37 +37,53 @@ class CommandBootswatchTheme(Command):
"""Given a swatch name and a parent theme, creates a custom theme."""
name = "bootswatch_theme"
+ doc_usage = "[options]"
+ doc_purpose = "Given a swatch name and a parent theme, creates a custom"\
+ " theme."
+ cmd_options = [
+ {
+ 'name': 'name',
+ 'short': 'n',
+ 'long': 'name',
+ 'default': 'custom',
+ 'type': str,
+ 'help': 'New theme name (default: custom)',
+ },
+ {
+ 'name': 'swatch',
+ 'short': 's',
+ 'default': 'slate',
+ 'type': str,
+ 'help': 'Name of the swatch from bootswatch.com.'
+ },
+ {
+ 'name': 'parent',
+ 'short': 'p',
+ 'long': 'parent',
+ 'default': 'site',
+ 'help': 'Parent theme name (default: site)',
+ },
+ ]
- def run(self, *args):
+ def _execute(self, options, args):
"""Given a swatch name and a parent theme, creates a custom theme."""
if requests is None:
print('To use the install_theme command, you need to install the '
'"requests" package.')
return
- 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
+ name = options['name']
+ swatch = options['swatch']
+ parent = options['parent']
- print("Creating '%s' theme from '%s' and '%s'" % (
- name, swatch, parent))
+ print("Creating '{0}' theme from '{1}' and '{2}'".format(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)
+ url = '/'.join(('http://bootswatch.com', swatch, fname))
print("Downloading: ", url)
data = requests.get(url).text
with open(os.path.join('themes', name, 'assets', 'css', fname),
@@ -77,5 +92,5 @@ class CommandBootswatchTheme(Command):
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)
+ print('Theme created. Change the THEME setting to "{0}" to use '
+ 'it.'.format(name))
diff --git a/nikola/plugins/command_build.plugin b/nikola/plugins/command_build.plugin
deleted file mode 100644
index 7d029a7..0000000
--- a/nikola/plugins/command_build.plugin
+++ /dev/null
@@ -1,10 +0,0 @@
-[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
deleted file mode 100644
index 8a1de5f..0000000
--- a/nikola/plugins/command_build.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import unicode_literals
-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 self.dodo:
- self.dodo.write(b'''
-import sys
-sys.path.insert(0, '.')
-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()
- ''')
- self.dodo.flush()
- first = args[0] if args else None
- if first in ('auto', 'clean', 'forget', 'ignore', 'list', 'run'):
- cmd = first
- args = args[1:]
- else:
- cmd = 'run'
- os.system('doit %s -f %s -d . %s' % (cmd, self.dodo.name,
- ''.join(args)))
- os.unlink(self.dodo.name)
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
index ae19c41..a396f63 100644
--- a/nikola/plugins/command_check.py
+++ b/nikola/plugins/command_check.py
@@ -23,9 +23,7 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser
import os
-import sys
try:
from urllib import unquote
from urlparse import urlparse
@@ -42,25 +40,48 @@ class CommandCheck(Command):
name = "check"
- def run(self, *args):
+ doc_usage = "-l [--find-sources] | -f"
+ doc_purpose = "Check links and files in the generated site."
+ cmd_options = [
+ {
+ 'name': 'links',
+ 'short': 'l',
+ 'long': 'check-links',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Check for dangling links',
+ },
+ {
+ 'name': 'files',
+ 'short': 'f',
+ 'long': 'check-files',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Check for unknown files',
+ },
+ {
+ 'name': 'find_sources',
+ 'long': 'find-sources',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List possible source files for files with broken links.',
+ },
+ ]
+
+ def _execute(self, options, 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:
+ if not options['links'] and not options['files']:
+ print(self.help())
+ return False
+ if options['links']:
+ scan_links(options['find_sources'])
+ if options['files']:
scan_files()
existing_targets = set([])
-def analize(task):
+def analize(task, find_sources=False):
try:
filename = task.split(":")[-1]
d = lxml.html.fromstring(open(filename).read())
@@ -79,26 +100,26 @@ def analize(task):
if os.path.exists(target_filename):
existing_targets.add(target_filename)
else:
- print("In %s broken link: " % filename, target)
- if '--find-sources' in sys.argv:
+ print("Broken link in {0}: ".format(filename), target)
+ if find_sources:
print("Possible sources:")
- print(os.popen(
- 'nikola build list --deps %s' % task, 'r').read())
+ print(os.popen('nikola list --deps ' + task,
+ 'r').read())
print("===============================\n")
except Exception as exc:
print("Error with:", filename, exc)
-def scan_links():
+def scan_links(find_sources=False):
print("Checking Links:\n===============\n")
- for task in os.popen('nikola build list --all', 'r').readlines():
+ for task in os.popen('nikola list --all', 'r').readlines():
task = task.strip()
if task.split(':')[0] in ('render_tags', 'render_archive',
'render_galleries', 'render_indexes',
'render_pages',
'render_site') and '.html' in task:
- analize(task)
+ analize(task, find_sources)
def scan_files():
@@ -106,7 +127,7 @@ def scan_files():
task_fnames = set([])
real_fnames = set([])
# First check that all targets are generated in the right places
- for task in os.popen('nikola build list --all', 'r').readlines():
+ for task in os.popen('nikola list --all', 'r').readlines():
task = task.strip()
if 'output' in task and ':' in task:
fname = task.split(':')[-1]
diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command_console.py
index 7a009fd..4af759f 100644
--- a/nikola/plugins/command_console.py
+++ b/nikola/plugins/command_console.py
@@ -22,6 +22,8 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from __future__ import print_function, unicode_literals
+
import os
from nikola.plugin_categories import Command
@@ -31,5 +33,31 @@ class Deploy(Command):
"""Start debugging console."""
name = "console"
- def run(self, *args):
- os.system('python -i -c "from nikola.console import *"')
+ def _execute(self, options, args):
+ """Start the console."""
+ from nikola import Nikola
+ try:
+ import conf
+ SITE = Nikola(**conf.__dict__)
+ SITE.scan_posts()
+ print("You can now access your configuration as conf and your "
+ "site engine as SITE.")
+ except ImportError:
+ print("No configuration found.")
+ import code
+ try:
+ import readline
+ except ImportError:
+ pass
+ else:
+ import rlcompleter
+ readline.set_completer(rlcompleter.Completer(globals()).complete)
+ readline.parse_and_bind("tab:complete")
+
+ pythonrc = os.environ.get("PYTHONSTARTUP")
+ if pythonrc and os.path.isfile(pythonrc):
+ try:
+ execfile(pythonrc) # NOQA
+ except NameError:
+ pass
+ code.interact(local=globals())
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
index 48d6e91..ffa86ab 100644
--- a/nikola/plugins/command_deploy.py
+++ b/nikola/plugins/command_deploy.py
@@ -23,7 +23,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser
import os
from nikola.plugin_categories import Command
@@ -33,9 +32,10 @@ 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))
+ doc_usage = ""
+ doc_purpose = "Deploy the site."
+
+ def _execute(self, command, args):
for command in self.site.config['DEPLOY_COMMANDS']:
print("==>", command)
os.system(command)
diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command_import_blogger.py
index aea210a..35a702e 100644
--- a/nikola/plugins/command_import_blogger.py
+++ b/nikola/plugins/command_import_blogger.py
@@ -27,7 +27,6 @@ import codecs
import csv
import datetime
import os
-from optparse import OptionParser
import time
try:
@@ -52,9 +51,63 @@ class CommandImportBlogger(Command):
"""Import a blogger dump."""
name = "import_blogger"
+ needs_config = False
+ doc_usage = "[options] blogger_export_file"
+ doc_purpose = "Import a blogger dump."
+ cmd_options = [
+ {
+ 'name': 'output_folder',
+ 'long': 'output-folder',
+ 'short': 'o',
+ 'default': 'new_site',
+ 'help': 'Location to write imported content.'
+ },
+ {
+ 'name': 'exclude_drafts',
+ 'long': 'no-drafts',
+ 'short': 'd',
+ 'default': False,
+ 'type': bool,
+ 'help': "Don't import drafts",
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Import a Wordpress blog from an export file into a Nikola site."""
+
+ # Parse the data
+ if feedparser is None:
+ print('To use the import_blogger command,'
+ ' you have to install the "feedparser" package.')
+ return
+
+ if not args:
+ print(self.help())
+ return
+
+ options['filename'] = args[0]
+ self.blogger_export_file = options['filename']
+ self.output_folder = options['output_folder']
+ self.import_into_existing_site = False
+ self.exclude_drafts = options['exclude_drafts']
+ self.url_map = {}
+ channel = self.get_channel_from_file(self.blogger_export_file)
+ self.context = self.populate_context(channel)
+ conf_template = self.generate_base_site()
+ self.context['REDIRECTIONS'] = self.configure_redirections(
+ self.url_map)
+
+ self.import_posts(channel)
+ self.write_urlmap_csv(
+ os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
+
+ self.write_configuration(self.get_configuration_output_path(
+ ), conf_template.render(**self.context))
@classmethod
def get_channel_from_file(cls, filename):
+ if not os.path.isfile(filename):
+ raise Exception("Missing file: %s" % filename)
return feedparser.parse(filename)
@staticmethod
@@ -65,7 +118,7 @@ class CommandImportBlogger(Command):
src = (urlparse(k).path + 'index.html')[1:]
dst = (urlparse(v).path)
if src == 'index.html':
- print("Can't do a redirect for: %r" % k)
+ print("Can't do a redirect for: {0!r}".format(k))
else:
redirections.append((src, dst))
@@ -73,11 +126,11 @@ class CommandImportBlogger(Command):
def generate_base_site(self):
if not os.path.exists(self.output_folder):
- os.system('nikola init --empty %s' % (self.output_folder, ))
+ os.system('nikola init --empty ' + self.output_folder)
else:
self.import_into_existing_site = True
- print('The folder %s already exists - assuming that this is a '
- 'already existing nikola site.' % self.output_folder)
+ print('The folder {0} already exists - assuming that this is a '
+ 'already existing nikola site.'.format(self.output_folder))
conf_template = Template(filename=os.path.join(
os.path.dirname(utils.__file__), 'conf.py.in'))
@@ -92,7 +145,7 @@ class CommandImportBlogger(Command):
context['BLOG_TITLE'] = channel.feed.title
context['BLOG_DESCRIPTION'] = '' # Missing in the dump
- context['BLOG_URL'] = channel.feed.link.rstrip('/')
+ context['SITE_URL'] = channel.feed.link.rstrip('/')
context['BLOG_EMAIL'] = channel.feed.author_detail.email
context['BLOG_AUTHOR'] = channel.feed.author_detail.name
context['POST_PAGES'] = '''(
@@ -124,12 +177,8 @@ class CommandImportBlogger(Command):
@staticmethod
def write_metadata(filename, title, slug, post_date, description, tags):
with codecs.open(filename, "w+", "utf8") as fd:
- fd.write('%s\n' % title)
- fd.write('%s\n' % slug)
- fd.write('%s\n' % post_date)
- fd.write('%s\n' % ','.join(tags))
- fd.write('\n')
- fd.write('%s\n' % description)
+ fd.write('\n'.join((title, slug, post_date, ','.join(tags), '',
+ description)))
def import_item(self, item, out_folder=None):
"""Takes an item from the feed and creates a post file."""
@@ -145,8 +194,8 @@ class CommandImportBlogger(Command):
# blogger supports empty titles, which Nikola doesn't
if not title:
- print("Warning: Empty title in post with URL %s. Using NO_TITLE "
- "as placeholder, please fix." % link)
+ print("Warning: Empty title in post with URL {0}. Using NO_TITLE "
+ "as placeholder, please fix.".format(link))
title = "NO_TITLE"
if link_path.lower().endswith('.html'):
@@ -179,11 +228,11 @@ class CommandImportBlogger(Command):
else:
is_draft = False
- self.url_map[link] = self.context['BLOG_URL'] + '/' + \
+ self.url_map[link] = self.context['SITE_URL'] + '/' + \
out_folder + '/' + slug + '.html'
if is_draft and self.exclude_drafts:
- print('Draft "%s" will not be imported.' % (title, ))
+ print('Draft "{0}" will not be imported.'.format(title))
elif content.strip():
# If no content is found, no files are written.
content = self.transform_content(content)
@@ -195,8 +244,8 @@ class CommandImportBlogger(Command):
os.path.join(self.output_folder, out_folder, slug + '.html'),
content)
else:
- print('Not going to import "%s" because it seems to contain'
- ' no content.' % (title, ))
+ print('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
def process_item(self, item):
post_type = item.tags[0].term
@@ -235,10 +284,10 @@ class CommandImportBlogger(Command):
if not self.import_into_existing_site:
filename = 'conf.py'
else:
- filename = 'conf.py.wordpress_import-%s' % datetime.datetime.now(
- ).strftime('%Y%m%d_%H%M%s')
+ filename = 'conf.py.wordpress_import-{0}'.format(
+ datetime.datetime.now().strftime('%Y%m%d_%H%M%s'))
config_output_path = os.path.join(self.output_folder, filename)
- print('Configuration will be written to: %s' % config_output_path)
+ print('Configuration will be written to: ' + config_output_path)
return config_output_path
@@ -247,54 +296,6 @@ class CommandImportBlogger(Command):
with codecs.open(filename, 'w+', 'utf8') as fd:
fd.write(rendered_template)
- def run(self, *arguments):
- """Import a Wordpress blog from an export file into a Nikola site."""
- # Parse the data
- if feedparser is None:
- print('To use the import_blogger command,'
- ' you have to install the "feedparser" package.')
- return
-
- parser = OptionParser(
- usage="nikola %s [options] blogger_export_file" % self.name)
- parser.add_option('-f', '--filename', dest='filename',
- help='Blogger export file from which the import is '
- 'made.')
- parser.add_option('-o', '--output-folder', dest='output_folder',
- default='new_site',
- help='The location into which the imported content '
- 'will be written')
- parser.add_option('-d', '--no-drafts', dest='exclude_drafts',
- default=False, action="store_true", help='Do not '
- 'import drafts.')
-
- (options, args) = parser.parse_args(list(arguments))
-
- if not options.filename and args:
- options.filename = args[0]
-
- if not options.filename:
- parser.print_usage()
- return
-
- self.blogger_export_file = options.filename
- self.output_folder = options.output_folder
- self.import_into_existing_site = False
- self.exclude_drafts = options.exclude_drafts
- self.url_map = {}
- channel = self.get_channel_from_file(self.blogger_export_file)
- self.context = self.populate_context(channel)
- conf_template = self.generate_base_site()
- self.context['REDIRECTIONS'] = self.configure_redirections(
- self.url_map)
-
- self.import_posts(channel)
- self.write_urlmap_csv(
- os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
-
- self.write_configuration(self.get_configuration_output_path(
- ), conf_template.render(**self.context))
-
def replacer(dst):
return links.get(dst, dst)
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py
index 07028d8..e7ecca0 100644
--- a/nikola/plugins/command_import_wordpress.py
+++ b/nikola/plugins/command_import_wordpress.py
@@ -28,7 +28,6 @@ import csv
import datetime
import os
import re
-from optparse import OptionParser
try:
from urlparse import urlparse
@@ -53,9 +52,104 @@ class CommandImportWordpress(Command):
"""Import a wordpress dump."""
name = "import_wordpress"
+ needs_config = False
+ doc_usage = "[options] wordpress_export_file"
+ doc_purpose = "Import a wordpress dump."
+ cmd_options = [
+ {
+ 'name': 'output_folder',
+ 'long': 'output-folder',
+ 'short': 'o',
+ 'default': 'new_site',
+ 'help': 'Location to write imported content.'
+ },
+ {
+ 'name': 'exclude_drafts',
+ 'long': 'no-drafts',
+ 'short': 'd',
+ 'default': False,
+ 'type': bool,
+ 'help': "Don't import drafts",
+ },
+ {
+ 'name': 'squash_newlines',
+ 'long': 'squash-newlines',
+ 'default': False,
+ 'type': bool,
+ 'help': "Shorten multiple newlines in a row to only two newlines",
+ },
+ {
+ 'name': 'no_downloads',
+ 'long': 'no-downloads',
+ 'default': False,
+ 'type': bool,
+ 'help': "Do not try to download files for the import",
+ },
+ ]
+
+ def _execute(self, options={}, args=[]):
+ """Import a Wordpress blog from an export file into a Nikola site."""
+ # Parse the data
+ print(options, args)
+ if requests is None:
+ print('To use the import_wordpress command,'
+ ' you have to install the "requests" package.')
+ return
- @staticmethod
- def read_xml_file(filename):
+ if not args:
+ print(self.help())
+ return
+
+ options['filename'] = args[0]
+
+ if len(args) > 1:
+ options['output_folder'] = args[1]
+
+ self.wordpress_export_file = options['filename']
+ self.squash_newlines = options.get('squash_newlines', False)
+ self.no_downloads = options.get('no_downloads', False)
+ self.output_folder = options.get('output_folder', 'new_site')
+ self.import_into_existing_site = False
+ self.exclude_drafts = options.get('exclude_drafts', False)
+ self.url_map = {}
+ channel = self.get_channel_from_file(self.wordpress_export_file)
+ self.context = self.populate_context(channel)
+ conf_template = self.generate_base_site()
+
+ self.import_posts(channel)
+
+ self.context['REDIRECTIONS'] = self.configure_redirections(
+ self.url_map)
+ self.write_urlmap_csv(
+ os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
+ rendered_template = conf_template.render(**self.context)
+ rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ',
+ rendered_template)
+ self.write_configuration(self.get_configuration_output_path(),
+ rendered_template)
+
+ @classmethod
+ def _glue_xml_lines(cls, xml):
+ new_xml = xml[0]
+ previous_line_ended_in_newline = new_xml.endswith(b'\n')
+ previous_line_was_indentet = False
+ for line in xml[1:]:
+ if (re.match(b'^[ \t]+', line) and previous_line_ended_in_newline):
+ new_xml = b''.join((new_xml, line))
+ previous_line_was_indentet = True
+ elif previous_line_was_indentet:
+ new_xml = b''.join((new_xml, line))
+ previous_line_was_indentet = False
+ else:
+ new_xml = b'\n'.join((new_xml, line))
+ previous_line_was_indentet = False
+
+ previous_line_ended_in_newline = line.endswith(b'\n')
+
+ return new_xml
+
+ @classmethod
+ def read_xml_file(cls, filename):
xml = []
with open(filename, 'rb') as fd:
@@ -64,9 +158,8 @@ class CommandImportWordpress(Command):
if b'<atom:link rel=' in line:
continue
xml.append(line)
- xml = b'\n'.join(xml)
- return xml
+ return cls._glue_xml_lines(xml)
@classmethod
def get_channel_from_file(cls, filename):
@@ -82,7 +175,7 @@ class CommandImportWordpress(Command):
src = (urlparse(k).path + 'index.html')[1:]
dst = (urlparse(v).path)
if src == 'index.html':
- print("Can't do a redirect for: %r" % k)
+ print("Can't do a redirect for: {0!r}".format(k))
else:
redirections.append((src, dst))
@@ -90,11 +183,11 @@ class CommandImportWordpress(Command):
def generate_base_site(self):
if not os.path.exists(self.output_folder):
- os.system('nikola init --empty %s' % (self.output_folder, ))
+ os.system('nikola init ' + self.output_folder)
else:
self.import_into_existing_site = True
- print('The folder %s already exists - assuming that this is a '
- 'already existing nikola site.' % self.output_folder)
+ print('The folder {0} already exists - assuming that this is a '
+ 'already existing nikola site.'.format(self.output_folder))
conf_template = Template(filename=os.path.join(
os.path.dirname(utils.__file__), 'conf.py.in'))
@@ -111,15 +204,16 @@ class CommandImportWordpress(Command):
'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('{%s}author' % wordpress_namespace)
+ context['SITE_URL'] = get_text_tag(channel, 'link', '#')
+ context['BASE_URL'] = get_text_tag(channel, 'link', '#')
+ author = channel.find('{{{0}}}author'.format(wordpress_namespace))
context['BLOG_EMAIL'] = get_text_tag(
author,
- '{%s}author_email' % wordpress_namespace,
+ '{{{0}}}author_email'.format(wordpress_namespace),
"joe@example.com")
context['BLOG_AUTHOR'] = get_text_tag(
author,
- '{%s}author_display_name' % wordpress_namespace,
+ '{{{0}}}author_display_name'.format(wordpress_namespace),
"Joe Example")
context['POST_PAGES'] = '''(
("posts/*.wp", "posts", "post.tmpl", True),
@@ -134,25 +228,29 @@ class CommandImportWordpress(Command):
return context
- @staticmethod
- def download_url_content_to_file(url, dst_path):
+ def download_url_content_to_file(self, url, dst_path):
+ if self.no_downloads:
+ return
+
try:
with open(dst_path, 'wb+') as fd:
fd.write(requests.get(url).content)
except requests.exceptions.ConnectionError as err:
- print("Downloading %s to %s failed: %s" % (url, dst_path, err))
+ print("Downloading {0} to {1} failed: {2}".format(url, dst_path,
+ err))
def import_attachment(self, item, wordpress_namespace):
url = get_text_tag(
- item, '{%s}attachment_url' % wordpress_namespace, 'foo')
- link = get_text_tag(item, '{%s}link' % wordpress_namespace, 'foo')
+ item, '{{{0}}}attachment_url'.format(wordpress_namespace), 'foo')
+ link = get_text_tag(item, '{{{0}}}link'.format(wordpress_namespace),
+ 'foo')
path = urlparse(url).path
dst_path = os.path.join(*([self.output_folder, '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))
+ print("Downloading {0} => {1}".format(url, dst_path))
self.download_url_content_to_file(url, dst_path)
dst_url = '/'.join(dst_path.split(os.sep)[2:])
links[link] = '/' + dst_url
@@ -173,10 +271,18 @@ class CommandImportWordpress(Command):
return new_caption
- @classmethod
- def transform_content(cls, content):
- new_content = cls.transform_sourcecode(content)
- return cls.transform_caption(new_content)
+ def transform_multiple_newlines(self, content):
+ """Replaces multiple newlines with only two."""
+ if self.squash_newlines:
+ return re.sub(r'\n{3,}', r'\n\n', content)
+ else:
+ return content
+
+ def transform_content(self, content):
+ new_content = self.transform_sourcecode(content)
+ new_content = self.transform_caption(new_content)
+ new_content = self.transform_multiple_newlines(new_content)
+ return new_content
@classmethod
def write_content(cls, filename, content):
@@ -188,13 +294,16 @@ class CommandImportWordpress(Command):
@staticmethod
def write_metadata(filename, title, slug, post_date, description, tags):
+ if not description:
+ description = ""
+
with codecs.open(filename, "w+", "utf8") as fd:
- fd.write('%s\n' % title)
- fd.write('%s\n' % slug)
- fd.write('%s\n' % post_date)
- fd.write('%s\n' % ','.join(tags))
+ fd.write('{0}\n'.format(title))
+ fd.write('{0}\n'.format(slug))
+ fd.write('{0}\n'.format(post_date))
+ fd.write('{0}\n'.format(','.join(tags)))
fd.write('\n')
- fd.write('%s\n' % description)
+ fd.write('{0}\n'.format(description))
def import_item(self, item, wordpress_namespace, out_folder=None):
"""Takes an item from the feed and creates a post file."""
@@ -208,19 +317,19 @@ class CommandImportWordpress(Command):
slug = utils.slugify(urlparse(link).path)
if not slug: # it happens if the post has no "nice" URL
slug = get_text_tag(
- item, '{%s}post_name' % wordpress_namespace, None)
+ item, '{{{0}}}post_name'.format(wordpress_namespace), None)
if not slug: # it *may* happen
slug = get_text_tag(
- item, '{%s}post_id' % wordpress_namespace, None)
+ item, '{{{0}}}post_id'.format(wordpress_namespace), None)
if not slug: # should never happen
print("Error converting post:", title)
return
description = get_text_tag(item, 'description', '')
post_date = get_text_tag(
- item, '{%s}post_date' % wordpress_namespace, None)
+ item, '{{{0}}}post_date'.format(wordpress_namespace), None)
status = get_text_tag(
- item, '{%s}status' % wordpress_namespace, 'publish')
+ item, '{{{0}}}status'.format(wordpress_namespace), 'publish')
content = get_text_tag(
item, '{http://purl.org/rss/1.0/modules/content/}encoded', '')
@@ -237,13 +346,13 @@ class CommandImportWordpress(Command):
continue
tags.append(text)
- self.url_map[link] = self.context['BLOG_URL'] + '/' + \
- out_folder + '/' + slug + '.html'
-
if is_draft and self.exclude_drafts:
- print('Draft "%s" will not be imported.' % (title, ))
+ print('Draft "{0}" will not be imported.'.format(title))
elif content.strip():
# If no content is found, no files are written.
+ self.url_map[link] = self.context['SITE_URL'] + '/' + \
+ out_folder + '/' + slug + '.html'
+
content = self.transform_content(content)
self.write_metadata(os.path.join(self.output_folder, out_folder,
@@ -253,15 +362,15 @@ class CommandImportWordpress(Command):
os.path.join(self.output_folder, out_folder, slug + '.wp'),
content)
else:
- print('Not going to import "%s" because it seems to contain'
- ' no content.' % (title, ))
+ print('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
def process_item(self, item):
# The namespace usually is something like:
# http://wordpress.org/export/1.2/
wordpress_namespace = item.nsmap['wp']
post_type = get_text_tag(
- item, '{%s}post_type' % wordpress_namespace, 'post')
+ item, '{{{0}}}post_type'.format(wordpress_namespace), 'post')
if post_type == 'attachment':
self.import_attachment(item, wordpress_namespace)
@@ -285,10 +394,10 @@ class CommandImportWordpress(Command):
if not self.import_into_existing_site:
filename = 'conf.py'
else:
- filename = 'conf.py.wordpress_import-%s' % datetime.datetime.now(
- ).strftime('%Y%m%d_%H%M%s')
+ filename = 'conf.py.wordpress_import-{0}'.format(
+ datetime.datetime.now().strftime('%Y%m%d_%H%M%s'))
config_output_path = os.path.join(self.output_folder, filename)
- print('Configuration will be written to: %s' % config_output_path)
+ print('Configuration will be written to:', config_output_path)
return config_output_path
@@ -297,53 +406,6 @@ class CommandImportWordpress(Command):
with codecs.open(filename, 'w+', 'utf8') as fd:
fd.write(rendered_template)
- def run(self, *arguments):
- """Import a Wordpress blog from an export file into a Nikola site."""
- # Parse the data
- if requests is None:
- print('To use the import_wordpress command,'
- ' you have to install the "requests" package.')
- return
-
- parser = OptionParser(usage="nikola %s [options] "
- "wordpress_export_file" % self.name)
- parser.add_option('-f', '--filename', dest='filename',
- help='WordPress export file from which the import '
- 'made.')
- parser.add_option('-o', '--output-folder', dest='output_folder',
- default='new_site', help='The location into which '
- 'the imported content will be written')
- parser.add_option('-d', '--no-drafts', dest='exclude_drafts',
- default=False, action="store_true", help='Do not '
- 'import drafts.')
-
- (options, args) = parser.parse_args(list(arguments))
-
- if not options.filename and args:
- options.filename = args[0]
-
- if not options.filename:
- parser.print_usage()
- return
-
- self.wordpress_export_file = options.filename
- self.output_folder = options.output_folder
- self.import_into_existing_site = False
- self.exclude_drafts = options.exclude_drafts
- self.url_map = {}
- channel = self.get_channel_from_file(self.wordpress_export_file)
- self.context = self.populate_context(channel)
- conf_template = self.generate_base_site()
- self.context['REDIRECTIONS'] = self.configure_redirections(
- self.url_map)
-
- self.import_posts(channel)
- self.write_urlmap_csv(
- os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
-
- self.write_configuration(self.get_configuration_output_path(
- ), conf_template.render(**self.context))
-
def replacer(dst):
return links.get(dst, dst)
diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command_init.py
index e9bd001..bc36266 100644
--- a/nikola/plugins/command_init.py
+++ b/nikola/plugins/command_init.py
@@ -23,7 +23,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser, OptionGroup
import os
import shutil
import codecs
@@ -39,16 +38,23 @@ class CommandInit(Command):
name = "init"
- usage = """Usage: nikola init folder [options].
-
-That will create a sample site in the specified folder.
-The destination folder must not exist.
-"""
+ doc_usage = "[--demo] folder"
+ needs_config = False
+ doc_purpose = """Create a Nikola site in the specified folder."""
+ cmd_options = [
+ {
+ 'name': 'demo',
+ 'long': 'demo',
+ 'default': False,
+ 'type': bool,
+ 'help': "Create a site filled with example data.",
+ }
+ ]
SAMPLE_CONF = {
'BLOG_AUTHOR': "Your Name",
'BLOG_TITLE': "Demo Site",
- 'BLOG_URL': "http://nikola.ralsina.com.ar",
+ 'SITE_URL': "http://nikola.ralsina.com.ar",
'BLOG_EMAIL': "joe@demo.site",
'BLOG_DESCRIPTION': "This is a demo site for Nikola.",
'DEFAULT_LANG': "en",
@@ -67,7 +73,7 @@ The destination folder must not exist.
"wiki": ('.wiki',),
"ipynb": ('.ipynb',),
"html": ('.html', '.htm')
- }""",
+}""",
'REDIRECTIONS': '[]',
}
@@ -95,32 +101,22 @@ The destination folder must not exist.
def get_path_to_nikola_modules():
return os.path.dirname(nikola.__file__)
- def run(self, *args):
+ def _execute(self, options={}, args=None):
"""Create a new site."""
- parser = OptionParser(usage=self.usage)
- group = OptionGroup(parser, "Site Options")
- group.add_option(
- "--empty", action="store_true", dest='empty', default=True,
- help="Create an empty site with only a config.")
- group.add_option("--demo", action="store_false", dest='empty',
- help="Create a site filled with example data.")
- parser.add_option_group(group)
- (options, args) = parser.parse_args(list(args))
-
if not args:
print("Usage: nikola init folder [options]")
- return
+ return False
target = args[0]
if target is None:
print(self.usage)
else:
- if options.empty:
+ if not options or not options.get('demo'):
self.create_empty_site(target)
- print('Created empty site at %s.' % target)
+ print('Created empty site at {0}.'.format(target))
else:
self.copy_sample_site(target)
- print("A new site with example data has been created at %s."
- % target)
+ print("A new site with example data has been created at "
+ "{0}.".format(target))
print("See README.txt in that folder for more information.")
self.create_configuration(target)
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
index 0dc000b..04a2cce 100644
--- a/nikola/plugins/command_install_theme.py
+++ b/nikola/plugins/command_install_theme.py
@@ -23,7 +23,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser
import os
import json
from io import BytesIO
@@ -41,31 +40,39 @@ class CommandInstallTheme(Command):
"""Start test server."""
name = "install_theme"
+ doc_usage = "[[-u] theme_name] | [[-u] -l]"
+ doc_purpose = "Install theme into current site."
+ cmd_options = [
+ {
+ 'name': 'list',
+ 'short': 'l',
+ 'long': 'list',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Show list of available themes.'
+ },
+ {
+ 'name': 'url',
+ 'short': 'u',
+ 'long': 'url',
+ 'type': str,
+ 'help': "URL for the theme repository (default: "
+ "http://nikola.ralsina.com.ar/themes/index.json)",
+ 'default': 'http://nikola.ralsina.com.ar/themes/index.json'
+ },
+ ]
- def run(self, *args):
+ def _execute(self, options, args):
"""Install theme into current site."""
- if requests is None:
- print('To use the install_theme command, you need to install the '
- '"requests" package.')
- return
- 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
+ listing = options['list']
+ url = options['url']
+ if args:
+ name = args[0]
+ else:
+ name = None
if name is None and not listing:
- print("This command needs either the -n or the -l option.")
+ print("This command needs either a theme name or the -l option.")
return False
data = requests.get(url).text
data = json.loads(data)
@@ -84,11 +91,11 @@ class CommandInstallTheme(Command):
os.makedirs("themes")
except:
raise OSError("mkdir 'theme' error!")
- print('Downloading: %s' % data[name])
+ print('Downloading: ' + data[name])
zip_file = BytesIO()
zip_file.write(requests.get(data[name]).content)
- print('Extracting: %s into themes' % name)
+ print('Extracting: {0} into themes'.format(name))
utils.extract_all(zip_file)
else:
- print("Can't find theme %s" % name)
+ print("Can't find theme " + name)
return False
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py
index 9b6397b..a823da3 100644
--- a/nikola/plugins/command_new_post.py
+++ b/nikola/plugins/command_new_post.py
@@ -25,7 +25,6 @@
from __future__ import unicode_literals, print_function
import codecs
import datetime
-from optparse import OptionParser
import os
import sys
@@ -51,9 +50,9 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages):
if not filtered:
type_name = "post" if is_post else "page"
raise Exception("Can't find a way, using your configuration, to create"
- "a %s in format %s. You may want to tweak "
- "post_compilers or post_pages in conf.py" %
- (type_name, compiler))
+ "a {0} in format {1}. You may want to tweak "
+ "post_compilers or post_pages in conf.py".format(
+ type_name, compiler))
return filtered[0]
@@ -61,42 +60,88 @@ class CommandNewPost(Command):
"""Create a new post."""
name = "new_post"
-
- def run(self, *args):
- """Create a new post."""
+ doc_usage = "[options] [path]"
+ doc_purpose = "Create a new blog post or site page."
+ cmd_options = [
+ {
+ 'name': 'is_page',
+ 'short': 'p',
+ 'long': 'page',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Create a page instead of a blog post.'
+ },
+ {
+ 'name': 'title',
+ 'short': 't',
+ 'long': 'title',
+ 'type': str,
+ 'default': '',
+ 'help': 'Title for the page/post.'
+ },
+ {
+ 'name': 'tags',
+ 'long': 'tags',
+ 'type': str,
+ 'default': '',
+ 'help': 'Comma-separated tags for the page/post.'
+ },
+ {
+ 'name': 'onefile',
+ 'short': '1',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Create post with embedded metadata (single file format)'
+ },
+ {
+ 'name': 'twofile',
+ 'short': '2',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Create post with separate metadata (two file format)'
+ },
+ {
+ 'name': 'post_format',
+ 'short': 'f',
+ 'long': 'format',
+ 'type': str,
+ 'default': 'rest',
+ 'help': 'Markup format for post, one of rest, markdown, wiki, '
+ 'bbcode, html, textile, txt2tags',
+ }
+ ]
+
+ def _execute(self, options, args):
+ """Create a new post or page."""
compiler_names = [p.name for p in
self.site.plugin_manager.getPluginsOfCategory(
"PageCompiler")]
- parser = OptionParser(usage="nikola %s [options]" % self.name)
- parser.add_option('-p', '--page', dest='is_post', action='store_false',
- default=True, 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('-2', dest='onefile', action='store_false',
- help='Create post with separate metadata (two 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 (one of %s)' %
- ','.join(compiler_names))
- (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
+ if len(args) > 1:
+ print(self.help())
+ return False
+ elif args:
+ path = args[0]
+ else:
+ path = None
+
+ is_page = options.get('is_page', False)
+ is_post = not is_page
+ title = options['title'] or None
+ tags = options['tags']
+ onefile = options['onefile']
+ twofile = options['twofile']
+
+ if twofile:
+ onefile = False
+ if not onefile and not twofile:
+ onefile = self.site.config.get('ONE_FILE_POSTS', True)
+
+ post_format = options['post_format']
+
if post_format not in compiler_names:
- print("ERROR: Unknown post format %s" % post_format)
+ print("ERROR: Unknown post format " + post_format)
return
compiler_plugin = self.site.plugin_manager.getPluginByName(
post_format, "PageCompiler").plugin_object
@@ -118,19 +163,29 @@ class CommandNewPost(Command):
if isinstance(title, bytes):
title = title.decode(sys.stdin.encoding)
title = title.strip()
- slug = utils.slugify(title)
+ if not path:
+ slug = utils.slugify(title)
+ else:
+ slug = utils.slugify(os.path.splitext(os.path.basename(path))[0])
date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
data = [title, slug, date, tags]
output_path = os.path.dirname(entry[0])
meta_path = os.path.join(output_path, slug + ".meta")
pattern = os.path.basename(entry[0])
suffix = pattern[1:]
- txt_path = os.path.join(output_path, slug + suffix)
+ if not path:
+ txt_path = os.path.join(output_path, slug + suffix)
+ else:
+ txt_path = path
if (not onefile and os.path.isfile(meta_path)) or \
os.path.isfile(txt_path):
print("The title already exists!")
exit()
+
+ d_name = os.path.dirname(txt_path)
+ if not os.path.exists(d_name):
+ os.makedirs(d_name)
compiler_plugin.create_post(txt_path, onefile, title, slug, date, tags)
if not onefile: # write metadata file
diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py
index 75e07a9..64efe7d 100644
--- a/nikola/plugins/command_serve.py
+++ b/nikola/plugins/command_serve.py
@@ -23,7 +23,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
-from optparse import OptionParser
import os
try:
from BaseHTTPServer import HTTPServer
@@ -39,26 +38,39 @@ class CommandBuild(Command):
"""Start test server."""
name = "serve"
+ doc_usage = "[options]"
+ doc_purpose = "Start the test webserver."
- 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))
+ cmd_options = (
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ {
+ 'name': 'address',
+ 'short': 'a',
+ 'long': '--address',
+ 'type': str,
+ 'default': '127.0.0.1',
+ 'help': 'Address to bind (default: 127.0.0.1)',
+ },
+ )
+ def _execute(self, options, args):
+ """Start test server."""
out_dir = self.site.config['OUTPUT_FOLDER']
if not os.path.isdir(out_dir):
- print("Error: Missing '%s' folder?" % out_dir)
+ print("Error: Missing '{0}' folder?".format(out_dir))
else:
os.chdir(out_dir)
- httpd = HTTPServer((options.address, options.port),
+ httpd = HTTPServer((options['address'], options['port']),
OurHTTPRequestHandler)
sa = httpd.socket.getsockname()
- print("Serving HTTP on {0[0]} port {0[1]}...".format(sa))
+ print("Serving HTTP on", sa[0], "port", sa[1], "...")
httpd.serve_forever()
diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile_bbcode.py
index fd7fe1a..26de727 100644
--- a/nikola/plugins/compile_bbcode.py
+++ b/nikola/plugins/compile_bbcode.py
@@ -68,10 +68,10 @@ class CompileTextile(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('[note]<!--\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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n')
fd.write('-->[/note]\n\n')
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile_html.py
index 850a3e5..6c1c381 100644
--- a/nikola/plugins/compile_html.py
+++ b/nikola/plugins/compile_html.py
@@ -51,10 +51,10 @@ class CompileHtml(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n')
fd.write('-->\n\n')
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile_markdown/__init__.py
index 5eb25c8..7aa03a9 100644
--- a/nikola/plugins/compile_markdown/__init__.py
+++ b/nikola/plugins/compile_markdown/__init__.py
@@ -55,8 +55,10 @@ class CompileMarkdown(PageCompiler):
output = markdown(data, ['fenced_code', 'codehilite'])
# h1 is reserved for the title so increment all header levels
for n in reversed(range(1, 9)):
- output = re.sub('<h%i>' % n, '<h%i>' % (n + 1), output)
- output = re.sub('</h%i>' % n, '</h%i>' % (n + 1), output)
+ output = re.sub('<h{0}>'.format(n), '<h{0}>'.format(n + 1),
+ output)
+ output = re.sub('</h{0}>'.format(n), '</h{0}>'.format(n + 1),
+ 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'
@@ -69,10 +71,10 @@ class CompileMarkdown(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n')
fd.write('-->\n\n')
diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py
index 4191add..b0a0c00 100644
--- a/nikola/plugins/compile_rest/__init__.py
+++ b/nikola/plugins/compile_rest/__init__.py
@@ -44,6 +44,8 @@ from .slides import slides
directives.register_directive('slides', slides)
from .gist_directive import GitHubGist
directives.register_directive('gist', GitHubGist)
+from .soundcloud import soundcloud
+directives.register_directive('soundcloud', soundcloud)
from nikola.plugin_categories import PageCompiler
@@ -75,10 +77,10 @@ class CompileRest(PageCompiler):
tags=""):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
- 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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n\n')
fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile_rest/gist_directive.py
index 3bfe818..0ea8f23 100644
--- a/nikola/plugins/compile_rest/gist_directive.py
+++ b/nikola/plugins/compile_rest/gist_directive.py
@@ -24,11 +24,11 @@ class GitHubGist(Directive):
has_content = False
def get_raw_gist_with_filename(self, gistID, filename):
- url = "https://raw.github.com/gist/%s/%s" % (gistID, filename)
+ url = '/'.join(("https://raw.github.com/gist", gistID, filename))
return requests.get(url).text
def get_raw_gist(self, gistID):
- url = "https://raw.github.com/gist/%s/" % (gistID)
+ url = "https://raw.github.com/gist/{0}/".format(gistID)
return requests.get(url).text
def run(self):
@@ -43,12 +43,12 @@ class GitHubGist(Directive):
if 'file' in self.options:
filename = self.options['file']
rawGist = (self.get_raw_gist_with_filename(gistID, filename))
- embedHTML = ('<script src="https://gist.github.com/%s.js?file=%s">'
- '</script>') % (gistID, filename)
+ embedHTML = ('<script src="https://gist.github.com/{0}.js'
+ '?file={1}"></script>').format(gistID, filename)
else:
rawGist = (self.get_raw_gist(gistID))
- embedHTML = ('<script src="https://gist.github.com/%s.js">'
- '</script>') % gistID
+ embedHTML = ('<script src="https://gist.github.com/{0}.js">'
+ '</script>').format(gistID)
return [nodes.raw('', embedHTML, format='html'),
nodes.raw('', '<noscript>', format='html'),
diff --git a/nikola/plugins/compile_rest/pygments_code_block_directive.py b/nikola/plugins/compile_rest/pygments_code_block_directive.py
index f858427..79bada2 100644
--- a/nikola/plugins/compile_rest/pygments_code_block_directive.py
+++ b/nikola/plugins/compile_rest/pygments_code_block_directive.py
@@ -165,9 +165,9 @@ def code_block_directive(name, arguments, options, content, lineno,
after_index = content.find(after_text)
if after_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "start-at" option of "%s" '
- 'code-block directive:\nText not found.'
- % options['start-at'])
+ 'Problem with "start-at" option of "{0}" '
+ 'code-block directive:\nText not found.'.format(
+ options['start-at']))
# patch mmueller start
# Move the after_index to the beginning of the line with the
# match.
@@ -192,9 +192,9 @@ def code_block_directive(name, arguments, options, content, lineno,
after_index = content.find(after_text)
if after_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "start-after" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['start-after'])
+ 'Problem with "start-after" option of "{0}" '
+ 'code-block directive:\nText not found.'.format(
+ options['start-after']))
line_offset = len(content[:after_index +
len(after_text)].splitlines())
content = content[after_index + len(after_text):]
@@ -207,9 +207,9 @@ def code_block_directive(name, arguments, options, content, lineno,
before_index = content.find(before_text)
if before_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "end-at" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['end-at'])
+ 'Problem with "end-at" option of "{0}" '
+ 'code-block directive:\nText not found.'.format(
+ options['end-at']))
content = content[:before_index + len(before_text)]
before_text = options.get('end-before', None)
@@ -219,9 +219,9 @@ def code_block_directive(name, arguments, options, content, lineno,
before_index = content.find(before_text)
if before_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "end-before" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['end-before'])
+ 'Problem with "end-before" option of "{0}" '
+ 'code-block directive:\nText not found.'.format(
+ options['end-before']))
content = content[:before_index]
else:
@@ -246,8 +246,9 @@ def code_block_directive(name, arguments, options, content, lineno,
lineno = 1 + line_offset
total_lines = content.count('\n') + 1 + line_offset
lnwidth = len(str(total_lines))
- fstr = "\n%%%dd " % lnwidth
- code_block += nodes.inline(fstr[1:] % lineno, fstr[1:] % lineno,
+ fstr = "\n%{0}d ".format(lnwidth)
+ code_block += nodes.inline(fstr[1:].format(lineno),
+ fstr[1:].format(lineno),
classes=['linenumber'])
# parse content with pygments and add to code_block element
@@ -272,7 +273,8 @@ def code_block_directive(name, arguments, options, content, lineno,
linenos = list(range(lineno, lineno + len(values)))
for chunk, ln in zip(values, linenos)[1:]:
if ln <= total_lines:
- code_block += nodes.inline(fstr % ln, fstr % ln,
+ code_block += nodes.inline(fstr.format(ln),
+ fstr.format(ln),
classes=['linenumber'])
code_block += nodes.Text(chunk, chunk)
lineno += len(values) - 1
@@ -319,8 +321,8 @@ def string_bool(argument):
elif argument.lower() == 'false':
return False
else:
- raise ValueError('"%s" unknown; choose from "True" or "False"' %
- argument)
+ raise ValueError('"{0}" unknown; choose from "True" or "False"'.format(
+ argument))
def csharp_unicodelevel(argument):
@@ -340,7 +342,8 @@ def listings_directive(name, arguments, options, content, lineno,
fname = arguments[0]
options['include'] = os.path.join('listings', fname)
target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = [core.publish_doctree('`%s <%s>`_' % (fname, target))[0]]
+ generated_nodes = [core.publish_doctree('`{0} <{1}>`_'.format(fname,
+ target))[0]]
generated_nodes += code_block_directive(name, [arguments[1]], options,
content, lineno, content_offset,
block_text, state, state_machine)
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile_rest/slides.py
index c9d55f3..f9901f5 100644
--- a/nikola/plugins/compile_rest/slides.py
+++ b/nikola/plugins/compile_rest/slides.py
@@ -77,12 +77,13 @@ class slides(Directive):
options.update(self.options)
options = json.dumps(options)
output = []
- output.append('<script> $(function(){ $("#slides").slides(%s); });'
- '</script>' % options)
+ output.append('<script> $(function(){ $("#slides").slides(' + options +
+ '); });'
+ '</script>')
output.append('<div id="slides" class="slides"><div '
'class="slides_container">')
for image in self.content:
- output.append("""<div><img src="%s"></div>""" % image)
+ output.append("""<div><img src="{0}"></div>""".format(image))
output.append("""</div></div>""")
return [nodes.raw('', '\n'.join(output), format='html')]
diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile_rest/soundcloud.py
new file mode 100644
index 0000000..d47bebf
--- /dev/null
+++ b/nikola/plugins/compile_rest/soundcloud.py
@@ -0,0 +1,32 @@
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+CODE = ("""<iframe width="{width}" height="{height}"
+scrolling="no" frameborder="no"
+src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/"""
+ """{sid}">
+</iframe>""")
+
+
+def soundcloud(name, args, options, content, lineno,
+ contentOffset, blockText, state, stateMachine):
+ """ Restructured text extension for inserting SoundCloud embedded music """
+ string_vars = {
+ 'sid': content[0],
+ 'width': 600,
+ 'height': 160,
+ 'extra': ''
+ }
+ extra_args = content[1:] # Because content[0] is ID
+ extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
+ extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
+ extra_args = dict(extra_args)
+ if 'width' in extra_args:
+ string_vars['width'] = extra_args.pop('width')
+ if 'height' in extra_args:
+ string_vars['height'] = extra_args.pop('height')
+
+ return [nodes.raw('', CODE.format(**string_vars), format='html')]
+
+soundcloud.content = True
+directives.register_directive('soundcloud', soundcloud)
diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile_rest/vimeo.py
index 3eefcc4..34f2a50 100644
--- a/nikola/plugins/compile_rest/vimeo.py
+++ b/nikola/plugins/compile_rest/vimeo.py
@@ -37,8 +37,8 @@ except ImportError:
except ImportError:
json = None
-CODE = """<iframe src="http://player.vimeo.com/video/%(vimeo_id)s"
-width="%(width)s" height="%(height)s"
+CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}"
+width="{width}" height="{height}"
frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen>
</iframe>
"""
@@ -76,8 +76,8 @@ def vimeo(name, args, options, content, lineno, contentOffset, blockText,
if json: # we can attempt to retrieve video attributes from vimeo
try:
- url = ('http://vimeo.com/api/v2/video/%(vimeo_id)s.json' %
- string_vars)
+ url = ('http://vimeo.com/api/v2/video/{vimeo_id}'
+ '.json'.format(**string_vars))
data = requests.get(url).text
video_attributes = json.loads(data)
string_vars['height'] = video_attributes['height']
@@ -86,7 +86,7 @@ def vimeo(name, args, options, content, lineno, contentOffset, blockText,
# fall back to the defaults
pass
- return [nodes.raw('', CODE % string_vars, format='html')]
+ return [nodes.raw('', CODE.format(**string_vars), format='html')]
vimeo.content = True
directives.register_directive('vimeo', vimeo)
diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile_rest/youtube.py
index fe3b28b..30ac000 100644
--- a/nikola/plugins/compile_rest/youtube.py
+++ b/nikola/plugins/compile_rest/youtube.py
@@ -26,9 +26,9 @@ from docutils import nodes
from docutils.parsers.rst import directives
CODE = """\
-<iframe width="%(width)s"
-height="%(height)s"
-src="http://www.youtube.com/embed/%(yid)s?rel=0&amp;hd=1&amp;wmode=transparent"
+<iframe width="{width}"
+height="{height}"
+src="http://www.youtube.com/embed/{yid}?rel=0&amp;hd=1&amp;wmode=transparent"
></iframe>"""
@@ -51,6 +51,6 @@ def youtube(name, args, options, content, lineno,
string_vars['width'] = extra_args.pop('width')
if 'height' in extra_args:
string_vars['height'] = extra_args.pop('height')
- return [nodes.raw('', CODE % (string_vars), format='html')]
+ return [nodes.raw('', CODE.format(**string_vars), format='html')]
youtube.content = True
directives.register_directive('youtube', youtube)
diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile_textile.py
index 7fa4e3f..3ca370d 100644
--- a/nikola/plugins/compile_textile.py
+++ b/nikola/plugins/compile_textile.py
@@ -62,10 +62,10 @@ class CompileTextile(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<notextile> <!--\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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n')
fd.write('--></notextile>\n\n')
diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile_txt2tags.py
index 2446dfd..90372bd 100644
--- a/nikola/plugins/compile_txt2tags.py
+++ b/nikola/plugins/compile_txt2tags.py
@@ -65,10 +65,10 @@ class CompileTextile(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write("\n'''\n<!--\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('.. title: {0}\n'.format(title))
+ fd.write('.. slug: {0}\n'.format(slug))
+ fd.write('.. date: {0}\n'.format(date))
+ fd.write('.. tags: {0}\n'.format(tags))
fd.write('.. link: \n')
fd.write('.. description: \n')
fd.write("-->\n'''\n")
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task_create_bundles.py
index 95f10c2..ad670e1 100644
--- a/nikola/plugins/task_create_bundles.py
+++ b/nikola/plugins/task_create_bundles.py
@@ -77,7 +77,8 @@ class BuildBundles(LateTask):
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
file_dep = [get_asset_path(
- os.path.join(dname, fname), kw['themes'], kw['files_folders'])
+ os.path.join(dname, fname), kw['themes'],
+ kw['files_folders'])
for fname in files
]
file_dep = filter(None, file_dep) # removes missing files
@@ -108,16 +109,17 @@ def get_asset_path(path, themes, files_folders={'files': ''}):
If the asset is not provided by a theme, then it will be checked for
in the FILES_FOLDERS
- >>> get_asset_path('assets/css/rst.css', ['site','default'])
+ >>> get_asset_path('assets/css/rst.css', ['site', 'default'])
'nikola/data/themes/default/assets/css/rst.css'
- >>> get_asset_path('assets/css/theme.css', ['site','default'])
+ >>> get_asset_path('assets/css/theme.css', ['site', 'default'])
'nikola/data/themes/site/assets/css/theme.css'
- >>> get_asset_path('nikola.py',['site','default'],{'nikola':''})
+ >>> get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''})
'nikola/nikola.py'
- >>> get_asset_path('nikola/nikola.py',['site','default'],{'nikola':'nikola'})
+ >>> get_asset_path('nikola/nikola.py', ['site', 'default'],
+ ... {'nikola':'nikola'})
'nikola/nikola.py'
"""
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task_indexes.py
index 757998e..7baf660 100644
--- a/nikola/plugins/task_indexes.py
+++ b/nikola/plugins/task_indexes.py
@@ -79,11 +79,11 @@ class Indexes(Task):
context["nextlink"] = None
context['index_teasers'] = kw['index_teasers']
if i > 1:
- context["prevlink"] = "index-%s.html" % (i - 1)
+ context["prevlink"] = "index-{0}.html".format(i - 1)
if i == 1:
context["prevlink"] = "index.html"
if i < num_pages - 1:
- context["nextlink"] = "index-%s.html" % (i + 1)
+ context["nextlink"] = "index-{0}.html".format(i + 1)
context["permalink"] = self.site.link("index", i, lang)
output_name = os.path.join(
kw['output_folder'], self.site.path("index", i,
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task_redirect.py
index b133948..e440c30 100644
--- a/nikola/plugins/task_redirect.py
+++ b/nikola/plugins/task_redirect.py
@@ -71,5 +71,6 @@ def create_redirect(src, dst):
except:
pass
with codecs.open(src, "wb+", "utf8") as fd:
- fd.write('<head><meta http-equiv="refresh" content="0; '
- 'url=%s"></head>' % dst)
+ fd.write('<!DOCTYPE html><head><title>Redirecting...</title>'
+ '<meta http-equiv="refresh" content="0; '
+ 'url={0}"></head>'.format(dst))
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
index e69a457..0880e3e 100644
--- a/nikola/plugins/task_render_galleries.py
+++ b/nikola/plugins/task_render_galleries.py
@@ -136,7 +136,6 @@ class Galleries(Task):
# Sort by date
image_list.sort(key=lambda a: self.image_date(a))
image_name_list = [os.path.basename(x) for x in image_list]
-
thumbs = []
# Do thumbnails and copy originals
for img, img_name in list(zip(image_list, image_name_list)):
@@ -187,7 +186,7 @@ class Galleries(Task):
output_gallery, ".thumbnail".join([fname, ext]))
excluded_dest_path = os.path.join(output_gallery, img_name)
yield {
- 'basename': str('render_galleries'),
+ 'basename': str('render_galleries_clean'),
'name': excluded_thumb_dest_path.encode('utf8'),
'file_dep': [exclude_path],
#'targets': [excluded_thumb_dest_path],
@@ -198,7 +197,7 @@ class Galleries(Task):
'uptodate': [utils.config_changed(kw)],
}
yield {
- 'basename': str('render_galleries'),
+ 'basename': str('render_galleries_clean'),
'name': excluded_dest_path.encode('utf8'),
'file_dep': [exclude_path],
#'targets': [excluded_dest_path],
@@ -214,9 +213,9 @@ class Galleries(Task):
context["title"] = os.path.basename(gallery_path)
context["description"] = kw["blog_description"]
if kw['use_filename_as_title']:
- img_titles = ['id="%s" alt="%s" title="%s"' %
- (fn[:-4], fn[:-4], utils.unslugify(fn[:-4]))
- for fn in image_name_list]
+ img_titles = ['id="{0}" alt="{1}" title="{2}"'.format(
+ fn[:-4], fn[:-4], utils.unslugify(fn[:-4])) for fn
+ in image_name_list]
else:
img_titles = [''] * len(image_name_list)
context["images"] = list(zip(image_name_list, thumbs, img_titles))
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task_render_listings.py
index a899f10..b115a2f 100644
--- a/nikola/plugins/task_render_listings.py
+++ b/nikola/plugins/task_render_listings.py
@@ -50,29 +50,32 @@ class Listings(Task):
# 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)
- print("CRUMBSINOUT", in_name, out_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]))] + ['.', '#']
- crumbs = utils.get_crumbs(os.path.relpath(out_name, kw['output_folder']), is_file=True)
+ def render_listing(in_name, out_name, folders=[], files=[]):
+ if in_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)
+ else:
+ code = ''
+ title = ''
+ crumbs = utils.get_crumbs(os.path.relpath(out_name,
+ kw['output_folder']),
+ is_file=True)
context = {
'code': code,
'title': title,
'crumbs': crumbs,
'lang': kw['default_lang'],
+ 'folders': folders,
+ 'files': files,
'description': title,
}
self.site.render_template('listing.tmpl', out_name.encode('utf8'),
@@ -80,12 +83,27 @@ class Listings(Task):
flag = True
template_deps = self.site.template_system.template_deps('listing.tmpl')
for root, dirs, files in os.walk(kw['listings_folder']):
+ flag = False
# Render all files
+ out_name = os.path.join(
+ kw['output_folder'],
+ root, 'index.html'
+ )
+ yield {
+ 'basename': self.name,
+ 'name': out_name.encode('utf8'),
+ 'file_dep': template_deps,
+ 'targets': [out_name],
+ 'actions': [(render_listing, [None, out_name, dirs, files])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(
+ self.site.config['GLOBAL_CONTEXT'])]
+ }
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'],
diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task_render_rss.py
index fb35843..9ce1d63 100644
--- a/nikola/plugins/task_render_rss.py
+++ b/nikola/plugins/task_render_rss.py
@@ -39,7 +39,7 @@ class RenderRSS(Task):
"translations": self.site.config["TRANSLATIONS"],
"filters": self.site.config["FILTERS"],
"blog_title": self.site.config["BLOG_TITLE"],
- "blog_url": self.site.config["BLOG_URL"],
+ "site_url": self.site.config["SITE_URL"],
"blog_description": self.site.config["BLOG_DESCRIPTION"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
@@ -59,7 +59,7 @@ class RenderRSS(Task):
'file_dep': deps,
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"], kw["blog_url"],
+ (lang, kw["blog_title"], kw["site_url"],
kw["blog_description"], posts, output_name,
kw["rss_teasers"]))],
'clean': True,
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task_render_tags.py
index a561a81..744f0cb 100644
--- a/nikola/plugins/task_render_tags.py
+++ b/nikola/plugins/task_render_tags.py
@@ -22,7 +22,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#from __future__ import unicode_literals
+from __future__ import unicode_literals
import codecs
import json
import os
@@ -42,7 +42,7 @@ class RenderTags(Task):
kw = {
"translations": self.site.config["TRANSLATIONS"],
"blog_title": self.site.config["BLOG_TITLE"],
- "blog_url": self.site.config["BLOG_URL"],
+ "site_url": self.site.config["SITE_URL"],
"blog_description": self.site.config["BLOG_DESCRIPTION"],
"messages": self.site.MESSAGES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
@@ -56,6 +56,8 @@ class RenderTags(Task):
self.site.scan_posts()
+ yield self.list_tags_page(kw)
+
if not self.site.posts_per_tag:
yield {'basename': str(self.name), 'actions': []}
return
@@ -73,8 +75,6 @@ class RenderTags(Task):
else:
yield self.tag_page_as_list(tag, lang, post_list, kw)
- yield self.list_tags_page(kw)
-
# Tag cloud json file
tag_cloud_data = {}
for tag, posts in self.site.posts_per_tag.items():
@@ -136,7 +136,7 @@ class RenderTags(Task):
"""Given tag, n, returns a page name."""
name = self.site.path("tag", tag, lang)
if i:
- name = name.replace('.html', '-%s.html' % i)
+ name = name.replace('.html', '-{0}.html'.format(i))
return name
# FIXME: deduplicate this with render_indexes
@@ -152,11 +152,11 @@ class RenderTags(Task):
# 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)))
+ """{0} ({1})" href="{2}">""".format(
+ 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))
+ output_name = os.path.join(kw['output_folder'],
+ page_name(tag, i, lang))
output_name = output_name.encode('utf8')
context["title"] = kw["messages"][lang][
"Posts about %s"] % tag
@@ -231,8 +231,8 @@ class RenderTags(Task):
'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,
+ (lang, "{0} ({1})".format(kw["blog_title"], tag),
+ kw["site_url"], kw["blog_description"], post_list,
output_name, kw["rss_teasers"]))],
'clean': True,
'uptodate': [utils.config_changed(kw)],
diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
index 96b9dbd..9d89070 100644
--- a/nikola/plugins/task_sitemap/__init__.py
+++ b/nikola/plugins/task_sitemap/__init__.py
@@ -49,7 +49,8 @@ class Sitemap(LateTask):
return
"""Generate Google sitemap."""
kw = {
- "blog_url": self.site.config["BLOG_URL"],
+ "base_url": self.site.config["BASE_URL"],
+ "site_url": self.site.config["SITE_URL"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
}
output_path = os.path.abspath(kw['output_folder'])
@@ -59,18 +60,14 @@ class Sitemap(LateTask):
# Generate config
config_data = """<?xml version="1.0" encoding="UTF-8"?>
<site
- base_url="%s"
- store_into="%s"
+ base_url="{0}"
+ store_into="{1}"
verbose="1" >
- <directory path="%s" url="%s" />
+ <directory path="{2}" url="{3}" />
<filter action="drop" type="wildcard" pattern="*~" />
<filter action="drop" type="regexp" pattern="/\.[^/]*" />
- </site>""" % (
- kw["blog_url"],
- sitemap_path,
- output_path,
- kw["blog_url"],
- )
+ </site>""".format(kw["site_url"], sitemap_path, output_path,
+ kw["base_url"])
config_file = tempfile.NamedTemporaryFile(delete=False)
config_file.write(config_data.encode('utf8'))
config_file.close()
@@ -82,14 +79,15 @@ class Sitemap(LateTask):
0)
else:
sitemap.Generate()
- sitemap_gen.output.Log('Number of errors: %d' %
- sitemap_gen.output.num_errors, 1)
- sitemap_gen.output.Log('Number of warnings: %d' %
- sitemap_gen.output.num_warns, 1)
+ sitemap_gen.output.Log('Number of errors: {0}'.format(
+ sitemap_gen.output.num_errors), 1)
+ sitemap_gen.output.Log('Number of warnings: {0}'.format(
+ sitemap_gen.output.num_warns), 1)
os.unlink(config_file.name)
yield {
"basename": "sitemap",
+ "name": os.path.join(kw['output_folder'], "sitemap.xml.gz"),
"targets": [sitemap_path],
"actions": [(sitemap,)],
"uptodate": [config_changed(kw)],
diff --git a/nikola/plugins/task_sitemap/sitemap_gen.py b/nikola/plugins/task_sitemap/sitemap_gen.py
index a877c24..898325a 100644
--- a/nikola/plugins/task_sitemap/sitemap_gen.py
+++ b/nikola/plugins/task_sitemap/sitemap_gen.py
@@ -90,7 +90,7 @@ if sys.version_info[0] == 3:
unichr = chr
else:
bytes_str = str
- unicode_str = unicode
+ unicode_str = unicode # NOQA
# Text encodings
ENC_ASCII = 'ASCII'
diff --git a/nikola/post.py b/nikola/post.py
index 809e5b7..5060583 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -27,32 +27,43 @@ from __future__ import unicode_literals, print_function
import codecs
import os
+import re
+import sys
+import string
+import unidecode
import lxml.html
-from . import utils
+from .utils import to_datetime, slugify
__all__ = ['Post']
+TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE)
+
class Post(object):
"""Represents a blog post or web page."""
- def __init__(self, source_path, cache_folder, destination, use_in_feeds,
- translations, default_lang, blog_url, messages, template_name,
- file_metadata_regexp=None):
+ def __init__(
+ self, source_path, cache_folder, destination, use_in_feeds,
+ translations, default_lang, base_url, messages, template_name,
+ file_metadata_regexp=None, tzinfo=None
+ ):
"""Initialize post.
- The base path is the .txt post file. From it we calculate
+ The source path is the .txt post file. From it we calculate
the meta file, as well as any translations available, and
the .html fragment file path.
"""
self.translated_to = set([default_lang])
+ self.tags = ''
+ self.date = None
self.prev_post = None
self.next_post = None
- self.blog_url = blog_url
+ self.base_url = base_url
self.is_draft = False
+ self.is_mathjax = False
self.source_path = source_path # posts/blah.txt
self.post_name = os.path.splitext(source_path)[0] # posts/blah
# cache/posts/blah.html
@@ -63,24 +74,27 @@ class Post(object):
self.default_lang = default_lang
self.messages = messages
self.template_name = template_name
- if os.path.isfile(self.metadata_path):
- with codecs.open(self.metadata_path, "r", "utf8") as meta_file:
- meta_data = meta_file.readlines()
- while len(meta_data) < 6:
- meta_data.append("")
- (default_title, default_pagename, self.date, self.tags,
- self.link, default_description) = [x.strip() for x in
- meta_data][:6]
- else:
- (default_title, default_pagename, self.date, self.tags,
- self.link, default_description) = utils.get_meta(
- self.source_path, file_metadata_regexp)
+ self.meta = get_meta(self, file_metadata_regexp)
+
+ default_title = self.meta.get('title', '')
+ default_pagename = self.meta.get('slug', '')
+ default_description = self.meta.get('description', '')
+
+ for k, v in self.meta.items():
+ if k not in ['title', 'slug', 'description']:
+ if sys.version_info[0] == 2:
+ setattr(self, unidecode.unidecode(unicode(k)), v) # NOQA
+ else:
+ setattr(self, k, v)
if not default_title or not default_pagename or not self.date:
- raise OSError("You must set a title and slug and date! [%s]" %
- source_path)
+ raise OSError("You must set a title (found '{0}'), a slug (found "
+ "'{1}') and a date (found '{2}')! [in file "
+ "{3}]".format(default_title, default_pagename,
+ self.date, source_path))
- self.date = utils.to_datetime(self.date)
+ # If timezone is set, build localized datetime.
+ self.date = to_datetime(self.date, tzinfo)
self.tags = [x.strip() for x in self.tags.split(',')]
self.tags = [_f for _f in self.tags if _f]
@@ -89,45 +103,30 @@ class Post(object):
self.is_draft = 'draft' in self.tags
self.tags = [t for t in self.tags if t != 'draft']
+ # If mathjax is a tag, then enable mathjax rendering support
+ self.is_mathjax = 'mathjax' in self.tags
+
self.pagenames = {}
self.titles = {}
self.descriptions = {}
- # Load internationalized titles
- # TODO: this has gotten much too complicated. Rethink.
+
+ # Load internationalized metadata
for lang in translations:
if lang == default_lang:
self.titles[lang] = default_title
self.pagenames[lang] = default_pagename
self.descriptions[lang] = default_description
else:
- metadata_path = self.metadata_path + "." + lang
- source_path = self.source_path + "." + lang
- if os.path.isfile(source_path):
+ if os.path.isfile(self.source_path + "." + lang):
self.translated_to.add(lang)
- try:
- if os.path.isfile(metadata_path):
- with codecs.open(
- metadata_path, "r", "utf8") as meta_file:
- meta_data = [x.strip() for x in
- meta_file.readlines()]
- while len(meta_data) < 6:
- meta_data.append("")
- self.titles[lang] = meta_data[0] or default_title
- self.pagenames[lang] = meta_data[1] or\
- default_pagename
- self.descriptions[lang] = meta_data[5] or\
- default_description
- else:
- ttitle, ppagename, tmp1, tmp2, tmp3, ddescription = \
- utils.get_meta(source_path, file_metadata_regexp)
- self.titles[lang] = ttitle or default_title
- self.pagenames[lang] = ppagename or default_pagename
- self.descriptions[lang] = ddescription or\
- default_description
- except:
- self.titles[lang] = default_title
- self.pagenames[lang] = default_pagename
- self.descriptions[lang] = default_description
+
+ meta = self.meta.copy()
+ meta.update(get_meta(self, file_metadata_regexp, lang))
+
+ # FIXME this only gets three pieces of metadata from the i18n files
+ self.titles[lang] = meta.get('title', default_title)
+ self.pagenames[lang] = meta.get('slug', default_pagename)
+ self.descriptions[lang] = meta.get('description', default_description)
def title(self, lang):
"""Return localized title."""
@@ -164,12 +163,12 @@ class Post(object):
"""Return path to the translation's file, or to the original."""
file_name = self.base_path
if lang != self.default_lang:
- file_name_lang = file_name + ".%s" % lang
+ file_name_lang = '.'.join((file_name, lang))
if os.path.exists(file_name_lang):
file_name = file_name_lang
return file_name
- def text(self, lang, teaser_only=False):
+ def text(self, lang, teaser_only=False, strip_html=False):
"""Read the post file for that language and return its contents"""
file_name = self._translated_file_path(lang)
@@ -177,22 +176,30 @@ class Post(object):
data = post_file.read()
if data:
- data = lxml.html.make_links_absolute(data, self.permalink())
+ data = lxml.html.make_links_absolute(data, self.permalink(lang=lang))
if data and teaser_only:
e = lxml.html.fromstring(data)
teaser = []
+ teaser_str = self.messages[lang]["Read more"] + '...'
flag = False
for elem in e:
elem_string = lxml.html.tostring(elem).decode('utf8')
- if '<!-- TEASER_END -->' in elem_string.upper():
+ match = TEASER_REGEXP.match(elem_string)
+ if match:
flag = True
+ if match.group(2):
+ teaser_str = match.group(2)
break
teaser.append(elem_string)
if flag:
- teaser.append('<p><a href="%s">%s...</a></p>' %
- (self.permalink(lang),
- self.messages[lang]["Read more"]))
+ teaser.append('<p><a href="{0}">{1}</a></p>'.format(
+ self.permalink(lang), teaser_str))
data = ''.join(teaser)
+
+ if data and strip_html:
+ content = lxml.html.fromstring(data)
+ data = content.text_content().strip() # No whitespace wanted.
+
return data
def destination_path(self, lang, extension='.html'):
@@ -206,9 +213,9 @@ class Post(object):
pieces = list(os.path.split(self.translations[lang]))
pieces += list(os.path.split(self.folder))
pieces += [self.pagenames[lang] + extension]
- pieces = [_f for _f in pieces if _f]
+ pieces = [_f for _f in pieces if _f and _f != '.']
if absolute:
- pieces = [self.blog_url] + pieces
+ pieces = [self.base_url] + pieces
else:
pieces = [""] + pieces
link = "/".join(pieces)
@@ -216,3 +223,166 @@ class Post(object):
def source_ext(self):
return os.path.splitext(self.source_path)[1]
+
+# Code that fetches metadata from different places
+
+
+def re_meta(line, match=None):
+ """re.compile for meta"""
+ if match:
+ reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match)))
+ else:
+ reStr = re.compile('^\.\. (.*?): (.*)')
+ result = reStr.findall(line.strip())
+ if match and result:
+ return (match, result[0])
+ elif not match and result:
+ return (result[0][0], result[0][1].strip())
+ else:
+ return (None,)
+
+
+def _get_metadata_from_filename_by_regex(filename, metadata_regexp):
+ """
+ Tries to ried the metadata from the filename based on the given re.
+ This requires to use symbolic group names in the pattern.
+
+ The part to read the metadata from the filename based on a regular
+ expression is taken from Pelican - pelican/readers.py
+ """
+ match = re.match(metadata_regexp, filename)
+ meta = {}
+
+ if match:
+ # .items() for py3k compat.
+ for key, value in match.groupdict().items():
+ meta[key.lower()] = value # metadata must be lowercase
+
+ return meta
+
+
+def get_metadata_from_file(source_path, lang=None):
+ """Extracts metadata from the file itself, by parsing contents."""
+ try:
+ if lang:
+ source_path = "{0}.{1}".format(source_path, lang)
+ with codecs.open(source_path, "r", "utf8") as meta_file:
+ meta_data = [x.strip() for x in meta_file.readlines()]
+ return _get_metadata_from_file(meta_data)
+ except Exception: # The file may not exist, for multilingual sites
+ return {}
+
+
+def _get_metadata_from_file(meta_data):
+ """Parse file contents and obtain metadata.
+
+ >>> g = _get_metadata_from_file
+ >>> list(g([]).values())
+ []
+ >>> str(g(["FooBar","======"])["title"])
+ 'FooBar'
+ >>> str(g(["#FooBar"])["title"])
+ 'FooBar'
+ >>> str(g([".. title: FooBar"])["title"])
+ 'FooBar'
+ >>> 'title' in g(["",".. title: FooBar"])
+ False
+
+ """
+ meta = {}
+
+ re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#')))
+ # Assuming rst titles are going to be at least 4 chars long
+ # otherwise this detects things like ''' wich breaks other markups.
+ re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape(
+ string.punctuation)))
+
+ for i, line in enumerate(meta_data):
+ if not line:
+ break
+ if 'title' not in meta:
+ match = re_meta(line, 'title')
+ if match[0]:
+ meta['title'] = match[1]
+ if 'title' not in meta:
+ if re_rst_title.findall(line) and i > 0:
+ meta['title'] = meta_data[i - 1].strip()
+ if 'title' not in meta:
+ if re_md_title.findall(line):
+ meta['title'] = re_md_title.findall(line)[0]
+
+ match = re_meta(line)
+ if match[0]:
+ meta[match[0]] = match[1]
+
+ return meta
+
+
+def get_metadata_from_meta_file(path, lang=None):
+ """Takes a post path, and gets data from a matching .meta file."""
+ meta_path = os.path.splitext(path)[0] + '.meta'
+ if lang:
+ meta_path += '.' + lang
+ if os.path.isfile(meta_path):
+ with codecs.open(meta_path, "r", "utf8") as meta_file:
+ meta_data = meta_file.readlines()
+ while len(meta_data) < 6:
+ meta_data.append("")
+ (title, slug, date, tags, link, description) = [
+ x.strip() for x in meta_data][:6]
+
+ meta = {}
+
+ if title:
+ meta['title'] = title
+ if slug:
+ meta['slug'] = slug
+ if date:
+ meta['date'] = date
+ if tags:
+ meta['tags'] = tags
+ if link:
+ meta['link'] = link
+ if description:
+ meta['description'] = description
+
+ return meta
+ else:
+ return {}
+
+
+def get_meta(post, file_metadata_regexp=None, lang=None):
+ """Get post's meta from source.
+
+ If ``file_metadata_regexp`` is given it will be tried to read
+ metadata from the filename.
+ If any metadata is then found inside the file the metadata from the
+ file will override previous findings.
+ """
+ meta = {}
+
+ meta.update(get_metadata_from_meta_file(post.metadata_path, lang))
+
+ if meta:
+ return meta
+
+ if file_metadata_regexp is not None:
+ meta.update(_get_metadata_from_filename_by_regex(post.source_path,
+ file_metadata_regexp))
+
+ meta.update(get_metadata_from_file(post.source_path, lang))
+
+ if lang is None:
+ # Only perform these checks for the default language
+
+ if 'slug' not in meta:
+ # If no slug is found in the metadata use the filename
+ meta['slug'] = slugify(os.path.splitext(
+ os.path.basename(post.source_path))[0])
+
+ if 'title' not in meta:
+ # If no title is found, use the filename without extension
+ meta['title'] = os.path.splitext(
+ os.path.basename(post.source_path))[0]
+
+ return meta
diff --git a/nikola/utils.py b/nikola/utils.py
index eeb0c45..5589d68 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (c) 2012 Roberto Alsina y otros.
# Permission is hereby granted, free of charge, to any
@@ -33,7 +34,6 @@ import re
import codecs
import json
import shutil
-import string
import subprocess
import sys
from zipfile import ZipFile as zip
@@ -42,6 +42,7 @@ try:
except ImportError:
pass
+import pytz
if sys.version_info[0] == 3:
# Python 3
@@ -50,7 +51,7 @@ if sys.version_info[0] == 3:
unichr = chr
else:
bytes_str = str
- unicode_str = unicode
+ unicode_str = unicode # NOQA
from doit import tools
from unidecode import unidecode
@@ -58,9 +59,8 @@ from unidecode import unidecode
import PyRSS2Gen as rss
__all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree',
- 'generic_rss_renderer',
- 'copy_file', 'slugify', 'unslugify', 'get_meta', 'to_datetime',
- 'apply_filters', 'config_changed', 'get_crumbs']
+ 'generic_rss_renderer', 'copy_file', 'slugify', 'unslugify',
+ 'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs']
class CustomEncoder(json.JSONEncoder):
@@ -86,13 +86,13 @@ class config_changed(tools.config_changed):
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),))
+ raise Exception('Invalid type of config_changed parameter -- got '
+ '{0}, must be string or dict'.format(type(
+ self.config)))
def __repr__(self):
- return "Change with config: %s" % json.dumps(
- self.config, cls=CustomEncoder)
+ return "Change with config: {0}".format(json.dumps(self.config,
+ cls=CustomEncoder))
def get_theme_path(theme):
@@ -107,111 +107,7 @@ def get_theme_path(theme):
'data', 'themes', theme)
if os.path.isdir(dir_name):
return dir_name
- raise Exception("Can't find theme '%s'" % theme)
-
-
-def re_meta(line, match):
- """re.compile for meta"""
- reStr = re.compile('^%s(.*)' % re.escape(match))
- result = reStr.findall(line)
- if result:
- return result[0].strip()
- else:
- return ''
-
-
-def _get_metadata_from_filename_by_regex(filename, metadata_regexp):
- """
- Tries to ried the metadata from the filename based on the given re.
- This requires to use symbolic group names in the pattern.
-
- The part to read the metadata from the filename based on a regular
- expression is taken from Pelican - pelican/readers.py
- """
- title = slug = date = tags = link = description = ''
- match = re.match(metadata_regexp, filename)
- if match:
- # .items() for py3k compat.
- for key, value in match.groupdict().items():
- key = key.lower() # metadata must be lowercase
-
- if key == 'title':
- title = value
- if key == 'slug':
- slug = value
- if key == 'date':
- date = value
- if key == 'tags':
- tags = value
- if key == 'link':
- link = value
- if key == 'description':
- description = value
-
- return (title, slug, date, tags, link, description)
-
-
-def _get_metadata_from_file(source_path, title='', slug='', date='', tags='',
- link='', description=''):
- re_md_title = re.compile(r'^%s([^%s].*)' %
- (re.escape('#'), re.escape('#')))
- # Assuming rst titles are going to be at least 4 chars long
- # otherwise this detects things like ''' wich breaks other markups.
- re_rst_title = re.compile(r'^([%s]{4,})' % re.escape(string.punctuation))
-
- with codecs.open(source_path, "r", "utf8") as meta_file:
- meta_data = meta_file.readlines(15)
-
- for i, meta in enumerate(meta_data):
- if not title:
- title = re_meta(meta, '.. title:')
- if not title:
- if re_rst_title.findall(meta) and i > 0:
- title = meta_data[i - 1].strip()
- if not title:
- if re_md_title.findall(meta):
- title = re_md_title.findall(meta)[0]
- if not slug:
- slug = re_meta(meta, '.. slug:')
- if not date:
- date = re_meta(meta, '.. date:')
- if not tags:
- tags = re_meta(meta, '.. tags:')
- if not link:
- link = re_meta(meta, '.. link:')
- if not description:
- description = re_meta(meta, '.. description:')
-
- return (title, slug, date, tags, link, description)
-
-
-def get_meta(source_path, file_metadata_regexp=None):
- """Get post's meta from source.
-
- If ``file_metadata_regexp`` ist given it will be tried to read
- metadata from the filename.
- If any metadata is then found inside the file the metadata from the
- file will override previous findings.
- """
- title = slug = date = tags = link = description = ''
-
- if not (file_metadata_regexp is None):
- (title, slug, date, tags, link,
- description) = _get_metadata_from_filename_by_regex(
- source_path, file_metadata_regexp)
-
- (title, slug, date, tags, link, description) = _get_metadata_from_file(
- source_path, title, slug, date, tags, link, description)
-
- if not slug:
- # If no slug is found in the metadata use the filename
- slug = slugify(os.path.splitext(os.path.basename(source_path))[0])
-
- if not title:
- # If no title is found, use the filename without extension
- title = os.path.splitext(os.path.basename(source_path))[0]
-
- return (title, slug, date, tags, link, description)
+ raise Exception("Can't find theme '{0}'".format(theme))
def get_template_engine(themes):
@@ -267,8 +163,8 @@ def load_messages(themes, translations):
sorted(english.MESSAGES.keys()) and \
lang not in warned:
# FIXME: get real logging in place
- print("Warning: Incomplete translation for language '%s'." %
- lang)
+ print("Warning: Incomplete translation for language "
+ "'{0}'.".format(lang))
warned.append(lang)
messages[lang].update(english.MESSAGES)
messages[lang].update(translation.MESSAGES)
@@ -325,7 +221,9 @@ def generic_rss_renderer(lang, title, link, description, timeline, output_path,
'link': post.permalink(lang, absolute=True),
'description': post.text(lang, teaser_only=rss_teasers),
'guid': post.permalink(lang, absolute=True),
- 'pubDate': post.date,
+ # PyRSS2Gen's pubDate is GMT time.
+ 'pubDate': (post.date if post.date.tzinfo is None else
+ post.date.astimezone(pytz.timezone('UTC'))),
}
items.append(rss.RSSItem(**args))
rss_obj = rss.RSS2(
@@ -386,6 +284,16 @@ def slugify(value):
and converts spaces to hyphens.
From Django's "django/template/defaultfilters.py".
+
+ >>> slugify('\xe1\xe9\xed.\xf3\xfa')
+ 'aeiou'
+
+ >>> slugify('foo/bar')
+ 'foobar'
+
+ >>> slugify('foo bar')
+ 'foo-bar'
+
"""
value = unidecode(value)
# WARNING: this may not be python2/3 equivalent
@@ -418,22 +326,23 @@ def extract_all(zipfile):
namelist = z.namelist()
for f in namelist:
if f.endswith('/') and '..' in f:
- raise UnsafeZipException(
- 'The zip file contains ".." and is not safe to expand.')
+ raise UnsafeZipException('The zip file contains ".." and is '
+ 'not safe to expand.')
for f in namelist:
if f.endswith('/'):
if not os.path.isdir(f):
try:
os.makedirs(f)
except:
- raise OSError("mkdir '%s' error!" % f)
+ raise OSError("Failed making {0} directory "
+ "tree!".format(f))
else:
z.extract(f)
os.chdir(pwd)
# From https://github.com/lepture/liquidluck/blob/develop/liquidluck/utils.py
-def to_datetime(value):
+def to_datetime(value, tzinfo=None):
if isinstance(value, datetime.datetime):
return value
supported_formats = [
@@ -451,10 +360,14 @@ def to_datetime(value):
]
for format in supported_formats:
try:
- return datetime.datetime.strptime(value, format)
+ dt = datetime.datetime.strptime(value, format)
+ if tzinfo is None:
+ return dt
+ # Build a localized time by using a given timezone.
+ return tzinfo.localize(dt)
except ValueError:
pass
- raise ValueError('Unrecognized date/time: %r' % value)
+ raise ValueError('Unrecognized date/time: {0!r}'.format(value))
def apply_filters(task, filters):
diff --git a/requirements-3.txt b/requirements-3.txt
index b5f9d95..69f6a1c 100644
--- a/requirements-3.txt
+++ b/requirements-3.txt
@@ -1,15 +1,6 @@
-doit>=0.20.0
-pygments
-https://github.com/fluggo/Pillow/archive/master.zip
-docutils
-mako>=0.6
-unidecode
-lxml
-yapsy
+https://github.com/python-imaging/Pillow/archive/master.zip
mock>=1.0.0
requests
markdown
Jinja2
-PyRSS2Gen
bbcode
-flake8
diff --git a/requirements.txt b/requirements.txt
index 923c2ed..f7134c0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,5 @@
-doit>=0.20.0
-pygments
-pillow
-docutils
-mako>=0.6
-unidecode
-lxml
-configparser
-yapsy
mock>=1.0.0
requests
markdown
Jinja2
-PyRSS2Gen
bbcode
-flake8
diff --git a/scripts/nikola b/scripts/nikola
index 2143e9e..a3e5d72 100755
--- a/scripts/nikola
+++ b/scripts/nikola
@@ -31,39 +31,7 @@ else:
# LIBDIR trick end (marker for removal on platforms that don't need it)
-import nikola
-
-
-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")
+from nikola import main
if __name__ == "__main__":
-
- try:
- sys.path.append('')
- import conf
- config = conf.__dict__
- except ImportError:
- config = {}
-
- site = nikola.Nikola(**config)
-
- if len(sys.argv) < 2:
- sys.argv.append('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("Unknown command: %s" % cmd_name)
- print_help(site)
+ sys.exit(main.main(sys.argv[1:]))
diff --git a/setup.py b/setup.py
index 2ff115f..cdb8ac0 100644..100755
--- a/setup.py
+++ b/setup.py
@@ -38,8 +38,8 @@ dependencies = [
'unidecode',
'lxml',
'yapsy',
- 'mock>=1.0.0',
'PyRSS2Gen',
+ 'pytz',
]
if sys.version_info[0] == 2:
@@ -182,7 +182,7 @@ def find_package_data(
return out
setup(name='Nikola',
- version='5.3',
+ version='5.4.2',
description='Static blog/website generator',
author='Roberto Alsina and others',
author_email='ralsina@netmanagers.com.ar',
diff --git a/tests/data/translated_titles/conf.py b/tests/data/translated_titles/conf.py
new file mode 100644
index 0000000..69c7bc7
--- /dev/null
+++ b/tests/data/translated_titles/conf.py
@@ -0,0 +1,397 @@
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import time
+
+##############################################
+# Configuration, please edit
+##############################################
+
+
+# Data about this site
+BLOG_AUTHOR = "Your Name"
+BLOG_TITLE = "Demo Site"
+# This is the main URL for your site. It will be used
+# in a prominent link
+SITE_URL = "http://nikola.ralsina.com.ar"
+# This is the URL where nikola's output will be deployed.
+# If not set, defaults to SITE_URL
+# BASE_URL = "http://nikola.ralsina.com.ar
+BLOG_EMAIL = "joe@demo.site"
+BLOG_DESCRIPTION = "This is a demo site for Nikola."
+
+# Nikola is multilingual!
+#
+# Currently supported languages are:
+# English -> en
+# Greek -> gr
+# German -> de
+# French -> fr
+# Polish -> pl
+# Russian -> ru
+# Spanish -> es
+# Italian -> it
+# Simplified Chinese -> zh-cn
+#
+# If you want to use Nikola with a non-supported language you have to provide
+# a module containing the necessary translations
+# (p.e. look at the modules at: ./nikola/data/themes/default/messages/fr.py).
+# If a specific post is not translated to a language, then the version
+# in the default language will be shown instead.
+
+# What is the default language?
+DEFAULT_LANG = "en"
+
+# What other languages do you have?
+# The format is {"translationcode" : "path/to/translation" }
+# the path will be used as a prefix for the generated pages location
+TRANSLATIONS = {
+ "en": "",
+ # Example for another language:
+ "es": "./es",
+}
+
+# Links for the sidebar / navigation bar.
+# You should provide a key-value pair for each used language.
+SIDEBAR_LINKS = {
+ DEFAULT_LANG: (
+ ('/archive.html', 'Archives'),
+ ('/categories/index.html', 'Tags'),
+ ),
+ "es": ()
+}
+
+
+##############################################
+# Below this point, everything is optional
+##############################################
+
+
+# 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),
+)
+
+# 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'
+
+# A mapping of languages to file-extensions that represent that language.
+# Feel free to add or delete extensions to any list, but don't add any new
+# compilers unless you write the interface for it yourself.
+#
+# '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'),
+ "textile": ('.textile',),
+ "txt2tags": ('.t2t',),
+ "bbcode": ('.bb',),
+ "wiki": ('.wiki',),
+ "ipynb": ('.ipynb',),
+ "html": ('.html', '.htm')
+}
+
+# Create by default posts in one file format?
+# Set to False for two-file posts, with separate metadata.
+# ONE_FILE_POSTS = True
+
+# Paths for different autogenerated bits. These are combined with the
+# translation paths.
+
+# Final locations are:
+# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags)
+# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag)
+# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag)
+# TAG_PATH = "categories"
+
+# If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain
+# the posts themselves. If set to False, it will be just a list of links.
+# TAG_PAGES_ARE_INDEXES = True
+
+# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html
+# INDEX_PATH = ""
+# Final locations for the archives are:
+# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME
+# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
+# ARCHIVE_PATH = ""
+# ARCHIVE_FILENAME = "archive.html"
+# Final locations are:
+# output / TRANSLATION[lang] / RSS_PATH / rss.xml
+# RSS_PATH = ""
+
+# Slug the Tag URL easier for users to type, special characters are
+# often removed or replaced as well.
+# SLUG_TAG_PATH = True
+
+# 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 = []
+
+# Commands to execute to deploy. Can be anything, for example,
+# you may use rsync:
+# "rsync -rav output/* joe@my.site:/srv/www/site"
+# And then do a backup, or ping pingomatic.
+# To do manual deployment, set it to []
+# DEPLOY_COMMANDS = []
+
+# Where the output site should be located
+# If you don't use an absolute path, it will be considered as relative
+# to the location of conf.py
+# OUTPUT_FOLDER = 'output'
+
+# where the "cache" of partial generated content should be located
+# default: 'cache'
+# CACHE_FOLDER = 'cache'
+
+# Filters to apply to the output.
+# A directory where the keys are either: a file extensions, or
+# a tuple of file extensions.
+#
+# And the value is a list of commands to be applied in order.
+#
+# Each command must be either:
+#
+# A string containing a '%s' which will
+# be replaced with a filename. The command *must* produce output
+# in place.
+#
+# Or:
+#
+# A python callable, which will be called with the filename as
+# argument.
+#
+# By default, there are no filters.
+# FILTERS = {
+# ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
+# }
+
+# Create a gzipped copy of each generated file. Cheap server-side optimization.
+# GZIP_FILES = False
+# File extensions that will be compressed
+# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json')
+
+# #############################################################################
+# Image Gallery Options
+# #############################################################################
+
+# 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
+
+# #############################################################################
+# HTML fragments and diverse things that are used by the templates
+# #############################################################################
+
+# Data about post-per-page indexes
+# INDEXES_TITLE = "" # If this is empty, the default is BLOG_TITLE
+# INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d' translated
+
+# Name of the theme to use. Themes are located in themes/theme_name
+# THEME = 'site'
+
+# If you use 'site-reveal' theme you can select several subthemes
+# THEME_REVEAL_CONGIF_SUBTHEME = 'sky' # You can also use: beige/serif/simple/night/default
+
+# Again, if you use 'site-reveal' theme you can select several transitions between the slides
+# THEME_REVEAL_CONGIF_TRANSITION = 'cube' # You can also use: page/concave/linear/none/default
+
+# date format used to display post dates. (str used by datetime.datetime.strftime)
+# DATE_FORMAT = '%Y-%m-%d %H:%M'
+
+# FAVICONS contains (name, file, size) tuples.
+# Used for create favicon link like this:
+# <link rel="name" href="file" sizes="size"/>
+# about favicons, see: http://www.netmagazine.com/features/create-perfect-favicon
+# FAVICONS = {
+# ("icon", "/favicon.ico", "16x16"),
+# ("icon", "/icon_128x128.png", "128x128"),
+# }
+
+# Show only teasers in the index pages? Defaults to False.
+# INDEX_TEASERS = False
+
+# A HTML fragment describing the license, for the sidebar. Default is "".
+# I recommend using the Creative Commons' wizard:
+# http://creativecommons.org/choose/
+# LICENSE = """
+# <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/">
+# <img alt="Creative Commons License BY-NC-SA"
+# style="border-width:0; margin-bottom:12px;"
+# src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>"""
+
+# A small copyright notice for the page footer (in HTML).
+# Default is ''
+CONTENT_FOOTER = 'Contents &copy; {date} <a href="mailto:{email}">{author}</a> - Powered by <a href="http://nikola.ralsina.com.ar">Nikola</a>'
+CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
+ author=BLOG_AUTHOR,
+ date=time.gmtime().tm_year)
+
+# To enable comments via Disqus, you need to create a forum at
+# http://disqus.com, and set DISQUS_FORUM to the short name you selected.
+# If you want to disable comments, set it to False.
+# Default is "nikolademo", used by the demo sites
+# DISQUS_FORUM = "nikolademo"
+
+# Create index.html for story folders?
+# STORY_INDEX = False
+# Enable comments on story pages?
+# COMMENTS_IN_STORIES = False
+# Enable comments on picture gallery pages?
+# COMMENTS_IN_GALLERIES = False
+
+# Do you want a add a Mathjax config file?
+# MATHJAX_CONFIG = ""
+
+# If you are using the compile-ipynb plugin, just add this one:
+#MATHJAX_CONFIG = """
+#<script type="text/x-mathjax-config">
+#MathJax.Hub.Config({
+# tex2jax: {
+# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ],
+# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ]
+# },
+# displayAlign: 'left', // Change this to 'center' to center equations.
+# "HTML-CSS": {
+# styles: {'.MathJax_Display': {"margin": 0}}
+# }
+#});
+#</script>
+#"""
+
+# Enable Addthis social buttons?
+# Defaults to true
+# ADD_THIS_BUTTONS = True
+
+# Modify the number of Post per Index Page
+# Defaults to 10
+# INDEX_DISPLAY_POST_COUNT = 10
+
+# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None,
+# the base.tmpl will use the feed Nikola generates. However, you may want to
+# change it for a feedburner feed or something else.
+# RSS_LINK = None
+
+# Show only teasers in the RSS feed? Default to True
+# RSS_TEASERS = True
+
+# A search form to search this site, for the sidebar. You can use a google
+# custom search (http://www.google.com/cse/)
+# Or a duckduckgo search: https://duckduckgo.com/search_box.html
+# Default is no search form.
+# SEARCH_FORM = ""
+#
+# This search form works for any site and looks good in the "site" theme where it
+# appears on the navigation bar
+#SEARCH_FORM = """
+#<!-- Custom search -->
+#<form method="get" id="search" action="http://duckduckgo.com/"
+# class="navbar-form pull-left">
+#<input type="hidden" name="sites" value="%s"/>
+#<input type="hidden" name="k8" value="#444444"/>
+#<input type="hidden" name="k9" value="#D51920"/>
+#<input type="hidden" name="kt" value="h"/>
+#<input type="text" name="q" maxlength="255"
+# placeholder="Search&hellip;" class="span2" style="margin-top: 4px;"/>
+#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" />
+#</form>
+#<!-- End of custom search -->
+#""" % BLOG_URL
+#
+# Also, there is a local search plugin you can use.
+
+# Use content distribution networks for jquery and twitter-bootstrap css and js
+# If this is True, jquery is served from the Google CDN and twitter-bootstrap
+# is served from the NetDNA CDN
+# Set this to False if you want to host your site without requiring access to
+# external resources.
+# USE_CDN = False
+
+# Google analytics or whatever else you use. Added to the bottom of <body>
+# in the default template (base.tmpl).
+# ANALYTICS = ""
+
+# The possibility to extract metadata from the filename by using a
+# regular expression.
+# To make it work you need to name parts of your regular expression.
+# The following names will be used to extract metadata:
+# - title
+# - slug
+# - date
+# - tags
+# - link
+# - description
+#
+# An example re is the following:
+# '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md'
+# FILE_METADATA_REGEXP = None
+
+# Nikola supports Twitter Card summaries / Open Graph.
+# Twitter cards make it possible for you to attach media to Tweets
+# that link to your content.
+#
+# IMPORTANT:
+# Please note, that you need to opt-in for using Twitter Cards!
+# To do this please visit https://dev.twitter.com/form/participate-twitter-cards
+#
+# Uncomment and modify to following lines to match your accounts.
+# Specifying the id for either 'site' or 'creator' will be preferred
+# over the cleartext username. Specifying an ID is not necessary.
+# Displaying images is currently not supported.
+# TWITTER_CARD = {
+# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph
+# # 'site': '@website', # twitter nick for the website
+# # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead.
+# # 'creator': '@username', # Username for the content creator / author.
+# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
+# }
+
+
+# If you want to use formatted post time in W3C-DTF Format(ex. 2012-03-30T23:00:00+02:00),
+# set timzone if you want a localized posted date.
+#
+# TIMEZONE = 'Europe/Zurich'
+
+# If webassets is installed, bundle JS and CSS to make site loading faster
+# USE_BUNDLES = True
+
+# Plugins you don't want to use. Be careful :-)
+# DISABLED_PLUGINS = ["render_galleries"]
+
+# Put in global_context things you want available on all your templates.
+# It can be anything, data, functions, modules, etc.
+
+GLOBAL_CONTEXT = {}
diff --git a/tests/data/translated_titles/stories/1.txt b/tests/data/translated_titles/stories/1.txt
new file mode 100644
index 0000000..45fb214
--- /dev/null
+++ b/tests/data/translated_titles/stories/1.txt
@@ -0,0 +1,5 @@
+.. title: Foo
+.. slug: 1
+.. date: 2001/01/01 00:00:00
+
+Foo
diff --git a/tests/data/translated_titles/stories/1.txt.es b/tests/data/translated_titles/stories/1.txt.es
new file mode 100644
index 0000000..a888c1f
--- /dev/null
+++ b/tests/data/translated_titles/stories/1.txt.es
@@ -0,0 +1,4 @@
+.. title: Bar
+.. slug: 1
+
+Bar
diff --git a/tests/test_command_import_wordpress.py b/tests/test_command_import_wordpress.py
index bda9b49..3be2ad9 100644
--- a/tests/test_command_import_wordpress.py
+++ b/tests/test_command_import_wordpress.py
@@ -9,17 +9,38 @@ import mock
class BasicCommandImportWordpress(unittest.TestCase):
def setUp(self):
- self.import_command = nikola.plugins.command_import_wordpress.CommandImportWordpress(
- )
- self.import_filename = os.path.abspath(
- os.path.join(os.path.dirname(__file__),
- 'wordpress_export_example.xml'))
+ self.import_command = nikola.plugins.command_import_wordpress.CommandImportWordpress()
+ self.import_filename = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), 'wordpress_export_example.xml'))
def tearDown(self):
del self.import_command
del self.import_filename
+class TestXMLGlueing(BasicCommandImportWordpress):
+ def test_making_correct_newlines(self):
+ xml = [b"Some information about how to (un)subscripe to a google group with a normal mail client.\n",
+ b"<ul>\n",
+ b" <li>to post: <strong>groupname@googlegroups.com</strong></li>\n",
+ b" <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>\n",
+ b" <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>\n",
+ b"</ul>\n",
+ b"Easy.\n"]
+
+ expected_xml = b"""Some information about how to (un)subscripe to a google group with a normal mail client.
+
+<ul>
+ <li>to post: <strong>groupname@googlegroups.com</strong></li>
+ <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>
+ <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>
+</ul>
+
+Easy.
+"""
+ self.assertEqual(expected_xml, self.import_command._glue_xml_lines(xml))
+
+
class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def setUp(self):
super(self.__class__, self).setUp()
@@ -28,8 +49,7 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
self.write_urlmap = mock.MagicMock()
self.write_configuration = mock.MagicMock()
- site_generation_patch = mock.patch(
- 'nikola.plugins.command_import_wordpress.CommandImportWordpress.generate_base_site', self.site_generation)
+ site_generation_patch = mock.patch('os.system', self.site_generation)
data_import_patch = mock.patch(
'nikola.plugins.command_import_wordpress.CommandImportWordpress.import_posts', self.data_import)
write_urlmap_patch = mock.patch(
@@ -56,14 +76,14 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def test_create_import(self):
valid_import_arguments = (
- ['--filename', self.import_filename],
- ['-f', self.import_filename, '-o', 'some_folder'],
- [self.import_filename],
- [self.import_filename, 'folder_argument'],
+ dict(options={'output_folder': 'some_folder'},
+ args=[self.import_filename]),
+ dict(args=[self.import_filename]),
+ dict(args=[self.import_filename, 'folder_argument']),
)
for arguments in valid_import_arguments:
- self.import_command.run(*arguments)
+ self.import_command.execute(**arguments)
self.assertTrue(self.site_generation.called)
self.assertTrue(self.data_import.called)
@@ -73,29 +93,24 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
def test_ignoring_drafts(self):
valid_import_arguments = (
- ['--filename', self.import_filename, '--no-drafts'],
- ['-f', self.import_filename, '-o', 'some_folder', '-d'],
+ dict(options={'exclude_drafts': True}, args=[
+ self.import_filename]),
+ dict(
+ options={'exclude_drafts': True,
+ 'output_folder': 'some_folder'},
+ args=[self.import_filename]),
)
for arguments in valid_import_arguments:
- self.import_command.run(*arguments)
+ self.import_command.execute(**arguments)
self.assertTrue(self.import_command.exclude_drafts)
- def test_getting_help(self):
- for arguments in (['-h'], ['--help']):
- self.assertRaises(SystemExit, self.import_command.run, *arguments)
-
- self.assertFalse(self.site_generation.called)
- self.assertFalse(self.data_import.called)
- self.assertFalse(self.write_urlmap.called)
- self.assertFalse(self.write_configuration.called)
-
class CommandImportWordpressTest(BasicCommandImportWordpress):
def test_create_import_work_without_argument(self):
# Running this without an argument must not fail.
# It should show the proper usage of the command.
- self.import_command.run()
+ self.import_command.execute()
def test_populate_context(self):
channel = self.import_command.get_channel_from_file(
@@ -109,7 +124,7 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
self.assertEqual('Wordpress blog title', context['BLOG_TITLE'])
self.assertEqual('Nikola test blog ;) - with moré Ümläüts',
context['BLOG_DESCRIPTION'])
- self.assertEqual('http://some.blog', context['BLOG_URL'])
+ self.assertEqual('http://some.blog', context['SITE_URL'])
self.assertEqual('mail@some.blog', context['BLOG_EMAIL'])
self.assertEqual('Niko', context['BLOG_AUTHOR'])
@@ -120,6 +135,8 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
channel)
self.import_command.url_map = {} # For testing we use an empty one.
self.import_command.output_folder = 'new_site'
+ self.import_command.squash_newlines = True
+ self.import_command.no_downloads = False
write_metadata = mock.MagicMock()
write_content = mock.MagicMock()
@@ -142,11 +159,42 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
'kontakt', '2009-07-16 20:20:32', None, [])
self.assertTrue(write_content.called)
- write_content.assert_any_call('new_site/posts/200704hoert.wp', 'An image.\n\n\n\n<img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" />\n\n\n\nSome source code.\n\n\n\n\n~~~~~~~~~~~~{.Python}\n\n\nimport sys\n\nprint sys.version\n\n\n~~~~~~~~~~~~\n\n\n\n\nThe end.\n\n')
+ write_content.assert_any_call('new_site/posts/200704hoert.wp',
+ """An image.
+
+<img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" />
+
+Some source code.
+
+~~~~~~~~~~~~{.Python}
+
+import sys
+
+print sys.version
+
+~~~~~~~~~~~~
+
+The end.
+
+""")
+
write_content.assert_any_call(
- 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp', '<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz.\n\nDie Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.')
+ 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp',
+ '''<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz.
+Die Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.''')
write_content.assert_any_call(
- 'new_site/stories/kontakt.wp', '<h1>Datenschutz</h1>\n\nIch erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind:\n\n<ul>\n\n <li>Browsertyp und -version</li>\n\n <li>verwendetes Betriebssystem</li>\n\n <li>Referrer URL (die zuvor besuchte Seite)</li>\n\n <li>IP Adresse des zugreifenden Rechners</li>\n\n <li>Uhrzeit der Serveranfrage.</li>\n\n</ul>\n\nDiese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.')
+ 'new_site/stories/kontakt.wp', """<h1>Datenschutz</h1>
+Ich erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind:
+
+<ul>
+ <li>Browsertyp und -version</li>
+ <li>verwendetes Betriebssystem</li>
+ <li>Referrer URL (die zuvor besuchte Seite)</li>
+ <li>IP Adresse des zugreifenden Rechners</li>
+ <li>Uhrzeit der Serveranfrage.</li>
+</ul>
+
+Diese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammenf\xfchrung dieser Daten mit anderen Datenquellen wird nicht vorgenommen, die Daten werden einzig zu statistischen Zwecken erhoben.""")
self.assertTrue(len(self.import_command.url_map) > 0)
@@ -165,13 +213,16 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
"""Applying markup conversions to content."""
transform_sourcecode = mock.MagicMock()
transform_caption = mock.MagicMock()
+ transform_newlines = mock.MagicMock()
with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_sourcecode', transform_sourcecode):
with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_caption', transform_caption):
- self.import_command.transform_content("random content")
+ with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_multiple_newlines', transform_newlines):
+ self.import_command.transform_content("random content")
self.assertTrue(transform_sourcecode.called)
self.assertTrue(transform_caption.called)
+ self.assertTrue(transform_newlines.called)
def test_transforming_source_code(self):
"""
@@ -226,6 +277,37 @@ asdasdas"""
self.assertEqual(
expected_content, self.import_command.transform_caption(content))
+ def test_transform_multiple_newlines(self):
+ content = """This
+
+
+has
+
+
+
+way to many
+
+newlines.
+
+
+"""
+ expected_content = """This
+
+has
+
+way to many
+
+newlines.
+
+"""
+ self.import_command.squash_newlines = False
+ self.assertEqual(content,
+ self.import_command.transform_multiple_newlines(content))
+
+ self.import_command.squash_newlines = True
+ self.assertEqual(expected_content,
+ self.import_command.transform_multiple_newlines(content))
+
def test_transform_caption_with_link_inside(self):
content = """[caption caption="Fehlermeldung"]<a href="http://some.blog/openttd-missing_sound.png"><img class="size-thumbnail wp-image-551" title="openttd-missing_sound" src="http://some.blog/openttd-missing_sound-150x150.png" alt="Fehlermeldung" /></a>[/caption]"""
transformed_content = self.import_command.transform_caption(content)
diff --git a/tests/test_command_init.py b/tests/test_command_init.py
index 32ce345..1904fa1 100644
--- a/tests/test_command_init.py
+++ b/tests/test_command_init.py
@@ -35,27 +35,26 @@ class CommandInitCallTest(unittest.TestCase):
del self.create_empty_site
def test_init_default(self):
- for arguments in (('destination', '--demo'),):
- self.init_commad.run(*arguments)
+ for arguments in (dict(options={'demo': True}, args=['destination']), {}):
+ self.init_commad.execute(**arguments)
self.assertTrue(self.create_configuration.called)
self.assertTrue(self.copy_sample_site.called)
self.assertFalse(self.create_empty_site.called)
def test_init_called_without_target(self):
- self.init_commad.run()
+ self.init_commad.execute()
self.assertFalse(self.create_configuration.called)
self.assertFalse(self.copy_sample_site.called)
self.assertFalse(self.create_empty_site.called)
def test_init_empty_dir(self):
- for arguments in (('destination', ), ('destination', '--empty')):
- self.init_commad.run(*arguments)
+ self.init_commad.execute(args=['destination'])
- self.assertTrue(self.create_configuration.called)
- self.assertFalse(self.copy_sample_site.called)
- self.assertTrue(self.create_empty_site.called)
+ self.assertTrue(self.create_configuration.called)
+ self.assertFalse(self.copy_sample_site.called)
+ self.assertTrue(self.create_empty_site.called)
if __name__ == '__main__':
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 947a832..c940a07 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
+import codecs
from contextlib import contextmanager
import os
import shutil
import tempfile
import unittest
+import lxml.html
+
from context import nikola
+from nikola import main
@contextmanager
@@ -18,56 +22,147 @@ def cd(path):
os.chdir(old_dir)
-class IntegrationTest(unittest.TestCase):
+class EmptyBuildTest(unittest.TestCase):
"""Basic integration testcase."""
- def setUp(self):
+
+ dataname = None
+
+ @classmethod
+ def setUpClass(self):
"""Setup a demo site."""
self.tmpdir = tempfile.mkdtemp()
self.target_dir = os.path.join(self.tmpdir, "target")
- self.build_command = nikola.plugins.command_build.CommandBuild()
self.init_command = nikola.plugins.command_init.CommandInit()
- self.init_command.copy_sample_site(self.target_dir)
- self.init_command.create_configuration(self.target_dir)
+ self.fill_site()
self.patch_site()
self.build()
+ @classmethod
+ def fill_site(self):
+ """Add any needed initial content."""
+ self.init_command.create_empty_site(self.target_dir)
+ self.init_command.create_configuration(self.target_dir)
+
+ if self.dataname:
+ src = os.path.join(os.path.dirname(__file__), 'data',
+ self.dataname)
+ for root, dirs, files in os.walk(src):
+ for src_name in files:
+ rel_dir = os.path.relpath(root, src)
+ dst_file = os.path.join(self.target_dir, rel_dir, src_name)
+ src_file = os.path.join(root, src_name)
+ shutil.copy2(src_file, dst_file)
+
+ @classmethod
def patch_site(self):
"""Make any modifications you need to the site."""
- pass
+ @classmethod
def build(self):
"""Build the site."""
with cd(self.target_dir):
- self.build_command.run()
+ main.main(["build"])
- def tearDown(self):
- """Reove the demo site."""
- shutil.rmtree(self.tmpdir)
+ @classmethod
+ def tearDownClass(self):
+ """Remove the demo site."""
+ def test_build(self):
+ """Ensure the build did something."""
+ index_path = os.path.join(
+ self.target_dir, "output", "archive.html")
+ self.assertTrue(os.path.isfile(index_path))
-class EmptytBuild(IntegrationTest):
- """Basic integration testcase."""
- def setUp(self):
- """Setup a demo site."""
- self.tmpdir = tempfile.mkdtemp()
- self.target_dir = os.path.join(self.tmpdir, "target")
- self.build_command = nikola.plugins.command_build.CommandBuild()
- self.init_command = nikola.plugins.command_init.CommandInit()
- self.init_command.create_empty_site(self.target_dir)
+
+class DemoBuildTest(EmptyBuildTest):
+ """Test that a default build of --demo works."""
+
+ @classmethod
+ def fill_site(self):
+ """Fill the site with demo content."""
+ self.init_command.copy_sample_site(self.target_dir)
self.init_command.create_configuration(self.target_dir)
- self.patch_site()
- self.build()
- def test_deleted_dodo(self):
- """Test that a default build of --demo works."""
- # Ensure the temprary dodo file is deleted (Issue #302)
- self.assertFalse(os.path.isfile(self.build_command.dodo.name))
+class TranslatedBuildTest(EmptyBuildTest):
+ """Test a site with translated content."""
-class DefaultBuild(IntegrationTest):
- """Test that a default build of --demo works."""
+ dataname = "translated_titles"
+
+ def test_translated_titles(self):
+ """Check that translated title is picked up."""
+ en_file = os.path.join(self.target_dir, "output", "stories", "1.html")
+ es_file = os.path.join(self.target_dir, "output", "es", "stories", "1.html")
+ # Files should be created
+ self.assertTrue(os.path.isfile(en_file))
+ self.assertTrue(os.path.isfile(es_file))
+ # And now let's check the titles
+ with codecs.open(en_file, 'r', 'utf8') as inf:
+ doc = lxml.html.parse(inf)
+ self.assertEqual(doc.find('//title').text, 'Foo | Demo Site')
+ with codecs.open(es_file, 'r', 'utf8') as inf:
+ doc = lxml.html.parse(inf)
+ self.assertEqual(doc.find('//title').text, 'Bar | Demo Site')
+
+
+class RelativeLinkTest(DemoBuildTest):
+ """Check that SITE_URL with a path doesn't break links."""
+
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "rb", "utf-8") as inf:
+ data = inf.read()
+ data = data.replace('SITE_URL = "http://nikola.ralsina.com.ar"',
+ 'SITE_URL = "http://nikola.ralsina.com.ar/foo/bar/"')
+ with codecs.open(conf_path, "wb+", "utf8") as outf:
+ outf.write(data)
+
+ def test_relative_links(self):
+ """Check that the links in output/index.html are correct"""
+ test_path = os.path.join(self.target_dir, "output", "index.html")
+ flag = False
+ with open(test_path, "rb") as inf:
+ data = inf.read()
+ for _, _, url, _ in lxml.html.iterlinks(data):
+ # Just need to be sure this one is ok
+ if url.endswith("css"):
+ self.assertFalse(url.startswith(".."))
+ flag = True
+ # But I also need to be sure it is there!
+ self.assertTrue(flag)
+
+
+class RelativeLinkTest2(DemoBuildTest):
+ """Check that dropping stories to the root doesn't break links."""
+
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "rb", "utf-8") as inf:
+ data = inf.read()
+ data = data.replace('("stories/*.txt", "stories", "story.tmpl", False),',
+ '("stories/*.txt", "", "story.tmpl", False),')
+ data = data.replace('# INDEX_PATH = ""',
+ 'INDEX_PATH = "blog"')
+ with codecs.open(conf_path, "wb+", "utf8") as outf:
+ outf.write(data)
+ outf.flush()
- def test_deleted_dodo(self):
- """Test that a default build of --demo works."""
- # Ensure the temprary dodo file is deleted (Issue #302)
- self.assertFalse(os.path.isfile(self.build_command.dodo.name))
+ def test_relative_links(self):
+ """Check that the links in a story are correct"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ data = open(conf_path).read()
+ test_path = os.path.join(self.target_dir, "output", "about-nikola.html")
+ flag = False
+ with open(test_path, "rb") as inf:
+ data = inf.read()
+ for _, _, url, _ in lxml.html.iterlinks(data):
+ # Just need to be sure this one is ok
+ if url.endswith("css"):
+ self.assertFalse(url.startswith(".."))
+ flag = True
+ # But I also need to be sure it is there!
+ self.assertTrue(flag)
diff --git a/tests/test_rss_feeds.py b/tests/test_rss_feeds.py
index ae1cd41..5b9b981 100644
--- a/tests/test_rss_feeds.py
+++ b/tests/test_rss_feeds.py
@@ -16,11 +16,13 @@ class RSSFeedTest(unittest.TestCase):
def setUp(self):
self.blog_url = "http://some.blog"
- with mock.patch('nikola.nikola.utils.get_meta',
- mock.Mock(return_value=('post title',
- 'awesome_article',
- '2012-10-01 22:41', 'tags',
- 'link', 'description'))):
+ with mock.patch('nikola.post.get_meta',
+ mock.Mock(return_value=({'title': 'post title',
+ 'slug': 'awesome_article',
+ 'date': '2012-10-01 22:41',
+ 'tags': 'tags', 'link':
+ 'link', 'description':
+ 'description'}))):
with mock.patch('nikola.nikola.utils.os.path.isdir',
mock.Mock(return_value=True)):
with mock.patch('nikola.nikola.Post.text',
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2cb36a8..4f3fd72 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from context import nikola
import unittest
import mock
+from nikola.post import get_meta
+
+
+class dummy(object):
+ pass
class GetMetaTest(unittest.TestCase):
@@ -19,16 +23,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
- self.assertEqual('Nikola needs more tests!', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post)
+
+ self.assertEqual('Nikola needs more tests!', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_get_title_from_rest(self):
file_metadata = [".. slug: write-tests-now\n",
@@ -42,16 +49,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
+
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post)
- self.assertEqual('Post Title', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ self.assertEqual('Post Title', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_get_title_from_fname(self):
file_metadata = [".. slug: write-tests-now\n",
@@ -63,16 +73,19 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('file_with_metadata')
+ post = dummy()
+ post.source_path = 'file_with_metadata'
+ post.metadata_path = 'file_with_metadata.meta'
- self.assertEqual('file_with_metadata', title)
- self.assertEqual('write-tests-now', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post, 'file_with_metadata')
+
+ self.assertEqual('file_with_metadata', meta['title'])
+ self.assertEqual('write-tests-now', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_use_filename_as_slug_fallback(self):
file_metadata = [".. title: Nikola needs more tests!\n",
@@ -85,36 +98,40 @@ class GetMetaTest(unittest.TestCase):
opener_mock = mock.mock_open(read_data=file_metadata)
opener_mock.return_value.readlines.return_value = file_metadata
- with mock.patch('nikola.utils.codecs.open', opener_mock, create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('Slugify this')
+ post = dummy()
+ post.source_path = 'Slugify this'
+ post.metadata_path = 'Slugify this.meta'
+
+ with mock.patch('nikola.post.codecs.open', opener_mock, create=True):
+ meta = get_meta(post, 'Slugify this')
- self.assertEqual('Nikola needs more tests!', title)
- self.assertEqual('slugify-this', slug)
- self.assertEqual('2012/09/15 19:52:05', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ self.assertEqual('Nikola needs more tests!', meta['title'])
+ self.assertEqual('slugify-this', meta['slug'])
+ self.assertEqual('2012/09/15 19:52:05', meta['date'])
+ self.assertFalse('tags' in meta)
+ self.assertFalse('link' in meta)
+ self.assertFalse('description' in meta)
def test_extracting_metadata_from_filename(self):
- with mock.patch('nikola.utils.codecs.open', create=True):
- (
- title, slug, date, tags, link, description) = nikola.utils.get_meta('2013-01-23-the_slug-dubdubtitle.md',
- '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md')
-
- self.assertEqual('dubdubtitle', title)
- self.assertEqual('the_slug', slug)
- self.assertEqual('2013-01-23', date)
- self.assertEqual('', tags)
- self.assertEqual('', link)
- self.assertEqual('', description)
+ post = dummy()
+ post.source_path = '2013-01-23-the_slug-dubdubtitle.md'
+ post.metadata_path = '2013-01-23-the_slug-dubdubtitle.meta'
+ with mock.patch('nikola.post.codecs.open', create=True):
+ meta = get_meta(post,
+ '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md')
+
+ self.assertEqual('dubdubtitle', meta['title'])
+ self.assertEqual('the_slug', meta['slug'])
+ self.assertEqual('2013-01-23', meta['date'])
def test_get_meta_slug_only_from_filename(self):
- with mock.patch('nikola.utils.codecs.open', create=True):
- (title, slug, date, tags, link,
- description) = nikola.utils.get_meta('some/path/the_slug.md')
+ post = dummy()
+ post.source_path = 'some/path/the_slug.md'
+ post.metadata_path = 'some/path/the_slug.meta'
+ with mock.patch('nikola.post.codecs.open', create=True):
+ meta = get_meta(post)
- self.assertEqual('the_slug', slug)
+ self.assertEqual('the_slug', meta['slug'])
if __name__ == '__main__':
unittest.main()
diff --git a/tests/wordpress_export_example.xml b/tests/wordpress_export_example.xml
index 8ef1325..e697a5b 100644
--- a/tests/wordpress_export_example.xml
+++ b/tests/wordpress_export_example.xml
@@ -177,6 +177,58 @@ Diese Daten sind für mich nicht bestimmten Personen zuordenbar. Eine Zusammenf
<wp:meta_value><![CDATA[default]]></wp:meta_value>
</wp:postmeta>
</item>
+ <item>
+ <title>Indentation Test</title>
+ <link>http://some.blog/2012/04/indentation_test/</link>
+ <pubDate>Sun, 15 Apr 2012 11:44:59 +0000</pubDate>
+ <dc:creator>Niko</dc:creator>
+ <guid isPermaLink="false">http://some.blog/?p=2077</guid>
+ <description></description>
+ <content:encoded><![CDATA[Some examples for indented code that should not be broken.
+
+You should see some Python code hereafter. The code should be one block.
+<pre>class Borg:
+ _state = {}
+ def __init__(self):
+ self.__dict__ = self._state</pre>
+&nbsp;
+
+Here is a listing made with HTML that should display without the HTML being visible to the visitor.
+<ul>
+ <li>to post: <strong>groupname@googlegroups.com</strong></li>
+ <li>to <em>subscribe</em>: <strong>groupname+subscribe@googlegroups.com</strong></li>
+ <li>to <em>unsubscribe</em>: <strong>groupname+unsubscribe@googlegroups.com</strong></li>
+</ul>
+
+A listing with another listing inside.
+<ul>
+<li> foo
+ <ul>
+ <li> bar
+ </ul>
+</ul>
+]]></content:encoded>
+ <excerpt:encoded><![CDATA[]]></excerpt:encoded>
+ <wp:post_id>2077</wp:post_id>
+ <wp:post_date>2012-04-15 12:44:59</wp:post_date>
+ <wp:post_date_gmt>2012-04-15 11:44:59</wp:post_date_gmt>
+ <wp:comment_status>open</wp:comment_status>
+ <wp:ping_status>open</wp:ping_status>
+ <wp:post_name>python-borg-pattern</wp:post_name>
+ <wp:status>publish</wp:status>
+ <wp:post_parent>0</wp:post_parent>
+ <wp:menu_order>0</wp:menu_order>
+ <wp:post_type>post</wp:post_type>
+ <wp:post_password></wp:post_password>
+ <wp:is_sticky>0</wp:is_sticky>
+ <category domain="category" nicename="programming"><![CDATA[programming]]></category>
+ <category domain="post_tag" nicename="design-patterns"><![CDATA[Design Patterns]]></category>
+ <category domain="post_tag" nicename="python"><![CDATA[Python]]></category>
+ <wp:postmeta>
+ <wp:meta_key>_edit_last</wp:meta_key>
+ <wp:meta_value><![CDATA[2]]></wp:meta_value>
+ </wp:postmeta>
+ </item>
</channel>
</rss>