summaryrefslogtreecommitdiffstats
path: root/nikola/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/plugins')
-rw-r--r--nikola/plugins/__init__.py3
-rw-r--r--nikola/plugins/basic_import.py166
-rw-r--r--nikola/plugins/command/__init__.py25
-rw-r--r--nikola/plugins/command/auto.plugin9
-rw-r--r--nikola/plugins/command/auto.py103
-rw-r--r--nikola/plugins/command/bootswatch_theme.plugin (renamed from nikola/plugins/command_bootswatch_theme.plugin)4
-rw-r--r--nikola/plugins/command/bootswatch_theme.py (renamed from nikola/plugins/command_bootswatch_theme.py)49
-rw-r--r--nikola/plugins/command/check.plugin (renamed from nikola/plugins/command_check.plugin)4
-rw-r--r--nikola/plugins/command/check.py204
-rw-r--r--nikola/plugins/command/console.plugin (renamed from nikola/plugins/command_console.plugin)4
-rw-r--r--nikola/plugins/command/console.py (renamed from nikola/plugins/command_console.py)27
-rw-r--r--nikola/plugins/command/deploy.plugin (renamed from nikola/plugins/command_deploy.plugin)4
-rw-r--r--nikola/plugins/command/deploy.py141
-rw-r--r--nikola/plugins/command/import_blogger.plugin (renamed from nikola/plugins/command_import_blogger.plugin)4
-rw-r--r--nikola/plugins/command/import_blogger.py (renamed from nikola/plugins/command_import_blogger.py)159
-rw-r--r--nikola/plugins/command/import_feed.plugin10
-rw-r--r--nikola/plugins/command/import_feed.py197
-rw-r--r--nikola/plugins/command/import_wordpress.plugin (renamed from nikola/plugins/command_import_wordpress.plugin)4
-rw-r--r--nikola/plugins/command/import_wordpress.py (renamed from nikola/plugins/command_import_wordpress.py)256
-rw-r--r--nikola/plugins/command/init.plugin (renamed from nikola/plugins/command_init.plugin)4
-rw-r--r--nikola/plugins/command/init.py (renamed from nikola/plugins/command_init.py)45
-rw-r--r--nikola/plugins/command/install_plugin.plugin10
-rw-r--r--nikola/plugins/command/install_plugin.py185
-rw-r--r--nikola/plugins/command/install_theme.plugin (renamed from nikola/plugins/command_install_theme.plugin)4
-rw-r--r--nikola/plugins/command/install_theme.py163
-rw-r--r--nikola/plugins/command/mincss.plugin10
-rw-r--r--nikola/plugins/command/mincss.py75
-rw-r--r--nikola/plugins/command/new_post.plugin (renamed from nikola/plugins/command_new_post.plugin)4
-rw-r--r--nikola/plugins/command/new_post.py (renamed from nikola/plugins/command_new_post.py)108
-rw-r--r--nikola/plugins/command/planetoid.plugin (renamed from nikola/plugins/command_planetoid.plugin)4
-rw-r--r--nikola/plugins/command/planetoid/__init__.py (renamed from nikola/plugins/command_planetoid/__init__.py)30
-rw-r--r--nikola/plugins/command/serve.plugin (renamed from nikola/plugins/command_serve.plugin)4
-rw-r--r--nikola/plugins/command/serve.py153
-rw-r--r--nikola/plugins/command/version.plugin9
-rw-r--r--nikola/plugins/command/version.py (renamed from nikola/plugins/compile_rest/dummy.py)28
-rw-r--r--nikola/plugins/command_check.py166
-rw-r--r--nikola/plugins/command_deploy.py65
-rw-r--r--nikola/plugins/command_install_theme.py105
-rw-r--r--nikola/plugins/command_serve.py79
-rw-r--r--nikola/plugins/compile/__init__.py0
-rw-r--r--nikola/plugins/compile/asciidoc.plugin10
-rw-r--r--nikola/plugins/compile/asciidoc.py65
-rw-r--r--nikola/plugins/compile/bbcode.plugin (renamed from nikola/plugins/compile_bbcode.plugin)4
-rw-r--r--nikola/plugins/compile/bbcode.py (renamed from nikola/plugins/compile_bbcode.py)24
-rw-r--r--nikola/plugins/compile/html.plugin (renamed from nikola/plugins/compile_html.plugin)4
-rw-r--r--nikola/plugins/compile/html.py (renamed from nikola/plugins/compile_html.py)21
-rw-r--r--nikola/plugins/compile/ipynb.plugin (renamed from nikola/plugins/compile_ipynb.plugin)4
-rw-r--r--nikola/plugins/compile/ipynb/README.txt44
-rw-r--r--nikola/plugins/compile/ipynb/__init__.py (renamed from nikola/plugins/compile_ipynb/__init__.py)46
-rw-r--r--nikola/plugins/compile/markdown.plugin (renamed from nikola/plugins/compile_markdown.plugin)4
-rw-r--r--nikola/plugins/compile/markdown/__init__.py (renamed from nikola/plugins/compile_markdown/__init__.py)34
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.py (renamed from nikola/plugins/compile_markdown/mdx_gist.py)122
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.py (renamed from nikola/plugins/compile_markdown/mdx_nikola.py)4
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.py (renamed from nikola/plugins/compile_markdown/mdx_podcast.py)0
-rw-r--r--nikola/plugins/compile/misaka.plugin (renamed from nikola/plugins/compile_misaka.plugin)2
-rw-r--r--nikola/plugins/compile/misaka.py (renamed from nikola/plugins/compile_misaka/__init__.py)29
-rw-r--r--nikola/plugins/compile/pandoc.plugin10
-rw-r--r--nikola/plugins/compile/pandoc.py65
-rw-r--r--nikola/plugins/compile/php.plugin10
-rw-r--r--nikola/plugins/compile/php.py62
-rw-r--r--nikola/plugins/compile/rest.plugin (renamed from nikola/plugins/compile_rest.plugin)4
-rw-r--r--nikola/plugins/compile/rest/__init__.py200
-rw-r--r--nikola/plugins/compile/rest/chart.plugin10
-rw-r--r--nikola/plugins/compile/rest/chart.py150
-rw-r--r--nikola/plugins/compile/rest/doc.plugin10
-rw-r--r--nikola/plugins/compile/rest/doc.py88
-rw-r--r--nikola/plugins/compile/rest/gist.plugin10
-rw-r--r--nikola/plugins/compile/rest/gist.py (renamed from nikola/plugins/compile_rest/gist_directive.py)48
-rw-r--r--nikola/plugins/compile/rest/listing.plugin10
-rw-r--r--nikola/plugins/compile/rest/listing.py (renamed from nikola/plugins/compile_rest/listing.py)230
-rw-r--r--nikola/plugins/compile/rest/media.plugin10
-rw-r--r--nikola/plugins/compile/rest/media.py63
-rw-r--r--nikola/plugins/compile/rest/post_list.plugin9
-rw-r--r--nikola/plugins/compile/rest/post_list.py165
-rw-r--r--nikola/plugins/compile/rest/slides.plugin10
-rw-r--r--nikola/plugins/compile/rest/slides.py (renamed from nikola/plugins/compile_rest/slides.py)54
-rw-r--r--nikola/plugins/compile/rest/soundcloud.plugin10
-rw-r--r--nikola/plugins/compile/rest/soundcloud.py (renamed from nikola/plugins/compile_rest/soundcloud.py)18
-rw-r--r--nikola/plugins/compile/rest/vimeo.plugin7
-rw-r--r--nikola/plugins/compile/rest/vimeo.py (renamed from nikola/plugins/compile_rest/vimeo.py)43
-rw-r--r--nikola/plugins/compile/rest/youtube.plugin8
-rw-r--r--nikola/plugins/compile/rest/youtube.py (renamed from nikola/plugins/compile_rest/youtube.py)20
-rw-r--r--nikola/plugins/compile/textile.plugin (renamed from nikola/plugins/compile_textile.plugin)4
-rw-r--r--nikola/plugins/compile/textile.py (renamed from nikola/plugins/compile_textile.py)22
-rw-r--r--nikola/plugins/compile/txt2tags.plugin (renamed from nikola/plugins/compile_txt2tags.plugin)4
-rw-r--r--nikola/plugins/compile/txt2tags.py (renamed from nikola/plugins/compile_txt2tags.py)19
-rw-r--r--nikola/plugins/compile/wiki.plugin (renamed from nikola/plugins/compile_wiki.plugin)4
-rw-r--r--nikola/plugins/compile/wiki.py (renamed from nikola/plugins/compile_wiki.py)25
-rw-r--r--nikola/plugins/compile_ipynb/README.txt35
-rw-r--r--nikola/plugins/compile_rest/__init__.py138
-rw-r--r--nikola/plugins/loghandler/smtp.plugin9
-rw-r--r--nikola/plugins/loghandler/smtp.py54
-rw-r--r--nikola/plugins/loghandler/stderr.plugin9
-rw-r--r--nikola/plugins/loghandler/stderr.py50
-rw-r--r--nikola/plugins/task/__init__.py0
-rw-r--r--nikola/plugins/task/archive.plugin (renamed from nikola/plugins/task_archive.plugin)4
-rw-r--r--nikola/plugins/task/archive.py (renamed from nikola/plugins/task_archive.py)104
-rw-r--r--nikola/plugins/task/build_less.plugin10
-rw-r--r--nikola/plugins/task/build_less.py99
-rw-r--r--nikola/plugins/task/build_sass.plugin9
-rw-r--r--nikola/plugins/task/build_sass.py117
-rw-r--r--nikola/plugins/task/bundles.plugin (renamed from nikola/plugins/task_create_bundles.plugin)4
-rw-r--r--nikola/plugins/task/bundles.py (renamed from nikola/plugins/task_create_bundles.py)46
-rw-r--r--nikola/plugins/task/copy_assets.plugin (renamed from nikola/plugins/task_copy_assets.plugin)4
-rw-r--r--nikola/plugins/task/copy_assets.py (renamed from nikola/plugins/task_copy_assets.py)18
-rw-r--r--nikola/plugins/task/copy_files.plugin (renamed from nikola/plugins/task_copy_files.plugin)4
-rw-r--r--nikola/plugins/task/copy_files.py (renamed from nikola/plugins/task_copy_files.py)12
-rw-r--r--nikola/plugins/task/galleries.plugin (renamed from nikola/plugins/task_render_galleries.plugin)4
-rw-r--r--nikola/plugins/task/galleries.py553
-rw-r--r--nikola/plugins/task/gzip.plugin10
-rw-r--r--nikola/plugins/task/gzip.py78
-rw-r--r--nikola/plugins/task/indexes.plugin (renamed from nikola/plugins/task_indexes.plugin)6
-rw-r--r--nikola/plugins/task/indexes.py (renamed from nikola/plugins/task_indexes.py)82
-rw-r--r--nikola/plugins/task/listings.plugin (renamed from nikola/plugins/task_render_listings.plugin)4
-rw-r--r--nikola/plugins/task/listings.py (renamed from nikola/plugins/task_render_listings.py)31
-rw-r--r--nikola/plugins/task/localsearch.plugin (renamed from nikola/plugins/task_localsearch.plugin)4
-rw-r--r--nikola/plugins/task/localsearch/MIT-LICENSE.txt (renamed from nikola/plugins/task_localsearch/MIT-LICENSE.txt)0
-rw-r--r--nikola/plugins/task/localsearch/__init__.py (renamed from nikola/plugins/task_localsearch/__init__.py)20
-rw-r--r--nikola/plugins/task/localsearch/files/assets/css/img/loader.gif (renamed from nikola/plugins/task_localsearch/files/assets/css/img/loader.gif)bin4178 -> 4178 bytes
-rwxr-xr-xnikola/plugins/task/localsearch/files/assets/css/img/search.pngbin0 -> 315 bytes
-rwxr-xr-xnikola/plugins/task/localsearch/files/assets/css/tipuesearch.css159
-rw-r--r--nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js)174
-rw-r--r--nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js)11
-rwxr-xr-xnikola/plugins/task/localsearch/files/tipue_search.html (renamed from nikola/plugins/task_localsearch/files/tipue_search.html)0
-rw-r--r--nikola/plugins/task/mustache.plugin (renamed from nikola/plugins/task_mustache.plugin)4
-rw-r--r--nikola/plugins/task/mustache/__init__.py (renamed from nikola/plugins/task_mustache/__init__.py)35
-rw-r--r--nikola/plugins/task/mustache/mustache-template.html (renamed from nikola/plugins/task_mustache/mustache-template.html)6
-rw-r--r--nikola/plugins/task/mustache/mustache.html (renamed from nikola/plugins/task_mustache/mustache.html)10
-rw-r--r--nikola/plugins/task/pages.plugin (renamed from nikola/plugins/task_render_pages.plugin)4
-rw-r--r--nikola/plugins/task/pages.py (renamed from nikola/plugins/task_render_pages.py)14
-rw-r--r--nikola/plugins/task/posts.plugin (renamed from nikola/plugins/task_render_posts.plugin)4
-rw-r--r--nikola/plugins/task/posts.py66
-rw-r--r--nikola/plugins/task/redirect.plugin (renamed from nikola/plugins/task_redirect.plugin)4
-rw-r--r--nikola/plugins/task/redirect.py (renamed from nikola/plugins/task_redirect.py)24
-rw-r--r--nikola/plugins/task/rss.plugin (renamed from nikola/plugins/task_render_rss.plugin)6
-rw-r--r--nikola/plugins/task/rss.py (renamed from nikola/plugins/task_render_rss.py)29
-rw-r--r--nikola/plugins/task/sitemap.plugin (renamed from nikola/plugins/task_sitemap.plugin)4
-rw-r--r--nikola/plugins/task/sitemap/__init__.py172
-rw-r--r--nikola/plugins/task/sources.plugin (renamed from nikola/plugins/task_render_sources.plugin)4
-rw-r--r--nikola/plugins/task/sources.py (renamed from nikola/plugins/task_render_sources.py)63
-rw-r--r--nikola/plugins/task/tags.plugin (renamed from nikola/plugins/task_render_tags.plugin)4
-rw-r--r--nikola/plugins/task/tags.py (renamed from nikola/plugins/task_render_tags.py)137
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/expand.pngbin424 -> 0 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/link.pngbin463 -> 0 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/search.gifbin208 -> 0 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/tipuesearch.css232
-rw-r--r--nikola/plugins/task_render_galleries.py338
-rw-r--r--nikola/plugins/task_render_posts.py140
-rw-r--r--nikola/plugins/task_sitemap/__init__.py105
-rw-r--r--nikola/plugins/template/__init__.py0
-rw-r--r--nikola/plugins/template/jinja.plugin (renamed from nikola/plugins/template_jinja.plugin)4
-rw-r--r--nikola/plugins/template/jinja.py (renamed from nikola/plugins/template_jinja.py)48
-rw-r--r--nikola/plugins/template/mako.plugin (renamed from nikola/plugins/template_mako.plugin)4
-rw-r--r--nikola/plugins/template/mako.py (renamed from nikola/plugins/template_mako.py)41
154 files changed, 5253 insertions, 2554 deletions
diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py
index b1de7f1..139759b 100644
--- a/nikola/plugins/__init__.py
+++ b/nikola/plugins/__init__.py
@@ -1,3 +1,2 @@
+# -*- coding: utf-8 -*-
from __future__ import absolute_import
-
-from . import command_import_wordpress # NOQA
diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py
new file mode 100644
index 0000000..e368fca
--- /dev/null
+++ b/nikola/plugins/basic_import.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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, print_function
+import codecs
+import csv
+import datetime
+import os
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse # NOQA
+
+from lxml import etree, html
+from mako.template import Template
+
+from nikola import utils
+
+links = {}
+
+
+class ImportMixin(object):
+ """Mixin with common used methods."""
+
+ name = "import_mixin"
+ 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.'
+ },
+ ]
+
+ def _execute(self, options={}, args=[]):
+ """Import a blog from an export into a Nikola site."""
+ raise NotImplementedError("Must be implemented by a subclass.")
+
+ @classmethod
+ def get_channel_from_file(cls, filename):
+ tree = etree.fromstring(cls.read_xml_file(filename))
+ channel = tree.find('channel')
+ return channel
+
+ @staticmethod
+ def configure_redirections(url_map):
+ redirections = []
+ for k, v in url_map.items():
+ if not k[-1] == '/':
+ k = k + '/'
+
+ # remove the initial "/" because src is a relative file path
+ src = (urlparse(k).path + 'index.html')[1:]
+ dst = (urlparse(v).path)
+ if src == 'index.html':
+ utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k))
+ else:
+ redirections.append((src, dst))
+
+ return redirections
+
+ def generate_base_site(self):
+ if not os.path.exists(self.output_folder):
+ os.system('nikola init ' + self.output_folder)
+ else:
+ self.import_into_existing_site = True
+ utils.LOGGER.notice('The folder {0} already exists - assuming that this is a '
+ 'already existing nikola site.'.format(self.output_folder))
+
+ filename = os.path.join(os.path.dirname(utils.__file__), 'conf.py.in')
+ # The 'strict_undefined=True' will give the missing symbol name if any,
+ # (ex: NameError: 'THEME' is not defined )
+ # for other errors from mako/runtime.py, you can add format_extensions=True ,
+ # then more info will be writen to *somefile* (most probably conf.py)
+ conf_template = Template(filename=filename, strict_undefined=True)
+
+ return conf_template
+
+ @staticmethod
+ def populate_context(channel):
+ raise NotImplementedError("Must be implemented by a subclass.")
+
+ @classmethod
+ def transform_content(cls, content):
+ return content
+
+ @classmethod
+ def write_content(cls, filename, content):
+ doc = html.document_fromstring(content)
+ doc.rewrite_links(replacer)
+
+ utils.makedirs(os.path.dirname(filename))
+ with open(filename, "wb+") as fd:
+ fd.write(html.tostring(doc, encoding='utf8'))
+
+ @staticmethod
+ def write_metadata(filename, title, slug, post_date, description, tags):
+ if not description:
+ description = ""
+
+ utils.makedirs(os.path.dirname(filename))
+ with codecs.open(filename, "w+", "utf8") as fd:
+ 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('{0}\n'.format(description))
+
+ @staticmethod
+ def write_urlmap_csv(output_file, url_map):
+ utils.makedirs(os.path.dirname(output_file))
+ with codecs.open(output_file, 'w+', 'utf8') as fd:
+ csv_writer = csv.writer(fd)
+ for item in url_map.items():
+ csv_writer.writerow(item)
+
+ def get_configuration_output_path(self):
+ if not self.import_into_existing_site:
+ filename = 'conf.py'
+ else:
+ filename = 'conf.py.{name}-{time}'.format(
+ time=datetime.datetime.now().strftime('%Y%m%d_%H%M%S'),
+ name=self.name)
+ config_output_path = os.path.join(self.output_folder, filename)
+ utils.LOGGER.notice('Configuration will be written to: {0}'.format(config_output_path))
+
+ return config_output_path
+
+ @staticmethod
+ def write_configuration(filename, rendered_template):
+ utils.makedirs(os.path.dirname(filename))
+ with codecs.open(filename, 'w+', 'utf8') as fd:
+ fd.write(rendered_template)
+
+
+def replacer(dst):
+ return links.get(dst, dst)
diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py
new file mode 100644
index 0000000..9be4d63
--- /dev/null
+++ b/nikola/plugins/command/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin
new file mode 100644
index 0000000..87939b2
--- /dev/null
+++ b/nikola/plugins/command/auto.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = auto
+Module = auto
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.2
+Website = http://getnikola.com
+Description = Automatically detect site changes, rebuild and optionally refresh a browser.
diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py
new file mode 100644
index 0000000..cb726d9
--- /dev/null
+++ b/nikola/plugins/command/auto.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 codecs
+import json
+import os
+import subprocess
+
+from nikola.plugin_categories import Command
+from nikola.utils import req_missing
+
+GUARDFILE = """#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from livereload.task import Task
+import json
+import subprocess
+
+def f():
+ import subprocess
+ subprocess.call(("nikola", "build"))
+
+fdata = json.loads('''{0}''')
+
+for watch in fdata:
+ Task.add(watch, f)
+"""
+
+
+class Auto(Command):
+ """Start debugging console."""
+ name = "auto"
+ doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser"
+ cmd_options = [
+ {
+ 'name': 'browser',
+ 'short': 'b',
+ 'type': bool,
+ 'help': 'Start a web browser.',
+ 'default': False,
+ },
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Start the watcher."""
+ try:
+ from livereload.server import start
+ except ImportError:
+ req_missing(['livereload'], 'use the "auto" command')
+ return
+
+ # Run an initial build so we are uptodate
+ subprocess.call(("nikola", "build"))
+
+ port = options and options.get('port')
+
+ # Create a Guardfile
+ with codecs.open("Guardfile", "wb+", "utf8") as guardfile:
+ l = ["conf.py", "themes", "templates", self.site.config['GALLERY_PATH']]
+ for item in self.site.config['post_pages']:
+ l.append(os.path.dirname(item[0]))
+ for item in self.site.config['FILES_FOLDERS']:
+ l.append(os.path.dirname(item))
+ data = GUARDFILE.format(json.dumps(l))
+ guardfile.write(data)
+
+ out_folder = self.site.config['OUTPUT_FOLDER']
+
+ os.chmod("Guardfile", 0o755)
+
+ start(port, out_folder, options and options.get('browser'))
diff --git a/nikola/plugins/command_bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin
index f75f734..7091310 100644
--- a/nikola/plugins/command_bootswatch_theme.plugin
+++ b/nikola/plugins/command/bootswatch_theme.plugin
@@ -1,10 +1,10 @@
[Core]
Name = bootswatch_theme
-Module = command_bootswatch_theme
+Module = bootswatch_theme
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Given a swatch name and a parent theme, creates a custom theme.
diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py
index 8400c9f..eb27f94 100644
--- a/nikola/plugins/command_bootswatch_theme.py
+++ b/nikola/plugins/command/bootswatch_theme.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,15 +33,18 @@ except ImportError:
requests = None # NOQA
from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER)
class CommandBootswatchTheme(Command):
- """Given a swatch name and a parent theme, creates a custom theme."""
+ """Given a swatch name from bootswatch.com 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."
+ doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\
+ " theme"
cmd_options = [
{
'name': 'name',
@@ -60,37 +65,39 @@ class CommandBootswatchTheme(Command):
'name': 'parent',
'short': 'p',
'long': 'parent',
- 'default': 'site',
- 'help': 'Parent theme name (default: site)',
+ 'default': 'bootstrap3',
+ 'help': 'Parent theme name (default: bootstrap3)',
},
]
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
+ utils.req_missing(['requests'], 'install Bootswatch themes')
name = options['name']
swatch = options['swatch']
parent = options['parent']
+ version = ''
+
+ # See if we need bootswatch for bootstrap v2 or v3
+ themes = utils.get_theme_chain(parent)
+ if 'bootstrap3' not in themes:
+ version = '2'
+ elif 'bootstrap' not in themes:
+ LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap')
- print("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch,
- parent))
- try:
- os.makedirs(os.path.join('themes', name, 'assets', 'css'))
- except:
- pass
+ LOGGER.notice("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent))
+ utils.makedirs(os.path.join('themes', name, 'assets', 'css'))
for fname in ('bootstrap.min.css', 'bootstrap.css'):
- url = '/'.join(('http://bootswatch.com', swatch, fname))
- print("Downloading: ", url)
+ url = '/'.join(('http://bootswatch.com', version, swatch, fname))
+ LOGGER.notice("Downloading: " + url)
data = requests.get(url).text
with open(os.path.join('themes', name, 'assets', 'css', fname),
'wb+') as output:
- output.write(data)
+ output.write(data.encode('utf-8'))
with open(os.path.join('themes', name, 'parent'), 'wb+') as output:
- output.write(parent)
- print('Theme created. Change the THEME setting to "{0}" to use '
- 'it.'.format(name))
+ output.write(parent.encode('utf-8'))
+ LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use '
+ 'it.'.format(name))
diff --git a/nikola/plugins/command_check.plugin b/nikola/plugins/command/check.plugin
index d4dcd1c..8ceda5f 100644
--- a/nikola/plugins/command_check.plugin
+++ b/nikola/plugins/command/check.plugin
@@ -1,10 +1,10 @@
[Core]
Name = check
-Module = command_check
+Module = check
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Check the generated site
diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py
new file mode 100644
index 0000000..5c7e49a
--- /dev/null
+++ b/nikola/plugins/command/check.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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
+import os
+import re
+import sys
+try:
+ from urllib import unquote
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import unquote, urlparse # NOQA
+
+import lxml.html
+
+from nikola.plugin_categories import Command
+from nikola.utils import get_logger
+
+
+class CommandCheck(Command):
+ """Check the generated site."""
+
+ name = "check"
+ logger = None
+
+ 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': 'clean',
+ 'long': 'clean-files',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Remove all unknown files, use with caution',
+ },
+ {
+ '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."""
+
+ self.logger = get_logger('check', self.site.loghandlers)
+
+ if not options['links'] and not options['files'] and not options['clean']:
+ print(self.help())
+ return False
+ if options['links']:
+ failure = self.scan_links(options['find_sources'])
+ if options['files']:
+ failure = self.scan_files()
+ if options['clean']:
+ failure = self.clean_files()
+ if failure:
+ sys.exit(1)
+
+ existing_targets = set([])
+
+ def analyze(self, task, find_sources=False):
+ rv = False
+ self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']]
+ try:
+ filename = task.split(":")[-1]
+ d = lxml.html.fromstring(open(filename).read())
+ for l in d.iterlinks():
+ target = l[0].attrib[l[1]]
+ if target == "#":
+ continue
+ parsed = urlparse(target)
+ if parsed.scheme or target.startswith('//'):
+ continue
+ if parsed.fragment:
+ target = target.split('#')[0]
+ target_filename = os.path.abspath(
+ os.path.join(os.path.dirname(filename), unquote(target)))
+ if any(re.match(x, target_filename) for x in self.whitelist):
+ continue
+ elif target_filename not in self.existing_targets:
+ if os.path.exists(target_filename):
+ self.existing_targets.add(target_filename)
+ else:
+ rv = True
+ self.logger.warn("Broken link in {0}: ".format(filename), target)
+ if find_sources:
+ self.logger.warn("Possible sources:")
+ self.logger.warn(os.popen('nikola list --deps ' + task, 'r').read())
+ self.logger.warn("===============================\n")
+ except Exception as exc:
+ self.logger.error("Error with:", filename, exc)
+ return rv
+
+ def scan_links(self, find_sources=False):
+ self.logger.notice("Checking Links:")
+ self.logger.notice("===============")
+ failure = False
+ 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:
+ if self.analyze(task, find_sources):
+ failure = True
+ if not failure:
+ self.logger.notice("All links checked.")
+ return failure
+
+ def scan_files(self):
+ failure = False
+ self.logger.notice("Checking Files:")
+ self.logger.notice("===============\n")
+ only_on_output, only_on_input = self.real_scan_files()
+
+ # Ignore folders
+ only_on_output = [p for p in only_on_output if not os.path.isdir(p)]
+ only_on_input = [p for p in only_on_input if not os.path.isdir(p)]
+
+ if only_on_output:
+ only_on_output.sort()
+ self.logger.warn("Files from unknown origins:")
+ for f in only_on_output:
+ self.logger.warn(f)
+ failure = True
+ if only_on_input:
+ only_on_input.sort()
+ self.logger.warn("Files not generated:")
+ for f in only_on_input:
+ self.logger.warn(f)
+ if not failure:
+ self.logger.notice("All files checked.")
+ return failure
+
+ def clean_files(self):
+ only_on_output, _ = self.real_scan_files()
+ for f in only_on_output:
+ os.unlink(f)
+ return True
+
+ def real_scan_files(self):
+ task_fnames = set([])
+ real_fnames = set([])
+ output_folder = self.site.config['OUTPUT_FOLDER']
+ # First check that all targets are generated in the right places
+ for task in os.popen('nikola list --all', 'r').readlines():
+ task = task.strip()
+ if output_folder in task and ':' in task:
+ fname = task.split(':', 1)[-1]
+ task_fnames.add(fname)
+ # And now check that there are no non-target files
+ for root, dirs, files in os.walk(output_folder):
+ for src_name in files:
+ fname = os.path.join(root, src_name)
+ real_fnames.add(fname)
+
+ only_on_output = list(real_fnames - task_fnames)
+
+ only_on_input = list(task_fnames - real_fnames)
+
+ return (only_on_output, only_on_input)
diff --git a/nikola/plugins/command_console.plugin b/nikola/plugins/command/console.plugin
index 003b994..a2be9ca 100644
--- a/nikola/plugins/command_console.plugin
+++ b/nikola/plugins/command/console.plugin
@@ -1,9 +1,9 @@
[Core]
Name = console
-Module = command_console
+Module = console
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Start a debugging python console
diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command/console.py
index f4d0295..fe17dfc 100644
--- a/nikola/plugins/command_console.py
+++ b/nikola/plugins/command/console.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,14 +28,19 @@ from __future__ import print_function, unicode_literals
import os
+from nikola import __version__
from nikola.plugin_categories import Command
+from nikola.utils import get_logger, STDERR_HANDLER
+
+LOGGER = get_logger('console', STDERR_HANDLER)
class Console(Command):
"""Start debugging console."""
name = "console"
shells = ['ipython', 'bpython', 'plain']
- doc_purpose = "Start an interactive python console with access to your site and configuration."
+ doc_purpose = "Start an interactive Python (IPython->bpython->plain) console with access to your site and configuration"
+ header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)"
def ipython(self):
"""IPython shell."""
@@ -41,13 +48,12 @@ class Console(Command):
try:
import conf
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import IPython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
- IPython.embed(header='Nikola Console (conf = configuration, SITE '
- '= site engine)')
+ IPython.embed(header=self.header.format('IPython'))
def bpython(self):
"""bpython shell."""
@@ -55,14 +61,14 @@ class Console(Command):
try:
import conf
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import bpython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
- bpython.embed(banner='Nikola Console (conf = configuration, SITE '
- '= site engine)', locals_=gl)
+ bpython.embed(banner=self.header.format(
+ 'bpython (Slightly Deprecated)'), locals_=gl)
def plain(self):
"""Plain Python shell."""
@@ -73,7 +79,7 @@ class Console(Command):
SITE.scan_posts()
gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import code
try:
@@ -92,8 +98,7 @@ class Console(Command):
except NameError:
pass
- code.interact(local=gl, banner='Nikola Console (conf = '
- 'configuration, SITE = site engine)')
+ code.interact(local=gl, banner=self.header.format('Python'))
def _execute(self, options, args):
"""Start the console."""
diff --git a/nikola/plugins/command_deploy.plugin b/nikola/plugins/command/deploy.plugin
index c8776b5..10cc796 100644
--- a/nikola/plugins/command_deploy.plugin
+++ b/nikola/plugins/command/deploy.plugin
@@ -1,9 +1,9 @@
[Core]
Name = deploy
-Module = command_deploy
+Module = deploy
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Deploy the site
diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py
new file mode 100644
index 0000000..efb909d
--- /dev/null
+++ b/nikola/plugins/command/deploy.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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
+from ast import literal_eval
+import codecs
+from datetime import datetime
+import os
+import sys
+import subprocess
+import time
+import pytz
+
+from blinker import signal
+
+from nikola.plugin_categories import Command
+from nikola.utils import remove_file, get_logger
+
+
+class Deploy(Command):
+ """Deploy site. """
+ name = "deploy"
+
+ doc_usage = ""
+ doc_purpose = "deploy the site"
+
+ logger = None
+
+ def _execute(self, command, args):
+ self.logger = get_logger('deploy', self.site.loghandlers)
+ # Get last successful deploy date
+ timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
+ if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo':
+ self.logger.warn("\nWARNING WARNING WARNING WARNING\n"
+ "You are deploying using the nikolademo Disqus account.\n"
+ "That means you will not be able to moderate the comments in your own site.\n"
+ "And is probably not what you want to do.\n"
+ "Think about it for 5 seconds, I'll wait :-)\n\n")
+ time.sleep(5)
+
+ deploy_drafts = self.site.config.get('DEPLOY_DRAFTS', True)
+ deploy_future = self.site.config.get('DEPLOY_FUTURE', False)
+ if not (deploy_drafts and deploy_future):
+ # Remove drafts and future posts
+ out_dir = self.site.config['OUTPUT_FOLDER']
+ undeployed_posts = []
+ self.site.scan_posts()
+ for post in self.site.timeline:
+ if (not deploy_drafts and post.is_draft) or \
+ (not deploy_future and post.publish_later):
+ remove_file(os.path.join(out_dir, post.destination_path()))
+ remove_file(os.path.join(out_dir, post.source_path))
+ undeployed_posts.append(post)
+
+ for command in self.site.config['DEPLOY_COMMANDS']:
+ self.logger.notice("==> {0}".format(command))
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ self.logger.error('Failed deployment — command {0} '
+ 'returned {1}'.format(e.cmd, e.returncode))
+ sys.exit(e.returncode)
+
+ self.logger.notice("Successful deployment")
+ if self.site.config['TIMEZONE'] is not None:
+ tzinfo = pytz.timezone(self.site.config['TIMEZONE'])
+ else:
+ tzinfo = pytz.UTC
+ try:
+ with open(timestamp_path, 'rb') as inf:
+ last_deploy = literal_eval(inf.read().strip())
+ # this might ignore DST
+ last_deploy = last_deploy.replace(tzinfo=tzinfo)
+ clean = False
+ except Exception:
+ last_deploy = datetime(1970, 1, 1).replace(tzinfo=tzinfo)
+ clean = True
+
+ new_deploy = datetime.now()
+ self._emit_deploy_event(last_deploy, new_deploy, clean, undeployed_posts)
+
+ # Store timestamp of successful deployment
+ with codecs.open(timestamp_path, 'wb+', 'utf8') as outf:
+ outf.write(repr(new_deploy))
+
+ def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None):
+ """ Emit events for all timeline entries newer than last deploy.
+
+ last_deploy: datetime
+ Time stamp of the last successful deployment.
+
+ new_deploy: datetime
+ Time stamp of the current deployment.
+
+ clean: bool
+ True when it appears like deploy is being run after a clean.
+
+ """
+
+ if undeployed is None:
+ undeployed = []
+
+ event = {
+ 'last_deploy': last_deploy,
+ 'new_deploy': new_deploy,
+ 'clean': clean,
+ 'undeployed': undeployed
+ }
+
+ deployed = [
+ entry for entry in self.site.timeline
+ if entry.date > last_deploy and entry not in undeployed
+ ]
+
+ event['deployed'] = deployed
+
+ if len(deployed) > 0 or len(undeployed) > 0:
+ signal('deployed').send(event)
diff --git a/nikola/plugins/command_import_blogger.plugin b/nikola/plugins/command/import_blogger.plugin
index b275a7f..91a7cb6 100644
--- a/nikola/plugins/command_import_blogger.plugin
+++ b/nikola/plugins/command/import_blogger.plugin
@@ -1,10 +1,10 @@
[Core]
Name = import_blogger
-Module = command_import_blogger
+Module = import_blogger
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Import a blogger site from a XML dump.
diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command/import_blogger.py
index ecc4676..53618b4 100644
--- a/nikola/plugins/command_import_blogger.py
+++ b/nikola/plugins/command/import_blogger.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,8 +25,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals, print_function
-import codecs
-import csv
import datetime
import os
import time
@@ -38,30 +38,23 @@ try:
import feedparser
except ImportError:
feedparser = None # NOQA
-from lxml import html
-from mako.template import Template
from nikola.plugin_categories import Command
from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin
-links = {}
+LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER)
-class CommandImportBlogger(Command):
+class CommandImportBlogger(Command, ImportMixin):
"""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.'
- },
+ doc_purpose = "import a blogger dump"
+ cmd_options = ImportMixin.cmd_options + [
{
'name': 'exclude_drafts',
'long': 'no-drafts',
@@ -74,11 +67,9 @@ class CommandImportBlogger(Command):
def _execute(self, options, args):
"""Import a Blogger 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.')
+ req_missing(['feedparser'], 'import Blogger dumps')
return
if not args:
@@ -101,8 +92,11 @@ class CommandImportBlogger(Command):
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))
+ conf_out_path = self.get_configuration_output_path()
+ # if it tracebacks here, look a comment in
+ # basic_import.Import_Mixin.generate_base_site
+ conf_termplate_render = conf_template.render(**self.context)
+ self.write_configuration(conf_out_path, conf_termplate_render)
@classmethod
def get_channel_from_file(cls, filename):
@@ -111,82 +105,36 @@ class CommandImportBlogger(Command):
return feedparser.parse(filename)
@staticmethod
- def configure_redirections(url_map):
- redirections = []
- for k, v in url_map.items():
- # remove the initial "/" because src is a relative file path
- src = (urlparse(k).path + 'index.html')[1:]
- dst = (urlparse(v).path)
- if src == 'index.html':
- print("Can't do a redirect for: {0!r}".format(k))
- else:
- redirections.append((src, dst))
-
- return redirections
-
- def generate_base_site(self):
- if not os.path.exists(self.output_folder):
- os.system('nikola init ' + self.output_folder)
- else:
- self.import_into_existing_site = True
- 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'))
-
- return conf_template
-
- @staticmethod
def populate_context(channel):
+ # may need changes when the template conf.py.in changes
context = {}
context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language
# in the dump
context['BLOG_TITLE'] = channel.feed.title
context['BLOG_DESCRIPTION'] = '' # Missing in the dump
- context['SITE_URL'] = channel.feed.link.rstrip('/')
+ context['SITE_URL'] = channel.feed.link
context['BLOG_EMAIL'] = channel.feed.author_detail.email
context['BLOG_AUTHOR'] = channel.feed.author_detail.name
- context['POST_PAGES'] = '''(
- ("posts/*.html", "posts", "post.tmpl", True),
- ("stories/*.html", "stories", "story.tmpl", False),
- )'''
- context['POST_COMPILERS'] = '''{
- "rest": ('.txt', '.rst'),
- "markdown": ('.md', '.mdown', '.markdown', '.wp'),
- "html": ('.html', '.htm')
- }
- '''
+ context['POSTS'] = '''(
+ ("posts/*.txt", "posts", "post.tmpl"),
+ ("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.html", "posts", "post.tmpl"),
+ )'''
+ context['PAGES'] = '''(
+ ("articles/*.txt", "articles", "story.tmpl"),
+ ("articles/*.rst", "articles", "story.tmpl"),
+ )'''
+ context['COMPILERS'] = '''{
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown', '.wp'),
+ "html": ('.html', '.htm')
+ }
+ '''
+ context['THEME'] = 'bootstrap3'
return context
- @classmethod
- def transform_content(cls, content):
- # No transformations yet
- return content
-
- @classmethod
- def write_content(cls, filename, content):
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
-
- with open(filename, "wb+") as fd:
- fd.write(html.tostring(doc, encoding='utf8'))
-
- @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('{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('{0}\n'.format(description))
-
def import_item(self, item, out_folder=None):
"""Takes an item from the feed and creates a post file."""
if out_folder is None:
@@ -201,8 +149,8 @@ class CommandImportBlogger(Command):
# blogger supports empty titles, which Nikola doesn't
if not title:
- print("Warning: Empty title in post with URL {0}. Using NO_TITLE "
- "as placeholder, please fix.".format(link))
+ LOGGER.warn("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'):
@@ -211,7 +159,7 @@ class CommandImportBlogger(Command):
slug = utils.slugify(link_path)
if not slug: # should never happen
- print("Error converting post:", title)
+ LOGGER.error("Error converting post:", title)
return
description = ''
@@ -239,7 +187,7 @@ class CommandImportBlogger(Command):
out_folder + '/' + slug + '.html'
if is_draft and self.exclude_drafts:
- print('Draft "{0}" will not be imported.'.format(title))
+ LOGGER.notice('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)
@@ -251,8 +199,8 @@ class CommandImportBlogger(Command):
os.path.join(self.output_folder, out_folder, slug + '.html'),
content)
else:
- print('Not going to import "{0}" because it seems to contain'
- ' no content.'.format(title))
+ LOGGER.warn('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
@@ -274,35 +222,8 @@ class CommandImportBlogger(Command):
# FIXME: not importing comments. Does blogger support "pages"?
pass
else:
- print("Unknown post_type:", post_type)
+ LOGGER.warn("Unknown post_type:", post_type)
def import_posts(self, channel):
for item in channel.entries:
self.process_item(item)
-
- @staticmethod
- def write_urlmap_csv(output_file, url_map):
- with codecs.open(output_file, 'w+', 'utf8') as fd:
- csv_writer = csv.writer(fd)
- for item in url_map.items():
- csv_writer.writerow(item)
-
- def get_configuration_output_path(self):
- if not self.import_into_existing_site:
- filename = 'conf.py'
- else:
- filename = 'conf.py.blogger_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: ' + config_output_path)
-
- return config_output_path
-
- @staticmethod
- def write_configuration(filename, rendered_template):
- with codecs.open(filename, 'w+', 'utf8') as fd:
- fd.write(rendered_template)
-
-
-def replacer(dst):
- return links.get(dst, dst)
diff --git a/nikola/plugins/command/import_feed.plugin b/nikola/plugins/command/import_feed.plugin
new file mode 100644
index 0000000..26e570a
--- /dev/null
+++ b/nikola/plugins/command/import_feed.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = import_feed
+Module = import_feed
+
+[Documentation]
+Author = Grzegorz Śliwiński
+Version = 0.1
+Website = http://www.fizyk.net.pl/
+Description = Import a blog posts from a RSS/Atom dump
+
diff --git a/nikola/plugins/command/import_feed.py b/nikola/plugins/command/import_feed.py
new file mode 100644
index 0000000..b25d9ec
--- /dev/null
+++ b/nikola/plugins/command/import_feed.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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, print_function
+import datetime
+import os
+import time
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse # NOQA
+
+try:
+ import feedparser
+except ImportError:
+ feedparser = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin
+
+LOGGER = utils.get_logger('import_feed', utils.STDERR_HANDLER)
+
+
+class CommandImportFeed(Command, ImportMixin):
+ """Import a feed dump."""
+
+ name = "import_feed"
+ needs_config = False
+ doc_usage = "[options] feed_file"
+ doc_purpose = "import a RSS/Atom dump"
+ cmd_options = ImportMixin.cmd_options
+
+ def _execute(self, options, args):
+ '''
+ Import Atom/RSS feed
+ '''
+ if feedparser is None:
+ req_missing(['feedparser'], 'import feeds')
+ return
+
+ if not args:
+ print(self.help())
+ return
+
+ options['filename'] = args[0]
+ self.feed_export_file = options['filename']
+ self.output_folder = options['output_folder']
+ self.import_into_existing_site = False
+ self.url_map = {}
+ channel = self.get_channel_from_file(self.feed_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_configuration(self.get_configuration_output_path(
+ ), conf_template.render(**self.context))
+
+ @classmethod
+ def get_channel_from_file(cls, filename):
+ return feedparser.parse(filename)
+
+ @staticmethod
+ def populate_context(channel):
+ context = {}
+ context['DEFAULT_LANG'] = channel.feed.title_detail.language \
+ if channel.feed.title_detail.language else 'en'
+ context['BLOG_TITLE'] = channel.feed.title
+
+ context['BLOG_DESCRIPTION'] = channel.feed.get('subtitle', '')
+ context['SITE_URL'] = channel.feed.get('link', '').rstrip('/')
+ context['BLOG_EMAIL'] = channel.feed.author_detail.get('email', '') if 'author_detail' in channel.feed else ''
+ context['BLOG_AUTHOR'] = channel.feed.author_detail.get('name', '') if 'author_detail' in channel.feed else ''
+
+ context['POST_PAGES'] = '''(
+ ("posts/*.html", "posts", "post.tmpl", True),
+ ("stories/*.html", "stories", "story.tmpl", False),
+ )'''
+ context['COMPILERS'] = '''{
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown', '.wp'),
+ "html": ('.html', '.htm')
+ }
+ '''
+
+ return context
+
+ def import_posts(self, channel):
+ for item in channel.entries:
+ self.process_item(item)
+
+ def process_item(self, item):
+ self.import_item(item, 'posts')
+
+ def import_item(self, item, out_folder=None):
+ """Takes an item from the feed and creates a post file."""
+ if out_folder is None:
+ out_folder = 'posts'
+
+ # link is something like http://foo.com/2012/09/01/hello-world/
+ # So, take the path, utils.slugify it, and that's our slug
+ link = item.link
+ link_path = urlparse(link).path
+
+ title = item.title
+
+ # blogger supports empty titles, which Nikola doesn't
+ if not title:
+ LOGGER.warn("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'):
+ link_path = link_path[:-5]
+
+ slug = utils.slugify(link_path)
+
+ if not slug: # should never happen
+ LOGGER.error("Error converting post:", title)
+ return
+
+ description = ''
+ post_date = datetime.datetime.fromtimestamp(time.mktime(
+ item.published_parsed))
+ if item.get('content'):
+ for candidate in item.get('content', []):
+ content = candidate.value
+ break
+ # FIXME: handle attachments
+ elif item.get('summary'):
+ content = item.get('summary')
+
+ tags = []
+ for tag in item.get('tags', []):
+ tags.append(tag.term)
+
+ if item.get('app_draft'):
+ tags.append('draft')
+ is_draft = True
+ else:
+ is_draft = False
+
+ self.url_map[link] = self.context['SITE_URL'] + '/' + \
+ out_folder + '/' + slug + '.html'
+
+ if is_draft and self.exclude_drafts:
+ LOGGER.notice('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)
+
+ self.write_metadata(os.path.join(self.output_folder, out_folder,
+ slug + '.meta'),
+ title, slug, post_date, description, tags)
+ self.write_content(
+ os.path.join(self.output_folder, out_folder, slug + '.html'),
+ content)
+ else:
+ LOGGER.warn('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
+
+ @staticmethod
+ def write_metadata(filename, title, slug, post_date, description, tags):
+ ImportMixin.write_metadata(filename,
+ title,
+ slug,
+ post_date.strftime(r'%Y/%m/%d %H:%m:%S'),
+ description,
+ tags)
diff --git a/nikola/plugins/command_import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin
index ff7cdca..fadc759 100644
--- a/nikola/plugins/command_import_wordpress.plugin
+++ b/nikola/plugins/command/import_wordpress.plugin
@@ -1,10 +1,10 @@
[Core]
Name = import_wordpress
-Module = command_import_wordpress
+Module = import_wordpress
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Import a wordpress site from a XML dump (requires markdown).
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command/import_wordpress.py
index b45fe78..4f32198 100644
--- a/nikola/plugins/command_import_wordpress.py
+++ b/nikola/plugins/command/import_wordpress.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,46 +25,43 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals, print_function
-import codecs
-import csv
-import datetime
import os
import re
+import sys
+from lxml import etree
try:
from urlparse import urlparse
+ from urllib import unquote
except ImportError:
- from urllib.parse import urlparse # NOQA
-
-from lxml import etree, html
-from mako.template import Template
+ from urllib.parse import urlparse, unquote # NOQA
try:
import requests
except ImportError:
requests = None # NOQA
+try:
+ import phpserialize
+except ImportError:
+ phpserialize = None # NOQA
+
from nikola.plugin_categories import Command
from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin, links
-links = {}
+LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER)
-class CommandImportWordpress(Command):
- """Import a wordpress dump."""
+class CommandImportWordpress(Command, ImportMixin):
+ """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.'
- },
+ doc_purpose = "import a WordPress dump"
+ cmd_options = ImportMixin.cmd_options + [
{
'name': 'exclude_drafts',
'long': 'no-drafts',
@@ -88,13 +87,7 @@ class CommandImportWordpress(Command):
]
def _execute(self, options={}, args=[]):
- """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
-
+ """Import a WordPress blog from an export file into a Nikola site."""
if not args:
print(self.help())
return
@@ -106,17 +99,38 @@ class CommandImportWordpress(Command):
options['output_folder'] = args.pop(0)
if args:
- print('You specified additional arguments ({0}). Please consider '
- 'putting these arguments before the filename if you '
- 'are running into problems.'.format(args))
+ LOGGER.warn('You specified additional arguments ({0}). Please consider '
+ 'putting these arguments before the filename if you '
+ 'are running into problems.'.format(args))
+
+ self.import_into_existing_site = False
+ self.url_map = {}
+ self.timezone = None
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 = {}
+ self.no_downloads = options.get('no_downloads', False)
+
+ if not self.no_downloads:
+ def show_info_about_mising_module(modulename):
+ LOGGER.error(
+ 'To use the "{commandname}" command, you have to install '
+ 'the "{package}" package or supply the "--no-downloads" '
+ 'option.'.format(
+ commandname=self.name,
+ package=modulename)
+ )
+
+ if requests is None and phpserialize is None:
+ req_missing(['requests', 'phpserialize'], 'import WordPress dumps without --no-downloads')
+ elif requests is None:
+ req_missing(['requests'], 'import WordPress dumps without --no-downloads')
+ elif phpserialize is None:
+ req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads')
+
channel = self.get_channel_from_file(self.wordpress_export_file)
self.context = self.populate_context(channel)
conf_template = self.generate_base_site()
@@ -130,6 +144,10 @@ class CommandImportWordpress(Command):
rendered_template = conf_template.render(**self.context)
rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ',
rendered_template)
+ if self.timezone:
+ rendered_template = re.sub('# TIMEZONE = \'Europe/Zurich\'',
+ 'TIMEZONE = \'' + self.timezone + '\'',
+ rendered_template)
self.write_configuration(self.get_configuration_output_path(),
rendered_template)
@@ -173,33 +191,6 @@ class CommandImportWordpress(Command):
return channel
@staticmethod
- def configure_redirections(url_map):
- redirections = []
- for k, v in url_map.items():
- # remove the initial "/" because src is a relative file path
- src = (urlparse(k).path + 'index.html')[1:]
- dst = (urlparse(v).path)
- if src == 'index.html':
- print("Can't do a redirect for: {0!r}".format(k))
- else:
- redirections.append((src, dst))
-
- return redirections
-
- def generate_base_site(self):
- if not os.path.exists(self.output_folder):
- os.system('nikola init ' + self.output_folder)
- else:
- self.import_into_existing_site = True
- 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'))
-
- return conf_template
-
- @staticmethod
def populate_context(channel):
wordpress_namespace = channel.nsmap['wp']
@@ -212,8 +203,11 @@ class CommandImportWordpress(Command):
context['BASE_URL'] = get_text_tag(channel, 'link', '#')
if not context['BASE_URL']:
base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace))
- context['BASE_URL'] = get_text_tag(base_site_url, None, "http://foo.com")
+ context['BASE_URL'] = get_text_tag(base_site_url,
+ None,
+ "http://foo.com")
context['SITE_URL'] = context['BASE_URL']
+ context['THEME'] = 'bootstrap3'
author = channel.find('{{{0}}}author'.format(wordpress_namespace))
context['BLOG_EMAIL'] = get_text_tag(
@@ -224,11 +218,13 @@ class CommandImportWordpress(Command):
author,
'{{{0}}}author_display_name'.format(wordpress_namespace),
"Joe Example")
- context['POST_PAGES'] = '''(
- ("posts/*.wp", "posts", "post.tmpl", True),
- ("stories/*.wp", "stories", "story.tmpl", False),
+ context['POSTS'] = '''(
+ ("posts/*.wp", "posts", "post.tmpl"),
)'''
- context['POST_COMPILERS'] = '''{
+ context['PAGES'] = '''(
+ ("stories/*.wp", "stories", "story.tmpl"),
+ )'''
+ context['COMPILERS'] = '''{
"rest": ('.txt', '.rst'),
"markdown": ('.md', '.mdown', '.markdown', '.wp'),
"html": ('.html', '.htm')
@@ -245,8 +241,7 @@ class CommandImportWordpress(Command):
with open(dst_path, 'wb+') as fd:
fd.write(requests.get(url).content)
except requests.exceptions.ConnectionError as err:
- print("Downloading {0} to {1} failed: {2}".format(url, dst_path,
- err))
+ LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err))
def import_attachment(self, item, wordpress_namespace):
url = get_text_tag(
@@ -257,14 +252,67 @@ class CommandImportWordpress(Command):
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 {0} => {1}".format(url, dst_path))
+ utils.makedirs(dst_dir)
+ LOGGER.notice("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
links[url] = '/' + dst_url
+ self.download_additional_image_sizes(
+ item,
+ wordpress_namespace,
+ os.path.dirname(url)
+ )
+
+ def download_additional_image_sizes(self, item, wordpress_namespace, source_path):
+ if phpserialize is None:
+ return
+
+ additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace))
+
+ if additional_metadata is None:
+ return
+
+ for element in additional_metadata:
+ meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace))
+ if meta_key is not None and meta_key.text == '_wp_attachment_metadata':
+ meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace))
+
+ if meta_value is None:
+ continue
+
+ # Someone from Wordpress thought it was a good idea
+ # serialize PHP objects into that metadata field. Given
+ # that the export should give you the power to insert
+ # your blogging into another site or system its not.
+ # Why don't they just use JSON?
+ if sys.version_info[0] == 2:
+ metadata = phpserialize.loads(meta_value.text)
+ size_key = 'sizes'
+ file_key = 'file'
+ else:
+ metadata = phpserialize.loads(meta_value.text.encode('UTF-8'))
+ size_key = b'sizes'
+ file_key = b'file'
+
+ if not size_key in metadata:
+ continue
+
+ for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]:
+ url = '/'.join([source_path, filename.decode('utf-8')])
+
+ path = urlparse(url).path
+ dst_path = os.path.join(*([self.output_folder, 'files']
+ + list(path.split('/'))))
+ dst_dir = os.path.dirname(dst_path)
+ utils.makedirs(dst_dir)
+ LOGGER.notice("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[url] = '/' + dst_url
+ links[url] = '/' + dst_url
+
@staticmethod
def transform_sourcecode(content):
new_content = re.sub('\[sourcecode language="([^"]+)"\]',
@@ -293,27 +341,6 @@ class CommandImportWordpress(Command):
new_content = self.transform_multiple_newlines(new_content)
return new_content
- @classmethod
- def write_content(cls, filename, content):
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
-
- with open(filename, "wb+") as fd:
- fd.write(html.tostring(doc, encoding='utf8'))
-
- @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('{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('{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."""
if out_folder is None:
@@ -323,12 +350,13 @@ class CommandImportWordpress(Command):
# link is something like http://foo.com/2012/09/01/hello-world/
# So, take the path, utils.slugify it, and that's our slug
link = get_text_tag(item, 'link', None)
- path = urlparse(link).path
+ path = unquote(urlparse(link).path)
# In python 2, path is a str. slug requires a unicode
- # object. Luckily, paths are also ASCII
+ # object. According to wikipedia, unquoted strings will
+ # usually be UTF8
if isinstance(path, utils.bytes_str):
- path = path.decode('ASCII')
+ path = path.decode('utf8')
slug = utils.slugify(path)
if not slug: # it happens if the post has no "nice" URL
slug = get_text_tag(
@@ -337,12 +365,15 @@ class CommandImportWordpress(Command):
slug = get_text_tag(
item, '{{{0}}}post_id'.format(wordpress_namespace), None)
if not slug: # should never happen
- print("Error converting post:", title)
+ LOGGER.error("Error converting post:", title)
return
description = get_text_tag(item, 'description', '')
post_date = get_text_tag(
item, '{{{0}}}post_date'.format(wordpress_namespace), None)
+ dt = utils.to_datetime(post_date)
+ if dt.tzinfo and self.timezone is None:
+ self.timezone = utils.get_tzname(dt)
status = get_text_tag(
item, '{{{0}}}status'.format(wordpress_namespace), 'publish')
content = get_text_tag(
@@ -350,7 +381,7 @@ class CommandImportWordpress(Command):
tags = []
if status == 'trash':
- print('Trashed post "{0}" will not be imported.'.format(title))
+ LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title))
return
elif status != 'publish':
tags.append('draft')
@@ -365,7 +396,7 @@ class CommandImportWordpress(Command):
tags.append(text)
if is_draft and self.exclude_drafts:
- print('Draft "{0}" will not be imported.'.format(title))
+ LOGGER.notice('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'] + '/' + \
@@ -380,8 +411,8 @@ class CommandImportWordpress(Command):
os.path.join(self.output_folder, out_folder, slug + '.wp'),
content)
else:
- print('Not going to import "{0}" because it seems to contain'
- ' no content.'.format(title))
+ LOGGER.warn('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:
@@ -401,33 +432,6 @@ class CommandImportWordpress(Command):
for item in channel.findall('item'):
self.process_item(item)
- @staticmethod
- def write_urlmap_csv(output_file, url_map):
- with codecs.open(output_file, 'w+', 'utf8') as fd:
- csv_writer = csv.writer(fd)
- for item in url_map.items():
- csv_writer.writerow(item)
-
- def get_configuration_output_path(self):
- if not self.import_into_existing_site:
- filename = 'conf.py'
- else:
- 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:', config_output_path)
-
- return config_output_path
-
- @staticmethod
- def write_configuration(filename, rendered_template):
- with codecs.open(filename, 'w+', 'utf8') as fd:
- fd.write(rendered_template)
-
-
-def replacer(dst):
- return links.get(dst, dst)
-
def get_text_tag(tag, name, default):
if tag is None:
diff --git a/nikola/plugins/command_init.plugin b/nikola/plugins/command/init.plugin
index f4adf4a..a539f51 100644
--- a/nikola/plugins/command_init.plugin
+++ b/nikola/plugins/command/init.plugin
@@ -1,9 +1,9 @@
[Core]
Name = init
-Module = command_init
+Module = init
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create a new site.
diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command/init.py
index bc36266..1873ec4 100644
--- a/nikola/plugins/command_init.py
+++ b/nikola/plugins/command/init.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,6 +33,10 @@ from mako.template import Template
import nikola
from nikola.plugin_categories import Command
+from nikola.utils import get_logger, makedirs, STDERR_HANDLER
+from nikola.winutils import fix_git_symlinked
+
+LOGGER = get_logger('init', STDERR_HANDLER)
class CommandInit(Command):
@@ -40,7 +46,7 @@ class CommandInit(Command):
doc_usage = "[--demo] folder"
needs_config = False
- doc_purpose = """Create a Nikola site in the specified folder."""
+ doc_purpose = "create a Nikola site in the specified folder"
cmd_options = [
{
'name': 'demo',
@@ -54,25 +60,33 @@ class CommandInit(Command):
SAMPLE_CONF = {
'BLOG_AUTHOR': "Your Name",
'BLOG_TITLE': "Demo Site",
- 'SITE_URL': "http://nikola.ralsina.com.ar",
+ 'SITE_URL': "http://getnikola.com/",
'BLOG_EMAIL': "joe@demo.site",
'BLOG_DESCRIPTION': "This is a demo site for Nikola.",
'DEFAULT_LANG': "en",
+ 'THEME': 'bootstrap3',
- 'POST_PAGES': """(
- ("posts/*.txt", "posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "story.tmpl", False),
+ 'POSTS': """(
+ ("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.txt", "posts", "post.tmpl"),
)""",
-
- 'POST_COMPILERS': """{
- "rest": ('.txt', '.rst'),
+ 'PAGES': """(
+ ("stories/*.rst", "stories", "story.tmpl"),
+ ("stories/*.txt", "stories", "story.tmpl"),
+)""",
+ 'COMPILERS': """{
+ "rest": ('.rst', '.txt'),
"markdown": ('.md', '.mdown', '.markdown'),
"textile": ('.textile',),
"txt2tags": ('.t2t',),
"bbcode": ('.bb',),
"wiki": ('.wiki',),
"ipynb": ('.ipynb',),
- "html": ('.html', '.htm')
+ "html": ('.html', '.htm'),
+ # Pandoc detects the input from the source filename
+ # but is disabled by default as it would conflict
+ # with many of the others.
+ # "pandoc": ('.rst', '.md', '.txt'),
}""",
'REDIRECTIONS': '[]',
}
@@ -82,6 +96,7 @@ class CommandInit(Command):
lib_path = cls.get_path_to_nikola_modules()
src = os.path.join(lib_path, 'data', 'samplesite')
shutil.copytree(src, target)
+ fix_git_symlinked(src, target)
@classmethod
def create_configuration(cls, target):
@@ -95,7 +110,7 @@ class CommandInit(Command):
@classmethod
def create_empty_site(cls, target):
for folder in ('files', 'galleries', 'listings', 'posts', 'stories'):
- os.makedirs(os.path.join(target, folder))
+ makedirs(os.path.join(target, folder))
@staticmethod
def get_path_to_nikola_modules():
@@ -112,11 +127,11 @@ class CommandInit(Command):
else:
if not options or not options.get('demo'):
self.create_empty_site(target)
- print('Created empty site at {0}.'.format(target))
+ LOGGER.notice('Created empty site at {0}.'.format(target))
else:
self.copy_sample_site(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.")
+ LOGGER.notice("A new site with example data has been created at "
+ "{0}.".format(target))
+ LOGGER.notice("See README.txt in that folder for more information.")
self.create_configuration(target)
diff --git a/nikola/plugins/command/install_plugin.plugin b/nikola/plugins/command/install_plugin.plugin
new file mode 100644
index 0000000..3dbabd8
--- /dev/null
+++ b/nikola/plugins/command/install_plugin.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = install_plugin
+Module = install_plugin
+
+[Documentation]
+Author = Roberto Alsina and Chris Warrick
+Version = 0.1
+Website = http://getnikola.com
+Description = Install a plugin into the current site.
+
diff --git a/nikola/plugins/command/install_plugin.py b/nikola/plugins/command/install_plugin.py
new file mode 100644
index 0000000..fdbd0b7
--- /dev/null
+++ b/nikola/plugins/command/install_plugin.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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
+import codecs
+import os
+import json
+import shutil
+import subprocess
+from io import BytesIO
+
+import pygments
+from pygments.lexers import PythonLexer
+from pygments.formatters import TerminalFormatter
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('install_plugin', utils.STDERR_HANDLER)
+
+
+# Stolen from textwrap in Python 3.3.2.
+def indent(text, prefix, predicate=None): # NOQA
+ """Adds 'prefix' to the beginning of selected lines in 'text'.
+
+ If 'predicate' is provided, 'prefix' will only be added to the lines
+ where 'predicate(line)' is True. If 'predicate' is not provided,
+ it will default to adding 'prefix' to all non-empty lines that do not
+ consist solely of whitespace characters.
+ """
+ if predicate is None:
+ def predicate(line):
+ return line.strip()
+
+ def prefixed_lines():
+ for line in text.splitlines(True):
+ yield (prefix + line if predicate(line) else line)
+ return ''.join(prefixed_lines())
+
+
+class CommandInstallPlugin(Command):
+ """Install a plugin."""
+
+ name = "install_plugin"
+ doc_usage = "[[-u] plugin_name] | [[-u] -l]"
+ doc_purpose = "install plugin into current site"
+ output_dir = 'plugins'
+ cmd_options = [
+ {
+ 'name': 'list',
+ 'short': 'l',
+ 'long': 'list',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Show list of available plugins.'
+ },
+ {
+ 'name': 'url',
+ 'short': 'u',
+ 'long': 'url',
+ 'type': str,
+ 'help': "URL for the plugin repository (default: "
+ "http://plugins.getnikola.com/v6/plugins.json)",
+ 'default': 'http://plugins.getnikola.com/v6/plugins.json'
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Install plugin into current site."""
+ if requests is None:
+ utils.req_missing(['requests'], 'install plugins')
+
+ listing = options['list']
+ url = options['url']
+ if args:
+ name = args[0]
+ else:
+ name = None
+
+ if name is None and not listing:
+ LOGGER.error("This command needs either a plugin name or the -l option.")
+ return False
+ data = requests.get(url).text
+ data = json.loads(data)
+ if listing:
+ print("Plugins:")
+ print("--------")
+ for plugin in sorted(data.keys()):
+ print(plugin)
+ return True
+ else:
+ self.do_install(name, data)
+
+ def do_install(self, name, data):
+ if name in data:
+ utils.makedirs(self.output_dir)
+ LOGGER.notice('Downloading: ' + data[name])
+ zip_file = BytesIO()
+ zip_file.write(requests.get(data[name]).content)
+ LOGGER.notice('Extracting: {0} into plugins'.format(name))
+ utils.extract_all(zip_file, 'plugins')
+ dest_path = os.path.join('plugins', name)
+ else:
+ try:
+ plugin_path = utils.get_plugin_path(name)
+ except:
+ LOGGER.error("Can't find plugin " + name)
+ return False
+
+ utils.makedirs(self.output_dir)
+ dest_path = os.path.join(self.output_dir, name)
+ if os.path.exists(dest_path):
+ LOGGER.error("{0} is already installed".format(name))
+ return False
+
+ LOGGER.notice('Copying {0} into plugins'.format(plugin_path))
+ shutil.copytree(plugin_path, dest_path)
+
+ reqpath = os.path.join(dest_path, 'requirements.txt')
+ print(reqpath)
+ if os.path.exists(reqpath):
+ LOGGER.notice('This plugin has Python dependencies.')
+ LOGGER.notice('Installing dependencies with pip...')
+ try:
+ subprocess.check_call(('pip', 'install', '-r', reqpath))
+ except subprocess.CalledProcessError:
+ LOGGER.error('Could not install the dependencies.')
+ print('Contents of the requirements.txt file:\n')
+ with codecs.open(reqpath, 'rb', 'utf-8') as fh:
+ print(indent(fh.read(), 4 * ' '))
+ print('You have to install those yourself or through a '
+ 'package manager.')
+ else:
+ LOGGER.notice('Dependency installation succeeded.')
+ reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt')
+ if os.path.exists(reqnpypath):
+ LOGGER.notice('This plugin has third-party '
+ 'dependencies you need to install '
+ 'manually.')
+ print('Contents of the requirements-nonpy.txt file:\n')
+ with codecs.open(reqnpypath, 'rb', 'utf-8') as fh:
+ for l in fh.readlines():
+ i, j = l.split('::')
+ print(indent(i.strip(), 4 * ' '))
+ print(indent(j.strip(), 8 * ' '))
+ print()
+
+ print('You have to install those yourself or through a package '
+ 'manager.')
+ confpypath = os.path.join(dest_path, 'conf.py.sample')
+ if os.path.exists(confpypath):
+ LOGGER.notice('This plugin has a sample config file.')
+ print('Contents of the conf.py.sample file:\n')
+ with codecs.open(confpypath, 'rb', 'utf-8') as fh:
+ print(indent(pygments.highlight(
+ fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' '))
+ return True
diff --git a/nikola/plugins/command_install_theme.plugin b/nikola/plugins/command/install_theme.plugin
index f010074..84b2623 100644
--- a/nikola/plugins/command_install_theme.plugin
+++ b/nikola/plugins/command/install_theme.plugin
@@ -1,10 +1,10 @@
[Core]
Name = install_theme
-Module = command_install_theme
+Module = install_theme
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Install a theme into the current site.
diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py
new file mode 100644
index 0000000..a9d835a
--- /dev/null
+++ b/nikola/plugins/command/install_theme.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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
+import os
+import json
+import shutil
+import codecs
+from io import BytesIO
+
+import pygments
+from pygments.lexers import PythonLexer
+from pygments.formatters import TerminalFormatter
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER)
+
+
+# Stolen from textwrap in Python 3.3.2.
+def indent(text, prefix, predicate=None): # NOQA
+ """Adds 'prefix' to the beginning of selected lines in 'text'.
+
+ If 'predicate' is provided, 'prefix' will only be added to the lines
+ where 'predicate(line)' is True. If 'predicate' is not provided,
+ it will default to adding 'prefix' to all non-empty lines that do not
+ consist solely of whitespace characters.
+ """
+ if predicate is None:
+ def predicate(line):
+ return line.strip()
+
+ def prefixed_lines():
+ for line in text.splitlines(True):
+ yield (prefix + line if predicate(line) else line)
+ return ''.join(prefixed_lines())
+
+
+class CommandInstallTheme(Command):
+ """Install a theme."""
+
+ name = "install_theme"
+ doc_usage = "[[-u] theme_name] | [[-u] -l]"
+ doc_purpose = "install theme into current site"
+ output_dir = 'themes'
+ 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://themes.getnikola.com/v6/themes.json)",
+ 'default': 'http://themes.getnikola.com/v6/themes.json'
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Install theme into current site."""
+ if requests is None:
+ utils.req_missing(['requests'], 'install themes')
+
+ listing = options['list']
+ url = options['url']
+ if args:
+ name = args[0]
+ else:
+ name = None
+
+ if name is None and not listing:
+ LOGGER.error("This command needs either a theme name or the -l option.")
+ return False
+ data = requests.get(url).text
+ data = json.loads(data)
+ if listing:
+ print("Themes:")
+ print("-------")
+ for theme in sorted(data.keys()):
+ print(theme)
+ return True
+ else:
+ self.do_install(name, data)
+ # See if the theme's parent is available. If not, install it
+ while True:
+ parent_name = utils.get_parent_theme_name(name)
+ if parent_name is None:
+ break
+ try:
+ utils.get_theme_path(parent_name)
+ break
+ except: # Not available
+ self.do_install(parent_name, data)
+ name = parent_name
+
+ def do_install(self, name, data):
+ if name in data:
+ utils.makedirs(self.output_dir)
+ LOGGER.notice('Downloading: ' + data[name])
+ zip_file = BytesIO()
+ zip_file.write(requests.get(data[name]).content)
+ LOGGER.notice('Extracting: {0} into themes'.format(name))
+ utils.extract_all(zip_file)
+ dest_path = os.path.join('themes', name)
+ else:
+ try:
+ theme_path = utils.get_theme_path(name)
+ except:
+ LOGGER.error("Can't find theme " + name)
+ return False
+
+ utils.makedirs(self.output_dir)
+ dest_path = os.path.join(self.output_dir, name)
+ if os.path.exists(dest_path):
+ LOGGER.error("{0} is already installed".format(name))
+ return False
+
+ LOGGER.notice('Copying {0} into themes'.format(theme_path))
+ shutil.copytree(theme_path, dest_path)
+ confpypath = os.path.join(dest_path, 'conf.py.sample')
+ if os.path.exists(confpypath):
+ LOGGER.notice('This plugin has a sample config file.')
+ print('Contents of the conf.py.sample file:\n')
+ with codecs.open(confpypath, 'rb', 'utf-8') as fh:
+ print(indent(pygments.highlight(
+ fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' '))
+ return True
diff --git a/nikola/plugins/command/mincss.plugin b/nikola/plugins/command/mincss.plugin
new file mode 100644
index 0000000..d394d06
--- /dev/null
+++ b/nikola/plugins/command/mincss.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = mincss
+Module = mincss
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Apply mincss to the generated site
+
diff --git a/nikola/plugins/command/mincss.py b/nikola/plugins/command/mincss.py
new file mode 100644
index 0000000..5c9a7cb
--- /dev/null
+++ b/nikola/plugins/command/mincss.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 os
+import sys
+
+try:
+ from mincss.processor import Processor
+except ImportError:
+ Processor = None
+
+from nikola.plugin_categories import Command
+from nikola.utils import req_missing, get_logger, STDERR_HANDLER
+
+
+class CommandMincss(Command):
+ """Check the generated site."""
+ name = "mincss"
+
+ doc_usage = ""
+ doc_purpose = "apply mincss to the generated site"
+
+ logger = get_logger('mincss', STDERR_HANDLER)
+
+ def _execute(self, options, args):
+ """Apply mincss the generated site."""
+ output_folder = self.site.config['OUTPUT_FOLDER']
+ if Processor is None:
+ req_missing(['mincss'], 'use the "mincss" command')
+ return
+
+ p = Processor(preserve_remote_urls=False)
+ urls = []
+ css_files = {}
+ for root, dirs, files in os.walk(output_folder):
+ for f in files:
+ url = os.path.join(root, f)
+ if url.endswith('.css'):
+ fname = os.path.basename(url)
+ if fname in css_files:
+ self.logger.error("You have two CSS files with the same name and that confuses me.")
+ sys.exit(1)
+ css_files[fname] = url
+ if not f.endswith('.html'):
+ continue
+ urls.append(url)
+ p.process(*urls)
+ for inline in p.links:
+ fname = os.path.basename(inline.href)
+ with open(css_files[fname], 'wb+') as outf:
+ outf.write(inline.after)
diff --git a/nikola/plugins/command_new_post.plugin b/nikola/plugins/command/new_post.plugin
index 6d70aff..ec35c35 100644
--- a/nikola/plugins/command_new_post.plugin
+++ b/nikola/plugins/command/new_post.plugin
@@ -1,10 +1,10 @@
[Core]
Name = new_post
-Module = command_new_post
+Module = new_post
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create a new post.
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command/new_post.py
index 933a51a..ea0f3de 100644
--- a/nikola/plugins/command_new_post.py
+++ b/nikola/plugins/command/new_post.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,20 +30,24 @@ import datetime
import os
import sys
+from blinker import signal
+
from nikola.plugin_categories import Command
from nikola import utils
+LOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER)
+
-def filter_post_pages(compiler, is_post, post_compilers, post_pages):
+def filter_post_pages(compiler, is_post, compilers, post_pages):
"""Given a compiler ("markdown", "rest"), and whether it's meant for
- a post or a page, and post_compilers, return the correct entry from
+ a post or a page, and compilers, return the correct entry from
post_pages."""
# First throw away all the post_pages with the wrong is_post
filtered = [entry for entry in post_pages if entry[3] == is_post]
# These are the extensions supported by the required format
- extensions = post_compilers[compiler]
+ extensions = compilers[compiler]
# Throw away the post_pages with the wrong extensions
filtered = [entry for entry in filtered if any([ext in entry[0] for ext in
@@ -51,13 +57,13 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages):
type_name = "post" if is_post else "page"
raise Exception("Can't find a way, using your configuration, to create "
"a {0} in format {1}. You may want to tweak "
- "post_compilers or post_pages in conf.py".format(
+ "COMPILERS or POSTS/PAGES in conf.py".format(
type_name, compiler))
return filtered[0]
-def get_default_compiler(is_post, post_compilers, post_pages):
- """Given post_compilers and post_pages, return a reasonable
+def get_default_compiler(is_post, compilers, post_pages):
+ """Given compilers and post_pages, return a reasonable
default compiler for this kind of post/page.
"""
@@ -67,19 +73,60 @@ def get_default_compiler(is_post, post_compilers, post_pages):
# Get extensions in filtered post_pages until one matches a compiler
for entry in filtered:
extension = os.path.splitext(entry[0])[-1]
- for compiler, extensions in post_compilers.items():
+ for compiler, extensions in compilers.items():
if extension in extensions:
return compiler
# No idea, back to default behaviour
return 'rest'
+def get_date(schedule=False, rule=None, last_date=None, force_today=False):
+ """Returns a date stamp, given a recurrence rule.
+
+ schedule - bool:
+ whether to use the recurrence rule or not
+
+ rule - str:
+ an iCal RRULE string that specifies the rule for scheduling posts
+
+ last_date - datetime:
+ timestamp of the last post
+
+ force_today - bool:
+ tries to schedule a post to today, if possible, even if the scheduled
+ time has already passed in the day.
+ """
+
+ date = now = datetime.datetime.now()
+ if schedule:
+ try:
+ from dateutil import rrule
+ except ImportError:
+ LOGGER.error('To use the --schedule switch of new_post, '
+ 'you have to install the "dateutil" package.')
+ rrule = None
+ if schedule and rrule and rule:
+ if last_date and last_date.tzinfo:
+ # strip tzinfo for comparisons
+ last_date = last_date.replace(tzinfo=None)
+ try:
+ rule_ = rrule.rrulestr(rule, dtstart=last_date)
+ except Exception:
+ LOGGER.error('Unable to parse rule string, using current time.')
+ else:
+ # Try to post today, instead of tomorrow, if no other post today.
+ if force_today:
+ now = now.replace(hour=0, minute=0, second=0, microsecond=0)
+ date = rule_.after(max(now, last_date or now), last_date is None)
+ return date.strftime('%Y/%m/%d %H:%M:%S')
+
+
class CommandNewPost(Command):
"""Create a new post."""
name = "new_post"
doc_usage = "[options] [path]"
- doc_purpose = "Create a new blog post or site page."
+ doc_purpose = "create a new blog post or site page"
cmd_options = [
{
'name': 'is_page',
@@ -126,12 +173,19 @@ class CommandNewPost(Command):
'default': '',
'help': 'Markup format for post, one of rest, markdown, wiki, '
'bbcode, html, textile, txt2tags',
- }
+ },
+ {
+ 'name': 'schedule',
+ 'short': 's',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Schedule post based on recurrence rule'
+ },
+
]
def _execute(self, options, args):
"""Create a new post or page."""
-
compiler_names = [p.name for p in
self.site.plugin_manager.getPluginsOfCategory(
"PageCompiler")]
@@ -161,18 +215,18 @@ class CommandNewPost(Command):
if not post_format: # Issue #400
post_format = get_default_compiler(
is_post,
- self.site.config['post_compilers'],
+ self.site.config['COMPILERS'],
self.site.config['post_pages'])
if post_format not in compiler_names:
- print("ERROR: Unknown post format " + post_format)
+ LOGGER.error("Unknown post format " + post_format)
return
compiler_plugin = self.site.plugin_manager.getPluginByName(
post_format, "PageCompiler").plugin_object
# Guess where we should put this
entry = filter_post_pages(post_format, is_post,
- self.site.config['post_compilers'],
+ self.site.config['COMPILERS'],
self.site.config['post_pages'])
print("Creating New Post")
@@ -193,7 +247,14 @@ class CommandNewPost(Command):
if isinstance(path, utils.bytes_str):
path = path.decode(sys.stdin.encoding)
slug = utils.slugify(os.path.splitext(os.path.basename(path))[0])
- date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
+ # Calculate the date to use for the post
+ schedule = options['schedule'] or self.site.config['SCHEDULE_ALL']
+ rule = self.site.config['SCHEDULE_RULE']
+ force_today = self.site.config['SCHEDULE_FORCE_TODAY']
+ self.site.scan_posts()
+ timeline = self.site.timeline
+ last_date = None if not timeline else timeline[0].date
+ date = get_date(schedule, rule, last_date, force_today)
data = [title, slug, date, tags]
output_path = os.path.dirname(entry[0])
meta_path = os.path.join(output_path, slug + ".meta")
@@ -206,20 +267,25 @@ class CommandNewPost(Command):
if (not onefile and os.path.isfile(meta_path)) or \
os.path.isfile(txt_path):
- print("The title already exists!")
+ LOGGER.error("The title already exists!")
exit()
d_name = os.path.dirname(txt_path)
- if not os.path.exists(d_name):
- os.makedirs(d_name)
+ utils.makedirs(d_name)
+ metadata = self.site.config['ADDITIONAL_METADATA']
compiler_plugin.create_post(
txt_path, onefile, title=title,
- slug=slug, date=date, tags=tags)
+ slug=slug, date=date, tags=tags, **metadata)
+
+ event = dict(path=txt_path)
if not onefile: # write metadata file
with codecs.open(meta_path, "wb+", "utf8") as fd:
fd.write('\n'.join(data))
with codecs.open(txt_path, "wb+", "utf8") as fd:
fd.write("Write your post here.")
- print("Your post's metadata is at: ", meta_path)
- print("Your post's text is at: ", txt_path)
+ LOGGER.notice("Your post's metadata is at: {0}".format(meta_path))
+ event['meta_path'] = meta_path
+ LOGGER.notice("Your post's text is at: {0}".format(txt_path))
+
+ signal('new_post').send(self, **event)
diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command/planetoid.plugin
index 8636d49..e767f31 100644
--- a/nikola/plugins/command_planetoid.plugin
+++ b/nikola/plugins/command/planetoid.plugin
@@ -1,9 +1,9 @@
[Core]
Name = planetoid
-Module = command_planetoid
+Module = planetoid
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Maintain a planet-like site
diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command/planetoid/__init__.py
index 183dd51..369862b 100644
--- a/nikola/plugins/command_planetoid/__init__.py
+++ b/nikola/plugins/command/planetoid/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -33,7 +34,9 @@ import sys
from doit.tools import timeout
from nikola.plugin_categories import Command, Task
-from nikola.utils import config_changed
+from nikola.utils import config_changed, req_missing, get_logger, STDERR_HANDLER
+
+LOGGER = get_logger('planetoid', STDERR_HANDLER)
try:
import feedparser
@@ -75,10 +78,10 @@ class Planetoid(Command, Task):
def gen_tasks(self):
if peewee is None or sys.version_info[0] == 3:
if sys.version_info[0] == 3:
- message = 'Peewee is currently incompatible with Python 3.'
+ message = 'Peewee, a requirement of the "planetoid" command, is currently incompatible with Python 3.'
else:
- message = 'You need to install the \"peewee\" module.'
-
+ req_missing('peewee', 'use the "planetoid" command')
+ message = ''
yield {
'basename': self.name,
'name': '',
@@ -159,12 +162,12 @@ class Planetoid(Command, Task):
# TODO: log failure
return
if parsed.feed.get('title'):
- print(parsed.feed.title)
+ LOGGER.notice(parsed.feed.title)
else:
- print(feed.url)
+ LOGGER.notice(feed.url)
feed.etag = parsed.get('etag', 'foo')
modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6]
- print("==========>", modified)
+ LOGGER.notice("==========>", modified)
modified = datetime.datetime(*modified)
feed.last_modified = modified
feed.save()
@@ -173,15 +176,14 @@ class Planetoid(Command, Task):
# TODO log failure
return
for entry_data in parsed.entries:
- print("=========================================")
+ LOGGER.notice("=========================================")
date = entry_data.get('published_parsed', None)
if date is None:
date = entry_data.get('updated_parsed', None)
if date is None:
- print("Can't parse date from:")
- print(entry_data)
+ LOGGER.error("Can't parse date from:\n", entry_data)
return False
- print("DATE:===>", date)
+ LOGGER.notice("DATE:===>", date)
date = datetime.datetime(*(date[:6]))
title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título'))
content = entry_data.get('content', None)
@@ -193,9 +195,9 @@ class Planetoid(Command, Task):
content = entry_data.get('summary', 'Sin contenido')
guid = str(entry_data.get('guid', entry_data.link))
link = entry_data.link
- print(repr([date, title]))
+ LOGGER.notice(repr([date, title]))
e = list(Entry.select().where(Entry.guid == guid))
- print(
+ LOGGER.notice(
repr(dict(
date=date,
title=title,
diff --git a/nikola/plugins/command_serve.plugin b/nikola/plugins/command/serve.plugin
index 684935d..e663cc6 100644
--- a/nikola/plugins/command_serve.plugin
+++ b/nikola/plugins/command/serve.plugin
@@ -1,10 +1,10 @@
[Core]
Name = serve
-Module = command_serve
+Module = serve
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Start test server.
diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py
new file mode 100644
index 0000000..07403d4
--- /dev/null
+++ b/nikola/plugins/command/serve.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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
+import os
+try:
+ from BaseHTTPServer import HTTPServer
+ from SimpleHTTPServer import SimpleHTTPRequestHandler
+except ImportError:
+ from http.server import HTTPServer # NOQA
+ from http.server import SimpleHTTPRequestHandler # NOQA
+
+from nikola.plugin_categories import Command
+from nikola.utils import get_logger
+
+
+class CommandBuild(Command):
+ """Start test server."""
+
+ name = "serve"
+ doc_usage = "[options]"
+ doc_purpose = "start the test webserver"
+ logger = None
+
+ 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."""
+ self.logger = get_logger('serve', self.site.loghandlers)
+ out_dir = self.site.config['OUTPUT_FOLDER']
+ if not os.path.isdir(out_dir):
+ self.logger.error("Missing '{0}' folder?".format(out_dir))
+ else:
+ os.chdir(out_dir)
+ httpd = HTTPServer((options['address'], options['port']),
+ OurHTTPRequestHandler)
+ sa = httpd.socket.getsockname()
+ self.logger.notice("Serving HTTP on {0} port {1} ...".format(*sa))
+ httpd.serve_forever()
+
+
+class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
+ extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
+ extensions_map[""] = "text/plain"
+
+ # NOTICE: this is a patched version of send_head() to disable all sorts of
+ # caching. `nikola serve` is a development server, hence caching should
+ # not happen to have access to the newest resources.
+ #
+ # The original code was copy-pasted from Python 2.7. Python 3.3 contains
+ # the same code, missing the binary mode comment.
+ #
+ # Note that it might break in future versions of Python, in which case we
+ # would need to do even more magic.
+ def send_head(self):
+ """Common code for GET and HEAD commands.
+
+ This sends the response code and MIME headers.
+
+ Return value is either a file object (which has to be copied
+ to the outputfile by the caller unless the command was HEAD,
+ and must be closed by the caller under all circumstances), or
+ None, in which case the caller has nothing further to do.
+
+ """
+ path = self.translate_path(self.path)
+ f = None
+ if os.path.isdir(path):
+ if not self.path.endswith('/'):
+ # redirect browser - doing basically what apache does
+ self.send_response(301)
+ self.send_header("Location", self.path + "/")
+ # begin no-cache patch
+ # For redirects. With redirects, caching is even worse and can
+ # break more. Especially with 301 Moved Permanently redirects,
+ # like this one.
+ self.send_header("Cache-Control", "no-cache, no-store, "
+ "must-revalidate")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ # end no-cache patch
+ self.end_headers()
+ return None
+ for index in "index.html", "index.htm":
+ index = os.path.join(path, index)
+ if os.path.exists(index):
+ path = index
+ break
+ else:
+ return self.list_directory(path)
+ ctype = self.guess_type(path)
+ try:
+ # Always read in binary mode. Opening files in text mode may cause
+ # newline translations, making the actual size of the content
+ # transmitted *less* than the content-length!
+ f = open(path, 'rb')
+ except IOError:
+ self.send_error(404, "File not found")
+ return None
+ self.send_response(200)
+ self.send_header("Content-type", ctype)
+ fs = os.fstat(f.fileno())
+ self.send_header("Content-Length", str(fs[6]))
+ self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
+ # begin no-cache patch
+ # For standard requests.
+ self.send_header("Cache-Control", "no-cache, no-store, "
+ "must-revalidate")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ # end no-cache patch
+ self.end_headers()
+ return f
diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin
new file mode 100644
index 0000000..3c1ae95
--- /dev/null
+++ b/nikola/plugins/command/version.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = version
+Module = version
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.2
+Website = http://getnikola.com
+Description = Show nikola version
diff --git a/nikola/plugins/compile_rest/dummy.py b/nikola/plugins/command/version.py
index 39543fd..65896e9 100644
--- a/nikola/plugins/compile_rest/dummy.py
+++ b/nikola/plugins/command/version.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,22 +24,21 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""A stupid codeblock replacement for neanderthals and users of Debian Sid."""
-
-from __future__ import unicode_literals
+from __future__ import print_function
-from docutils import nodes
-from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import Command
+from nikola import __version__
-CODE = '<pre>{0}</pre>'
+class CommandVersion(Command):
+ """Print the version."""
-class CodeBlock(Directive):
- required_arguments = 1
- has_content = True
+ name = "version"
- def run(self):
- """ Required by the Directive interface. Create docutils nodes """
- return [nodes.raw('', CODE.format('\n'.join(self.content)), format='html')]
+ doc_usage = ""
+ needs_config = False
+ doc_purpose = "print the Nikola version number"
-directives.register_directive('code', CodeBlock)
+ def _execute(self, options={}, args=None):
+ """Print the version number."""
+ print("Nikola version " + __version__)
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
deleted file mode 100644
index ea82703..0000000
--- a/nikola/plugins/command_check.py
+++ /dev/null
@@ -1,166 +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 print_function
-import os
-import sys
-try:
- from urllib import unquote
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import unquote, urlparse # NOQA
-
-import lxml.html
-
-from nikola.plugin_categories import Command
-
-
-class CommandCheck(Command):
- """Check the generated site."""
-
- name = "check"
-
- 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."""
- if not options['links'] and not options['files']:
- print(self.help())
- return False
- if options['links']:
- failure = scan_links(options['find_sources'])
- if options['files']:
- failure = scan_files()
- if failure:
- sys.exit(1)
-
-existing_targets = set([])
-
-
-def analize(task, find_sources=False):
- rv = False
- try:
- filename = task.split(":")[-1]
- d = lxml.html.fromstring(open(filename).read())
- for l in d.iterlinks():
- target = l[0].attrib[l[1]]
- if target == "#":
- continue
- parsed = urlparse(target)
- if parsed.scheme:
- continue
- if parsed.fragment:
- target = target.split('#')[0]
- target_filename = os.path.abspath(
- os.path.join(os.path.dirname(filename), unquote(target)))
- if target_filename not in existing_targets:
- if os.path.exists(target_filename):
- existing_targets.add(target_filename)
- else:
- rv = True
- print("Broken link in {0}: ".format(filename), target)
- if find_sources:
- print("Possible sources:")
- print(os.popen('nikola list --deps ' + task,
- 'r').read())
- print("===============================\n")
-
- except Exception as exc:
- print("Error with:", filename, exc)
- return rv
-
-
-def scan_links(find_sources=False):
- print("Checking Links:\n===============\n")
- failure = False
- 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:
- if analize(task, find_sources):
- failure = True
- return failure
-
-
-def scan_files():
- print("Checking Files:\n===============\n")
- task_fnames = set([])
- real_fnames = set([])
- # First check that all targets are generated in the right places
- failure = False
- for task in os.popen('nikola list --all', 'r').readlines():
- task = task.strip()
- if 'output' in task and ':' in task:
- fname = task.split(':')[-1]
- task_fnames.add(fname)
- # And now check that there are no non-target files
- for root, dirs, files in os.walk('output'):
- for src_name in files:
- fname = os.path.join(root, src_name)
- real_fnames.add(fname)
-
- only_on_output = list(real_fnames - task_fnames)
- if only_on_output:
- only_on_output.sort()
- print("\nFiles from unknown origins:\n")
- for f in only_on_output:
- print(f)
- failure = True
-
- only_on_input = list(task_fnames - real_fnames)
- if only_on_input:
- only_on_input.sort()
- print("\nFiles not generated:\n")
- for f in only_on_input:
- print(f)
-
- return failure
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
deleted file mode 100644
index 3277567..0000000
--- a/nikola/plugins/command_deploy.py
+++ /dev/null
@@ -1,65 +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 print_function
-from ast import literal_eval
-import codecs
-from datetime import datetime
-import os
-import subprocess
-
-
-from nikola.plugin_categories import Command
-
-
-class Deploy(Command):
- """Deploy site. """
- name = "deploy"
-
- doc_usage = ""
- doc_purpose = "Deploy the site."
-
- def _execute(self, command, args):
- for command in self.site.config['DEPLOY_COMMANDS']:
-
- # Get last succesful deploy date
- timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
- try:
- with open(timestamp_path, 'rb') as inf:
- last_deploy = literal_eval(inf.read().strip())
- except Exception:
- last_deploy = datetime(1970, 1, 1) # NOQA
-
- print("==>", command)
- ret = subprocess.check_call(command, shell=True)
- if ret != 0: # failed deployment
- raise Exception("Failed deployment")
- print("Successful deployment")
- new_deploy = datetime.now()
- # Store timestamp of successful deployment
- with codecs.open(timestamp_path, 'wb+', 'utf8') as outf:
- outf.write(repr(new_deploy))
- # Here is where we would do things with whatever is
- # on self.site.timeline and is newer than
- # last_deploy
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
deleted file mode 100644
index 2a0a0cc..0000000
--- a/nikola/plugins/command_install_theme.py
+++ /dev/null
@@ -1,105 +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 print_function
-import os
-import json
-from io import BytesIO
-
-try:
- import requests
-except ImportError:
- requests = None # NOQA
-
-from nikola.plugin_categories import Command
-from nikola import utils
-
-
-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 _execute(self, options, args):
- """Install theme into current site."""
- if requests is None:
- print('This command requires the requests package be installed.')
- return False
-
- 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 a theme name or the -l option.")
- return False
- data = requests.get(url).text
- data = json.loads(data)
- if listing:
- print("Themes:")
- print("-------")
- for theme in sorted(data.keys()):
- print(theme)
- return True
- else:
- if name in data:
- if os.path.isfile("themes"):
- raise IOError("'themes' isn't a directory!")
- elif not os.path.isdir("themes"):
- try:
- os.makedirs("themes")
- except:
- raise OSError("mkdir 'theme' error!")
- print('Downloading: ' + data[name])
- zip_file = BytesIO()
- zip_file.write(requests.get(data[name]).content)
- print('Extracting: {0} into themes'.format(name))
- utils.extract_all(zip_file)
- else:
- print("Can't find theme " + name)
- return False
diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py
deleted file mode 100644
index 64efe7d..0000000
--- a/nikola/plugins/command_serve.py
+++ /dev/null
@@ -1,79 +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 print_function
-import os
-try:
- from BaseHTTPServer import HTTPServer
- from SimpleHTTPServer import SimpleHTTPRequestHandler
-except ImportError:
- from http.server import HTTPServer # NOQA
- from http.server import SimpleHTTPRequestHandler # NOQA
-
-from nikola.plugin_categories import Command
-
-
-class CommandBuild(Command):
- """Start test server."""
-
- name = "serve"
- doc_usage = "[options]"
- doc_purpose = "Start the test webserver."
-
- 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 '{0}' folder?".format(out_dir))
- else:
- os.chdir(out_dir)
- httpd = HTTPServer((options['address'], options['port']),
- OurHTTPRequestHandler)
- sa = httpd.socket.getsockname()
- print("Serving HTTP on", sa[0], "port", sa[1], "...")
- httpd.serve_forever()
-
-
-class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
- extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
- extensions_map[""] = "text/plain"
diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/compile/__init__.py
diff --git a/nikola/plugins/compile/asciidoc.plugin b/nikola/plugins/compile/asciidoc.plugin
new file mode 100644
index 0000000..47c5608
--- /dev/null
+++ b/nikola/plugins/compile/asciidoc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = asciidoc
+Module = asciidoc
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile ASCIIDoc into HTML
+
diff --git a/nikola/plugins/compile/asciidoc.py b/nikola/plugins/compile/asciidoc.py
new file mode 100644
index 0000000..67dfe1a
--- /dev/null
+++ b/nikola/plugins/compile/asciidoc.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
+
+"""Implementation of compile_html based on asciidoc.
+
+You will need, of course, to install asciidoc
+
+"""
+
+import codecs
+import os
+import subprocess
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
+
+
+class CompileAsciiDoc(PageCompiler):
+ """Compile asciidoc into HTML."""
+
+ name = "asciidoc"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ try:
+ subprocess.check_call(('asciidoc', '-f', 'html', '-s', '-o', dest, source))
+ except OSError as e:
+ if e.strreror == 'No such file or directory':
+ req_missing(['asciidoc'], 'build this site (compile with asciidoc)', python=False)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write("/////////////////////////////////////////////\n")
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write("/////////////////////////////////////////////\n")
+ fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_bbcode.plugin b/nikola/plugins/compile/bbcode.plugin
index ec3ce2b..b3d9357 100644
--- a/nikola/plugins/compile_bbcode.plugin
+++ b/nikola/plugins/compile/bbcode.plugin
@@ -1,10 +1,10 @@
[Core]
Name = bbcode
-Module = compile_bbcode
+Module = bbcode
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile BBCode into HTML
diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile/bbcode.py
index f8022f3..e998417 100644
--- a/nikola/plugins/compile_bbcode.py
+++ b/nikola/plugins/compile/bbcode.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,7 @@
import codecs
import os
+import re
try:
import bbcode
@@ -33,9 +36,10 @@ except ImportError:
bbcode = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileTextile(PageCompiler):
+class CompileBbcode(PageCompiler):
"""Compile bbcode into HTML."""
name = "bbcode"
@@ -46,17 +50,15 @@ class CompileTextile(PageCompiler):
self.parser = bbcode.Parser()
self.parser.add_simple_formatter("note", "")
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if bbcode is None:
- raise Exception('To build this site, you need to install the '
- '"bbcode" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['bbcode'], 'build this site (compile BBCode)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = self.parser.format(data)
out_file.write(output)
@@ -64,9 +66,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('[note]<!--\n')
diff --git a/nikola/plugins/compile_html.plugin b/nikola/plugins/compile/html.plugin
index f6cdfbc..21dd338 100644
--- a/nikola/plugins/compile_html.plugin
+++ b/nikola/plugins/compile/html.plugin
@@ -1,10 +1,10 @@
[Core]
Name = html
-Module = compile_html
+Module = html
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile HTML into HTML (just copy)
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile/html.py
index 7551b33..a309960 100644
--- a/nikola/plugins/compile_html.py
+++ b/nikola/plugins/compile/html.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,13 +24,14 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on markdown."""
+"""Implementation of compile_html for HTML source files."""
import os
import shutil
import codecs
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs
class CompileHtml(PageCompiler):
@@ -36,24 +39,20 @@ class CompileHtml(PageCompiler):
name = "html"
- def compile_html(self, source, dest):
- try:
- os.makedirs(os.path.dirname(dest))
- except Exception:
- pass
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
shutil.copyfile(source, dest)
+ return True
def create_post(self, path, onefile=False, **kw):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
- for k, v in metadata.keys():
+ for k, v in metadata.items():
fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->\n\n')
fd.write("\n<p>Write your post here.</p>")
diff --git a/nikola/plugins/compile_ipynb.plugin b/nikola/plugins/compile/ipynb.plugin
index 51051e0..3d15bb0 100644
--- a/nikola/plugins/compile_ipynb.plugin
+++ b/nikola/plugins/compile/ipynb.plugin
@@ -1,10 +1,10 @@
[Core]
Name = ipynb
-Module = compile_ipynb
+Module = ipynb
[Documentation]
Author = Damián Avila
-Version = 0.1
+Version = 1.0
Website = http://www.oquanta.info
Description = Compile IPython notebooks into HTML
diff --git a/nikola/plugins/compile/ipynb/README.txt b/nikola/plugins/compile/ipynb/README.txt
new file mode 100644
index 0000000..0a7d6db
--- /dev/null
+++ b/nikola/plugins/compile/ipynb/README.txt
@@ -0,0 +1,44 @@
+To make this work...
+
+1- You can install the "jinja-site-ipython" theme using this command:
+
+$ nikola install_theme -n jinja-site-ipython
+
+(or xkcd-site-ipython, if you want xkcd styling)
+
+More info here about themes:
+http://getnikola.com/handbook.html#getting-more-themes
+
+OR
+
+You can to download the "jinja-site-ipython" theme from here:
+https://github.com/damianavila/jinja-site-ipython-theme-for-Nikola
+and copy the "site-ipython" folder inside the "themes" folder of your site.
+
+
+2- Then, just add:
+
+post_pages = (
+ ("posts/*.ipynb", "posts", "post.tmpl", True),
+ ("stories/*.ipynb", "stories", "story.tmpl", False),
+)
+
+and
+
+THEME = 'jinja-site-ipython' (or 'xkcd-site-ipython', if you want xkcd styling)
+
+to your conf.py.
+Finally... to use it:
+
+$nikola new_page -f ipynb
+
+**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
+create one metadata file and the corresponding naive IPython notebook.
+
+$nikola build
+
+And deploy the output folder... to see it locally: $nikola serve
+If you have any doubts, just ask: @damianavila
+
+Cheers.
+Damián
diff --git a/nikola/plugins/compile_ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py
index d38f6f2..7c318ca 100644
--- a/nikola/plugins/compile_ipynb/__init__.py
+++ b/nikola/plugins/compile/ipynb/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2013 Damian Avila.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013 Damian Avila.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,13 +31,15 @@ import codecs
import os
try:
- from .nbformat import current as nbformat
- from .nbconvert.converters import bloggerhtml as nbconverter
- bloggerhtml = True
+ from IPython.nbconvert.exporters import HTMLExporter
+ from IPython.nbformat import current as nbformat
+ from IPython.config import Config
+ flag = True
except ImportError:
- bloggerhtml = None
+ flag = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileIPynb(PageCompiler):
@@ -43,30 +47,26 @@ class CompileIPynb(PageCompiler):
name = "ipynb"
- def compile_html(self, source, dest):
- if bloggerhtml is None:
- raise Exception('To build this site, you also need '
- 'https://github.com/damianavila/com'
- 'pile_ipynb-for-Nikola.git.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- converter = nbconverter.ConverterBloggerHTML()
+ def compile_html(self, source, dest, is_two_file=True):
+ if flag is None:
+ req_missing(['ipython>=1.0.0'], 'build this site (compile ipynb)')
+ makedirs(os.path.dirname(dest))
+ HTMLExporter.default_template = 'basic'
+ c = Config(self.site.config['IPYNB_CONFIG'])
+ exportHtml = HTMLExporter(config=c)
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
- converter.nb = nbformat.reads_json(data)
- output = converter.convert()
- out_file.write(output)
+ nb = in_file.read()
+ nb_json = nbformat.reads_json(nb)
+ (body, resources) = exportHtml.from_notebook_node(nb_json)
+ out_file.write(body)
def create_post(self, path, onefile=False, **kw):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
meta_path = os.path.join(d_name, kw['slug'] + ".meta")
with codecs.open(meta_path, "wb+", "utf8") as fd:
if onefile:
@@ -78,7 +78,7 @@ class CompileIPynb(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
fd.write("""{
"metadata": {
- "name": "%s"
+ "name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
@@ -97,4 +97,4 @@ class CompileIPynb(PageCompiler):
"metadata": {}
}
]
-}""" % kw['slug'])
+}""")
diff --git a/nikola/plugins/compile_markdown.plugin b/nikola/plugins/compile/markdown.plugin
index f3e119b..157579a 100644
--- a/nikola/plugins/compile_markdown.plugin
+++ b/nikola/plugins/compile/markdown.plugin
@@ -1,10 +1,10 @@
[Core]
Name = markdown
-Module = compile_markdown
+Module = markdown
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Markdown into HTML
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py
index ae700e6..b41c6b5 100644
--- a/nikola/plugins/compile_markdown/__init__.py
+++ b/nikola/plugins/compile/markdown/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,17 +30,18 @@ from __future__ import unicode_literals
import codecs
import os
+import re
try:
from markdown import markdown
- from nikola.plugins.compile_markdown.mdx_nikola import NikolaExtension
+ from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension
nikola_extension = NikolaExtension()
- from nikola.plugins.compile_markdown.mdx_gist import GistExtension
+ from nikola.plugins.compile.markdown.mdx_gist import GistExtension
gist_extension = GistExtension()
- from nikola.plugins.compile_markdown.mdx_podcast import PodcastExtension
+ from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension
podcast_extension = PodcastExtension()
except ImportError:
@@ -48,27 +51,26 @@ except ImportError:
podcast_extension = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileMarkdown(PageCompiler):
"""Compile markdown into HTML."""
name = "markdown"
+ extensions = [gist_extension, nikola_extension, podcast_extension]
+ site = None
- extensions = ['fenced_code', 'codehilite', gist_extension,
- nikola_extension, podcast_extension]
-
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if markdown is None:
- raise Exception('To build this site, you need to install the '
- '"markdown" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['markdown'], 'build this site (compile Markdown)')
+ makedirs(os.path.dirname(dest))
+ self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS")
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = markdown(data, self.extensions)
out_file.write(output)
@@ -76,9 +78,7 @@ class CompileMarkdown(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
diff --git a/nikola/plugins/compile_markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py
index 808e383..3c3bef9 100644
--- a/nikola/plugins/compile_markdown/mdx_gist.py
+++ b/nikola/plugins/compile/markdown/mdx_gist.py
@@ -21,12 +21,11 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
+# Warning: URL formats of "raw" gists are undocummented and subject to change.
+# See also: http://developer.github.com/v3/gists/
+#
# Inspired by "[Python] reStructuredText GitHub Gist directive"
# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
-
-from __future__ import print_function
-
-
'''
Extension to Python Markdown for Embedded Gists (gist.github.com)
@@ -83,13 +82,48 @@ Example using reStructuredText syntax:
</noscript>
</div>
</p>
+
+Error Case: non-existent Gist ID:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 0]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/0.js"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/0 --></noscript>
+ </div>
+ </p>
+
+Error Case: non-existent file:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 doesntexist.py]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/4747847/doesntexist.py --></noscript>
+ </div>
+ </p>
+
'''
-from __future__ import unicode_literals
-import warnings
+from __future__ import unicode_literals, print_function
from markdown.extensions import Extension
from markdown.inlinepatterns import Pattern
from markdown.util import AtomicString
from markdown.util import etree
+from nikola.utils import get_logger, req_missing, STDERR_HANDLER
+
+LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
try:
import requests
@@ -98,13 +132,21 @@ except ImportError:
GIST_JS_URL = "https://gist.github.com/{0}.js"
GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
-GIST_RAW_URL = "https://raw.github.com/gist/{0}"
-GIST_FILE_RAW_URL = "https://raw.github.com/gist/{0}/{1}"
+GIST_RAW_URL = "https://gist.github.com/raw/{0}"
+GIST_FILE_RAW_URL = "https://gist.github.com/raw/{0}/{1}"
-GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\]'
+GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\s*\]'
GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$'
+class GistFetchException(Exception):
+ '''Raised when attempt to fetch content of a Gist from github.com fails.'''
+ def __init__(self, url, status_code):
+ Exception.__init__(self)
+ self.message = 'Received a {0} response from Gist URL: {1}'.format(
+ status_code, url)
+
+
class GistPattern(Pattern):
""" InlinePattern for footnote markers in a document's body text. """
@@ -113,11 +155,21 @@ class GistPattern(Pattern):
def get_raw_gist_with_filename(self, gist_id, filename):
url = GIST_FILE_RAW_URL.format(gist_id, filename)
- return requests.get(url).text
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
def get_raw_gist(self, gist_id):
url = GIST_RAW_URL.format(gist_id)
- return requests.get(url).text
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
def handleMatch(self, m):
gist_id = m.group('gist_id')
@@ -127,34 +179,32 @@ class GistPattern(Pattern):
gist_elem.set('class', 'gist')
script_elem = etree.SubElement(gist_elem, 'script')
- if gist_file:
- script_elem.set('src', GIST_FILE_JS_URL.format(
- gist_id, gist_file))
-
- else:
- script_elem.set('src', GIST_JS_URL.format(
- gist_id))
-
if requests:
- if gist_file:
- raw_gist = (self.get_raw_gist_with_filename(
- gist_id, gist_file))
- script_elem.set('src', GIST_FILE_JS_URL.format(
- gist_id, gist_file))
-
- else:
- raw_gist = (self.get_raw_gist(gist_id))
- script_elem.set('src', GIST_JS_URL.format(
- gist_id))
-
- # Insert source as <pre/> within <noscript>
noscript_elem = etree.SubElement(gist_elem, 'noscript')
- pre_elem = etree.SubElement(noscript_elem, 'pre')
- pre_elem.text = AtomicString(raw_gist)
+
+ try:
+ if gist_file:
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+ raw_gist = (self.get_raw_gist_with_filename(
+ gist_id, gist_file))
+
+ else:
+ script_elem.set('src', GIST_JS_URL.format(
+ gist_id))
+ raw_gist = (self.get_raw_gist(gist_id))
+
+ # Insert source as <pre/> within <noscript>
+ pre_elem = etree.SubElement(noscript_elem, 'pre')
+ pre_elem.text = AtomicString(raw_gist)
+
+ except GistFetchException as e:
+ LOGGER.warn(e.message)
+ warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
+ noscript_elem.append(warning_comment)
else:
- warnings.warn('"requests" package not installed. '
- 'Please install to add inline gist source.')
+ req_missing('requests', 'have inline gist source', optional=True)
return gist_elem
@@ -185,5 +235,7 @@ def makeExtension(configs=None):
if __name__ == '__main__':
import doctest
+
+ # Silence user warnings thrown by tests:
doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile_markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py
index f7a1959..b0ad2f7 100644
--- a/nikola/plugins/compile_markdown/mdx_nikola.py
+++ b/nikola/plugins/compile/markdown/mdx_nikola.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/compile_markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py
index be8bb6b..be8bb6b 100644
--- a/nikola/plugins/compile_markdown/mdx_podcast.py
+++ b/nikola/plugins/compile/markdown/mdx_podcast.py
diff --git a/nikola/plugins/compile_misaka.plugin b/nikola/plugins/compile/misaka.plugin
index 1b9c8a8..fef6d71 100644
--- a/nikola/plugins/compile_misaka.plugin
+++ b/nikola/plugins/compile/misaka.plugin
@@ -1,6 +1,6 @@
[Core]
Name = misaka
-Module = compile_misaka
+Module = misaka
[Documentation]
Author = Chris Lee
diff --git a/nikola/plugins/compile_misaka/__init__.py b/nikola/plugins/compile/misaka.py
index a3f687e..3733a85 100644
--- a/nikola/plugins/compile_misaka/__init__.py
+++ b/nikola/plugins/compile/misaka.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
# Copyright (c) 2013 Chris Lee
# Permission is hereby granted, free of charge, to any
@@ -28,10 +30,10 @@ from __future__ import unicode_literals
import codecs
import os
+import re
try:
import misaka
-
except ImportError:
misaka = None # NOQA
nikola_extension = None
@@ -39,30 +41,29 @@ except ImportError:
podcast_extension = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileMarkdown(PageCompiler):
- """Compile markdown into HTML."""
+class CompileMisaka(PageCompiler):
+ """Compile Misaka into HTML."""
- name = "markdown"
+ name = "misaka"
def __init__(self, *args, **kwargs):
- super(CompileMarkdown, self).__init__(*args, **kwargs)
+ super(CompileMisaka, self).__init__(*args, **kwargs)
if misaka is not None:
self.ext = misaka.EXT_FENCED_CODE | misaka.EXT_STRIKETHROUGH | \
misaka.EXT_AUTOLINK | misaka.EXT_NO_INTRA_EMPHASIS
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if misaka is None:
- raise Exception('To build this site, you need to install the '
- '"misaka" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['misaka'], 'build this site (compile with misaka)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = misaka.html(data, extensions=self.ext)
out_file.write(output)
@@ -70,9 +71,7 @@ class CompileMarkdown(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin
new file mode 100644
index 0000000..157b694
--- /dev/null
+++ b/nikola/plugins/compile/pandoc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = pandoc
+Module = pandoc
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile markups into HTML using pandoc
+
diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py
new file mode 100644
index 0000000..3a2911f
--- /dev/null
+++ b/nikola/plugins/compile/pandoc.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
+
+"""Implementation of compile_html based on pandoc.
+
+You will need, of course, to install pandoc
+
+"""
+
+import codecs
+import os
+import subprocess
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import req_missing, makedirs
+
+
+class CompilePandoc(PageCompiler):
+ """Compile markups into HTML using pandoc."""
+
+ name = "pandoc"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ try:
+ subprocess.check_call(('pandoc', '-o', dest, source))
+ except OSError as e:
+ if e.strreror == 'No such file or directory':
+ req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin
new file mode 100644
index 0000000..ac25259
--- /dev/null
+++ b/nikola/plugins/compile/php.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = php
+Module = php
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile PHP into HTML (just copy and name the file .php)
+
diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py
new file mode 100644
index 0000000..44701c8
--- /dev/null
+++ b/nikola/plugins/compile/php.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
+
+"""Implementation of compile_html for HTML+php."""
+
+from __future__ import unicode_literals
+
+import os
+import shutil
+import codecs
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs
+
+
+class CompilePhp(PageCompiler):
+ """Compile PHP into PHP."""
+
+ name = "php"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ shutil.copyfile(source, dest)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ os.makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("\n<p>Write your post here.</p>")
+
+ def extension(self):
+ return ".php"
diff --git a/nikola/plugins/compile_rest.plugin b/nikola/plugins/compile/rest.plugin
index 67eb562..55e9c59 100644
--- a/nikola/plugins/compile_rest.plugin
+++ b/nikola/plugins/compile/rest.plugin
@@ -1,10 +1,10 @@
[Core]
Name = rest
-Module = compile_rest
+Module = rest
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile reSt into HTML
diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py
new file mode 100644
index 0000000..c71a5f8
--- /dev/null
+++ b/nikola/plugins/compile/rest/__init__.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 codecs
+import os
+import re
+
+try:
+ import docutils.core
+ import docutils.nodes
+ import docutils.utils
+ import docutils.io
+ import docutils.readers.standalone
+ has_docutils = True
+except ImportError:
+ has_docutils = False
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import get_logger, makedirs, req_missing
+
+
+class CompileRest(PageCompiler):
+ """Compile reSt into HTML."""
+
+ name = "rest"
+ logger = None
+
+ def compile_html(self, source, dest, is_two_file=True):
+ """Compile reSt into HTML."""
+
+ if not has_docutils:
+ req_missing(['docutils'], 'build this site (compile reStructuredText)')
+ makedirs(os.path.dirname(dest))
+ error_level = 100
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ add_ln = 0
+ if not is_two_file:
+ spl = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)
+ data = spl[-1]
+ if len(spl) != 1:
+ # If errors occur, this will be added to the line
+ # number reported by docutils so the line number
+ # matches the actual line number (off by 7 with default
+ # metadata, could be more or less depending on the post
+ # author).
+ add_ln = len(spl[0].splitlines()) + 1
+
+ output, error_level, deps = rst2html(
+ data, settings_overrides={
+ 'initial_header_level': 2,
+ 'record_dependencies': True,
+ 'stylesheet_path': None,
+ 'link_stylesheet': True,
+ 'syntax_highlight': 'short',
+ 'math_output': 'mathjax',
+ }, logger=self.logger, l_source=source, l_add_ln=add_ln)
+ out_file.write(output)
+ deps_path = dest + '.dep'
+ if deps.list:
+ with codecs.open(deps_path, "wb+", "utf8") as deps_file:
+ deps_file.write('\n'.join(deps.list))
+ else:
+ if os.path.isfile(deps_path):
+ os.unlink(deps_path)
+ if error_level < 3:
+ return True
+ else:
+ return False
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write("\nWrite your post here.")
+
+ def set_site(self, site):
+ for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"):
+ if (plugin_info.name in site.config['DISABLED_PLUGINS']
+ or (plugin_info.name in site.EXTRA_PLUGINS and
+ plugin_info.name not in site.config['ENABLED_EXTRAS'])):
+ site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension")
+ continue
+
+ site.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(site)
+ plugin_info.plugin_object.short_help = plugin_info.description
+
+ self.logger = get_logger('compile_rest', site.loghandlers)
+ return super(CompileRest, self).set_site(site)
+
+
+def get_observer(settings):
+ """Return an observer for the docutils Reporter."""
+ def observer(msg):
+ """Report docutils/rest messages to a Nikola user.
+
+ Error code mapping:
+
+ +------+---------+------+----------+
+ | dNUM | dNAME | lNUM | lNAME | d = docutils, l = logbook
+ +------+---------+------+----------+
+ | 0 | DEBUG | 1 | DEBUG |
+ | 1 | INFO | 2 | INFO |
+ | 2 | WARNING | 4 | WARNING |
+ | 3 | ERROR | 5 | ERROR |
+ | 4 | SEVERE | 6 | CRITICAL |
+ +------+---------+------+----------+
+ """
+ errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6}
+ text = docutils.nodes.Element.astext(msg)
+ out = '[{source}:{line}] {text}'.format(source=settings['source'], line=msg['line'] + settings['add_ln'], text=text)
+ settings['logger'].log(errormap[msg['level']], out)
+
+ return observer
+
+
+class NikolaReader(docutils.readers.standalone.Reader):
+
+ def new_document(self):
+ """Create and return a new empty document tree (root node)."""
+ document = docutils.utils.new_document(self.source.source_path, self.settings)
+ document.reporter.stream = False
+ document.reporter.attach_observer(get_observer(self.l_settings))
+ return document
+
+
+def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
+ destination_path=None, reader=None,
+ parser=None, parser_name='restructuredtext', writer=None,
+ writer_name='html', settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=None, logger=None, l_source='', l_add_ln=0):
+ """
+ Set up & run a `Publisher`, and return a dictionary of document parts.
+ Dictionary keys are the names of parts, and values are Unicode strings;
+ encoding is up to the client. For programmatic use with string I/O.
+
+ For encoded string input, be sure to set the 'input_encoding' setting to
+ the desired encoding. Set it to 'unicode' for unencoded Unicode string
+ input. Here's how::
+
+ publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
+
+ Parameters: see `publish_programmatically`.
+
+ WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report
+ reStructuredText syntax errors.
+ """
+ if reader is None:
+ reader = NikolaReader()
+ # For our custom logging, we have special needs and special settings we
+ # specify here.
+ # logger a logger from Nikola
+ # source source filename (docutils gets a string)
+ # add_ln amount of metadata lines (see comment in compile_html above)
+ reader.l_settings = {'logger': logger, 'source': l_source,
+ 'add_ln': l_add_ln}
+
+ pub = docutils.core.Publisher(reader, parser, writer, settings=settings,
+ source_class=source_class,
+ destination_class=docutils.io.StringOutput)
+ pub.set_components(None, parser_name, writer_name)
+ pub.process_programmatic_settings(
+ settings_spec, settings_overrides, config_section)
+ pub.set_source(source, source_path)
+ pub.set_destination(None, destination_path)
+ pub.publish(enable_exit_status=enable_exit_status)
+
+ return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin
new file mode 100644
index 0000000..3e27a25
--- /dev/null
+++ b/nikola/plugins/compile/rest/chart.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_chart
+Module = chart
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Chart directive based in PyGal
+
diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py
new file mode 100644
index 0000000..ee917b9
--- /dev/null
+++ b/nikola/plugins/compile/rest/chart.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 ast import literal_eval
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+try:
+ import pygal
+except ImportError:
+ pygal = None # NOQA
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_chart"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('chart', Chart)
+ return super(Plugin, self).set_site(site)
+
+
+class Chart(Directive):
+ """ Restructured text extension for inserting charts as SVG
+
+ Usage:
+ .. chart:: Bar
+ :title: 'Browser usage evolution (in %)'
+ :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"]
+
+ 'Firefox', [None, None, 0, 16.6, 25, 31]
+ 'Chrome', [None, None, None, None, None, None]
+ 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6]
+ 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4]
+ """
+
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "copy": directives.unchanged,
+ "css": directives.unchanged,
+ "disable_xml_declaration": directives.unchanged,
+ "dots_size": directives.unchanged,
+ "explicit_size": directives.unchanged,
+ "fill": directives.unchanged,
+ "font_sizes": directives.unchanged,
+ "height": directives.unchanged,
+ "human_readable": directives.unchanged,
+ "include_x_axis": directives.unchanged,
+ "interpolate": directives.unchanged,
+ "interpolation_parameters": directives.unchanged,
+ "interpolation_precision": directives.unchanged,
+ "js": directives.unchanged,
+ "label_font_size": directives.unchanged,
+ "legend_at_bottom": directives.unchanged,
+ "legend_box_size": directives.unchanged,
+ "legend_font_size": directives.unchanged,
+ "logarithmic": directives.unchanged,
+ "major_label_font_size": directives.unchanged,
+ "margin": directives.unchanged,
+ "no_data_font_size": directives.unchanged,
+ "no_data_text": directives.unchanged,
+ "no_prefix": directives.unchanged,
+ "order_min": directives.unchanged,
+ "pretty_print": directives.unchanged,
+ "print_values": directives.unchanged,
+ "print_zeroes": directives.unchanged,
+ "range": directives.unchanged,
+ "rounded_bars": directives.unchanged,
+ "show_dots": directives.unchanged,
+ "show_legend": directives.unchanged,
+ "show_minor_x_labels": directives.unchanged,
+ "show_y_labels": directives.unchanged,
+ "spacing": directives.unchanged,
+ "strict": directives.unchanged,
+ "stroke": directives.unchanged,
+ "style": directives.unchanged,
+ "title": directives.unchanged,
+ "title_font_size": directives.unchanged,
+ "to_dict": directives.unchanged,
+ "tooltip_border_radius": directives.unchanged,
+ "tooltip_font_size": directives.unchanged,
+ "truncate_label": directives.unchanged,
+ "truncate_legend": directives.unchanged,
+ "value_font_size": directives.unchanged,
+ "value_formatter": directives.unchanged,
+ "width": directives.unchanged,
+ "x_label_rotation": directives.unchanged,
+ "x_labels": directives.unchanged,
+ "x_labels_major": directives.unchanged,
+ "x_labels_major_count": directives.unchanged,
+ "x_labels_major_every": directives.unchanged,
+ "x_title": directives.unchanged,
+ "y_label_rotation": directives.unchanged,
+ "y_labels": directives.unchanged,
+ "y_title": directives.unchanged,
+ "zero": directives.unchanged,
+ }
+
+ def run(self):
+ if pygal is None:
+ msg = req_missing(['pygal'], 'use the Chart directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+ options = {}
+ if 'style' in self.options:
+ style_name = self.options.pop('style')
+ else:
+ style_name = 'BlueStyle'
+ if '(' in style_name: # Parametric style
+ style = eval('pygal.style.' + style_name)
+ else:
+ style = getattr(pygal.style, style_name)
+ for k, v in self.options.items():
+ options[k] = literal_eval(v)
+
+ chart = getattr(pygal, self.arguments[0])(style=style)
+ chart.config(**options)
+ for line in self.content:
+ label, series = literal_eval('({0})'.format(line))
+ chart.add(label, series)
+
+ return [nodes.raw('', chart.render().decode('utf8'), format='html')]
diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin
new file mode 100644
index 0000000..1984f52
--- /dev/null
+++ b/nikola/plugins/compile/rest/doc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_doc
+Module = doc
+
+[Documentation]
+Author = Manuel Kaufmann
+Version = 0.1
+Website = http://getnikola.com
+Description = Role to link another page / post from the blog
+
diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py
new file mode 100644
index 0000000..915a7e1
--- /dev/null
+++ b/nikola/plugins/compile/rest/doc.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 docutils import nodes
+from docutils.parsers.rst import roles
+
+from nikola.utils import split_explicit_title
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = 'rest_doc'
+
+ def set_site(self, site):
+ self.site = site
+ roles.register_canonical_role('doc', doc_role)
+ doc_role.site = site
+ return super(Plugin, self).set_site(site)
+
+
+def doc_role(name, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+
+ # split link's text and post's slug in role content
+ has_explicit_title, title, slug = split_explicit_title(text)
+
+ # check if the slug given is part of our blog posts/pages
+ twin_slugs = False
+ post = None
+ for p in doc_role.site.timeline:
+ if p.meta('slug') == slug:
+ if post is None:
+ post = p
+ else:
+ twin_slugs = True
+ break
+
+ try:
+ if post is None:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ '"{0}" slug doesn\'t exist.'.format(slug),
+ line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ if not has_explicit_title:
+ # use post's title as link's text
+ title = post.title()
+
+ permalink = post.permalink()
+ if twin_slugs:
+ msg = inliner.reporter.warning(
+ 'More than one post with the same slug. Using "{0}"'.format(permalink))
+
+ node = make_link_node(rawtext, title, permalink, options)
+ return [node], []
+
+
+def make_link_node(rawtext, text, url, options):
+ node = nodes.reference(rawtext, text, refuri=url, *options)
+ return node
diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin
new file mode 100644
index 0000000..8f498ec
--- /dev/null
+++ b/nikola/plugins/compile/rest/gist.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_gist
+Module = gist
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Gist directive
+
diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile/rest/gist.py
index 1506519..e09ed76 100644
--- a/nikola/plugins/compile_rest/gist_directive.py
+++ b/nikola/plugins/compile/rest/gist.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# This file is public domain according to its author, Brian Hsu
from docutils.parsers.rst import Directive, directives
@@ -8,13 +9,32 @@ try:
except ImportError:
requests = None # NOQA
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_gist"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('gist', GitHubGist)
+ return super(Plugin, self).set_site(site)
+
class GitHubGist(Directive):
""" Embed GitHub Gist.
Usage:
+
.. gist:: GIST_ID
+ or
+
+ .. gist:: GIST_URL
+
+
"""
required_arguments = 1
@@ -24,33 +44,41 @@ class GitHubGist(Directive):
has_content = False
def get_raw_gist_with_filename(self, gistID, filename):
- url = '/'.join(("https://raw.github.com/gist", gistID, filename))
+ url = '/'.join(("https://gist.github.com/raw", gistID, filename))
return requests.get(url).text
def get_raw_gist(self, gistID):
- url = "https://raw.github.com/gist/{0}".format(gistID)
+ url = "https://gist.github.com/raw/{0}".format(gistID)
return requests.get(url).text
def run(self):
- if requests is None:
- print('To use the gist directive, you need to install the '
- '"requests" package.')
- return []
- gistID = self.arguments[0].strip()
+ if 'https://' in self.arguments[0]:
+ gistID = self.arguments[0].split('/')[-1].strip()
+ else:
+ gistID = self.arguments[0].strip()
embedHTML = ""
rawGist = ""
if 'file' in self.options:
filename = self.options['file']
- rawGist = (self.get_raw_gist_with_filename(gistID, filename))
+ if requests is not None:
+ rawGist = (self.get_raw_gist_with_filename(gistID, filename))
embedHTML = ('<script src="https://gist.github.com/{0}.js'
'?file={1}"></script>').format(gistID, filename)
else:
- rawGist = (self.get_raw_gist(gistID))
+ if requests is not None:
+ rawGist = (self.get_raw_gist(gistID))
embedHTML = ('<script src="https://gist.github.com/{0}.js">'
'</script>').format(gistID)
+ if requests is None:
+ reqnode = nodes.raw(
+ '', req_missing('requests', 'have inline gist source',
+ optional=True), format='html')
+ else:
+ reqnode = nodes.literal_block('', rawGist)
+
return [nodes.raw('', embedHTML, format='html'),
nodes.raw('', '<noscript>', format='html'),
- nodes.literal_block('', rawGist),
+ reqnode,
nodes.raw('', '</noscript>', format='html')]
diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin
new file mode 100644
index 0000000..4c9883e
--- /dev/null
+++ b/nikola/plugins/compile/rest/listing.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_listing
+Module = listing
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Extension for source listings
+
diff --git a/nikola/plugins/compile_rest/listing.py b/nikola/plugins/compile/rest/listing.py
index 1b816f5..31975bb 100644
--- a/nikola/plugins/compile_rest/listing.py
+++ b/nikola/plugins/compile/rest/listing.py
@@ -1,121 +1,109 @@
-# -*- 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.
-
-
-""" Define and register a listing directive using the existing CodeBlock """
-
-
-from __future__ import unicode_literals
-from codecs import open as codecs_open # for patching purposes
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
-
-from docutils import core
-from docutils.parsers.rst import directives
-try:
- from docutils.parsers.rst.directives.body import CodeBlock
-except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
- from dummy import CodeBlock # NOQA
-
-import os
-
-
-class Listing(CodeBlock):
- """ listing directive: create a CodeBlock from file
-
- Usage:
-
- .. listing:: nikola.py python
- :number-lines:
-
- """
- has_content = False
- required_arguments = 1
- optional_arguments = 1
-
- option_spec = {
- 'start-at': directives.unchanged,
- 'end-at': directives.unchanged,
- 'start-after': directives.unchanged,
- 'end-before': directives.unchanged,
- }
-
- def run(self):
- fname = self.arguments.pop(0)
- with codecs_open(os.path.join('listings', fname), 'rb+', 'utf8') as fileobject:
- self.content = fileobject.read().splitlines()
- self.trim_content()
- target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = (
- [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
- generated_nodes += self.get_code_from_file(fileobject)
- return generated_nodes
-
- def trim_content(self):
- """Cut the contents based in options."""
- start = 0
- end = len(self.content)
- if 'start-at' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-at'] in l:
- break
- else:
- start = 0
- elif 'start-before' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-before'] in l:
- if start > 0:
- start -= 1
- break
- else:
- start = 0
- if 'end-at' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-at'] in l:
- break
- else:
- end = len(self.content)
- elif 'end-before' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-before'] in l:
- end -= 1
- break
- else:
- end = len(self.content)
-
- self.content = self.content[start:end]
-
- def get_code_from_file(self, data):
- """ Create CodeBlock nodes from file object content """
- return super(Listing, self).run()
-
- def assert_has_content(self):
- """ Listing has no content, override check from superclass """
- pass
-
-
-directives.register_directive('listing', Listing)
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
+
+
+""" Define and register a listing directive using the existing CodeBlock """
+
+
+from __future__ import unicode_literals
+from codecs import open as codecs_open # for patching purposes
+import os
+try:
+ from urlparse import urlunsplit
+except ImportError:
+ from urllib.parse import urlunsplit # NOQA
+
+from docutils import core
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst.directives.misc import Include
+try:
+ from docutils.parsers.rst.directives.body import CodeBlock
+except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
+ class CodeBlock(Directive):
+ required_arguments = 1
+ has_content = True
+ CODE = '<pre>{0}</pre>'
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ return [nodes.raw('', self.CODE.format('\n'.join(self.content)), format='html')]
+ directives.register_directive('code', CodeBlock)
+
+
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_listing"
+
+ def set_site(self, site):
+ self.site = site
+ # Even though listings don't use CodeBlock anymore, I am
+ # leaving these to make the code directive work with
+ # docutils < 0.9
+ directives.register_directive('code-block', CodeBlock)
+ directives.register_directive('sourcecode', CodeBlock)
+ directives.register_directive('listing', Listing)
+ return super(Plugin, self).set_site(site)
+
+
+class Listing(Include):
+ """ listing directive: create a highlighted block of code from a file in listings/
+
+ Usage:
+
+ .. listing:: nikola.py python
+ :number-lines:
+
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 1
+
+ def run(self):
+ fname = self.arguments.pop(0)
+ lang = self.arguments.pop(0)
+ fpath = os.path.join('listings', fname)
+ self.arguments.insert(0, fpath)
+ self.options['code'] = lang
+ with codecs_open(fpath, 'rb+', 'utf8') as fileobject:
+ self.content = fileobject.read().splitlines()
+ self.state.document.settings.record_dependencies.add(fpath)
+ target = urlunsplit(("link", 'listing', fname, '', ''))
+ generated_nodes = (
+ [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
+ generated_nodes += self.get_code_from_file(fileobject)
+ return generated_nodes
+
+ def get_code_from_file(self, data):
+ """ Create CodeBlock nodes from file object content """
+ return super(Listing, self).run()
+
+ def assert_has_content(self):
+ """ Listing has no content, override check from superclass """
+ pass
diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin
new file mode 100644
index 0000000..5f5276b
--- /dev/null
+++ b/nikola/plugins/compile/rest/media.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_media
+Module = media
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Directive to support oembed via micawber
+
diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py
new file mode 100644
index 0000000..d1930dd
--- /dev/null
+++ b/nikola/plugins/compile/rest/media.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+try:
+ import micawber
+except ImportError:
+ micawber = None # NOQA
+
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_media"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('media', Media)
+ return super(Plugin, self).set_site(site)
+
+
+class Media(Directive):
+ """ Restructured text extension for inserting any sort of media using micawber."""
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 999
+
+ def run(self):
+ if micawber is None:
+ msg = req_missing(['micawber'], 'use the media directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+
+ providers = micawber.bootstrap_basic()
+ return [nodes.raw('', micawber.parse_text(" ".join(self.arguments), providers), format='html')]
diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin
new file mode 100644
index 0000000..82450a0
--- /dev/null
+++ b/nikola/plugins/compile/rest/post_list.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = rest_post_list
+Module = post_list
+
+[Documentation]
+Author = Udo Spallek
+Version = 0.1
+Website = http://getnikola.com
+Description = Includes a list of posts with tag and slide based filters.
diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py
new file mode 100644
index 0000000..eae4016
--- /dev/null
+++ b/nikola/plugins/compile/rest/post_list.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013 Udo Spallek, Roberto Alsina and others.
+
+# 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 uuid
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+from nikola import utils
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+ name = "rest_post_list"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('post-list', PostList)
+ PostList.site = site
+ return super(Plugin, self).set_site(site)
+
+
+class PostList(Directive):
+ """
+ Post List
+ =========
+ :Directive Arguments: None.
+ :Directive Options: lang, start, stop, reverse, tags, template, id
+ :Directive Content: None.
+
+ Provides a reStructuredText directive to create a list of posts.
+ The posts appearing in the list can be filtered by options.
+ *List slicing* is provided with the *start*, *stop* and *reverse* options.
+
+ The following not required options are recognized:
+
+ ``start`` : integer
+ The index of the first post to show.
+ A negative value like ``-3`` will show the *last* three posts in the
+ post-list.
+ Defaults to None.
+
+ ``stop`` : integer
+ The index of the last post to show.
+ A value negative value like ``-1`` will show every post, but not the
+ *last* in the post-list.
+ Defaults to None.
+
+ ``reverse`` : flag
+ Reverse the order of the post-list.
+ Defaults is to not reverse the order of posts.
+
+ ``tags`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``tags``.
+ Defaults to None.
+
+ ``slugs`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``slugs``.
+ Defaults to None.
+
+ ``all`` : flag
+ Shows all posts and pages in the post list.
+ Defaults to show only posts with set *use_in_feeds*.
+
+ ``lang`` : string
+ The language of post *titles* and *links*.
+ Defaults to default language.
+
+ ``template`` : string
+ The name of an alternative template to render the post-list.
+ Defaults to ``post_list_directive.tmpl``
+
+ ``id`` : string
+ A manual id for the post list.
+ Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex.
+ """
+ option_spec = {
+ 'start': int,
+ 'stop': int,
+ 'reverse': directives.flag,
+ 'tags': directives.unchanged,
+ 'slugs': directives.unchanged,
+ 'all': directives.flag,
+ 'lang': directives.unchanged,
+ 'template': directives.path,
+ 'id': directives.unchanged,
+ }
+
+ def run(self):
+ start = self.options.get('start')
+ stop = self.options.get('stop')
+ reverse = self.options.get('reverse', False)
+ tags = self.options.get('tags')
+ tags = [t.strip().lower() for t in tags.split(',')] if tags else []
+ slugs = self.options.get('slugs')
+ slugs = [s.strip() for s in slugs.split(',')] if slugs else []
+ show_all = self.options.get('all', False)
+ lang = self.options.get('lang', utils.LocaleBorg().current_lang)
+ template = self.options.get('template', 'post_list_directive.tmpl')
+ post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex)
+
+ posts = []
+ step = -1 if reverse is None else None
+ if show_all is None:
+ timeline = [p for p in self.site.timeline]
+ else:
+ timeline = [p for p in self.site.timeline if p.use_in_feeds]
+
+ for post in timeline[start:stop:step]:
+ if tags:
+ cont = True
+ for tag in tags:
+ if tag in [t.lower() for t in post.tags]:
+ cont = False
+
+ if cont:
+ continue
+
+ if slugs:
+ cont = True
+ for slug in slugs:
+ if slug == post.meta('slug'):
+ cont = False
+
+ if cont:
+ continue
+
+ posts += [post]
+
+ if not posts:
+ return []
+
+ template_data = {
+ 'lang': lang,
+ 'posts': posts,
+ 'date_format': self.site.GLOBAL_CONTEXT.get('date_format'),
+ 'post_list_id': post_list_id,
+ }
+ output = self.site.template_system.render_template(
+ template, None, template_data)
+ return [nodes.raw('', output, format='html')]
diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin
new file mode 100644
index 0000000..cee4b06
--- /dev/null
+++ b/nikola/plugins/compile/rest/slides.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_slides
+Module = slides
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Slides directive
+
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile/rest/slides.py
index 57fb754..41c3314 100644
--- a/nikola/plugins/compile_rest/slides.py
+++ b/nikola/plugins/compile/rest/slides.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,9 +26,24 @@
from __future__ import unicode_literals
+import uuid
+
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_slides"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('slides', Slides)
+ Slides.site = site
+ return super(Plugin, self).set_site(site)
+
class Slides(Directive):
""" Restructured text extension for inserting slideshows."""
@@ -35,31 +52,16 @@ class Slides(Directive):
def run(self):
if len(self.content) == 0:
return
- output = []
- output.append("""
- <div id="myCarousel" class="carousel slide">
- <ol class="carousel-indicators">
- """)
- for i in range(len(self.content)):
- if i == 0:
- classname = 'class="active"'
- else:
- classname = ''
- output.append(' <li data-target="#myCarousel" data-slide-to="{0}" {1}></li>'.format(i, classname))
- output.append("""</ol>
- <div class="carousel-inner">
- """)
- for i, image in enumerate(self.content):
- if i == 0:
- classname = "item active"
- else:
- classname = "item"
- output.append("""<div class="{0}"><img src="{1}" alt="" style="margin: 0 auto 0 auto;"></div>""".format(classname, image))
- output.append("""</div>
- <a class="left carousel-control" href="#myCarousel" data-slide="prev">&lsaquo;</a>
- <a class="right carousel-control" href="#myCarousel" data-slide="next">&rsaquo;</a>
- </div>""")
- return [nodes.raw('', '\n'.join(output), format='html')]
+
+ output = self.site.template_system.render_template(
+ 'slides.tmpl',
+ None,
+ {
+ 'content': self.content,
+ 'carousel_id': 'slides_' + uuid.uuid4().hex,
+ }
+ )
+ return [nodes.raw('', output, format='html')]
directives.register_directive('slides', Slides)
diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin
new file mode 100644
index 0000000..1d31a8f
--- /dev/null
+++ b/nikola/plugins/compile/rest/soundcloud.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_soundcloud
+Module = soundcloud
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Soundcloud directive
+
diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py
index 6bdd4d5..6fb3e99 100644
--- a/nikola/plugins/compile_rest/soundcloud.py
+++ b/nikola/plugins/compile/rest/soundcloud.py
@@ -1,10 +1,23 @@
-# coding: utf8
+# -*- coding: utf-8 -*-
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_soundcloud"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('soundcloud', SoundCloud)
+ return super(Plugin, self).set_site(site)
+
+
CODE = ("""<iframe width="{width}" height="{height}"
scrolling="no" frameborder="no"
src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/"""
@@ -45,6 +58,3 @@ class SoundCloud(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('soundcloud', SoundCloud)
diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin
new file mode 100644
index 0000000..e0ff3f1
--- /dev/null
+++ b/nikola/plugins/compile/rest/vimeo.plugin
@@ -0,0 +1,7 @@
+[Core]
+Name = rest_vimeo
+Module = vimeo
+
+[Documentation]
+Description = Vimeo directive
+
diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py
index c1dc143..6d66648 100644
--- a/nikola/plugins/compile_rest/vimeo.py
+++ b/nikola/plugins/compile/rest/vimeo.py
@@ -1,5 +1,6 @@
-# coding: utf8
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,13 +32,21 @@ try:
import requests
except ImportError:
requests = None # NOQA
-try:
- import json # python 2.6 or higher
-except ImportError:
- try:
- import simplejson as json # NOQA
- except ImportError:
- json = None
+import json
+
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_vimeo"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('vimeo', Vimeo)
+ return super(Plugin, self).set_site(site)
CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}"
@@ -77,18 +86,19 @@ class Vimeo(Directive):
'height': VIDEO_DEFAULT_HEIGHT,
}
if self.request_size:
- self.check_modules()
+ err = self.check_modules()
+ if err:
+ return err
self.set_video_size()
options.update(self.options)
return [nodes.raw('', CODE.format(**options), format='html')]
def check_modules(self):
+ msg = None
if requests is None:
- raise Exception("To use the Vimeo directive you need to install "
- "the requests module.")
- if json is None:
- raise Exception("To use the Vimeo directive you need python 2.6 "
- "or to install the simplejson module.")
+ msg = req_missing(['requests'], 'use the vimeo directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+ return None
def set_video_size(self):
# Only need to make a connection if width and height aren't provided
@@ -113,6 +123,3 @@ class Vimeo(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('vimeo', Vimeo)
diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin
new file mode 100644
index 0000000..01275be
--- /dev/null
+++ b/nikola/plugins/compile/rest/youtube.plugin
@@ -0,0 +1,8 @@
+[Core]
+Name = rest_youtube
+Module = youtube
+
+[Documentation]
+Version = 0.1
+Description = Youtube directive
+
diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile/rest/youtube.py
index 767be32..3d4bdd3 100644
--- a/nikola/plugins/compile_rest/youtube.py
+++ b/nikola/plugins/compile/rest/youtube.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,19 @@ from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_youtube"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('youtube', Youtube)
+ return super(Plugin, self).set_site(site)
+
+
CODE = """\
<iframe width="{width}"
height="{height}"
@@ -64,6 +79,3 @@ class Youtube(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('youtube', Youtube)
diff --git a/nikola/plugins/compile_textile.plugin b/nikola/plugins/compile/textile.plugin
index c13b3b1..6439b0f 100644
--- a/nikola/plugins/compile_textile.plugin
+++ b/nikola/plugins/compile/textile.plugin
@@ -1,10 +1,10 @@
[Core]
Name = textile
-Module = compile_textile
+Module = textile
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Textile into HTML
diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile/textile.py
index 85efd3f..b402329 100644
--- a/nikola/plugins/compile_textile.py
+++ b/nikola/plugins/compile/textile.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,7 @@
import codecs
import os
+import re
try:
from textile import textile
@@ -33,6 +36,7 @@ except ImportError:
textile = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileTextile(PageCompiler):
@@ -40,17 +44,15 @@ class CompileTextile(PageCompiler):
name = "textile"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if textile is None:
- raise Exception('To build this site, you need to install the '
- '"textile" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['textile'], 'build this site (compile Textile)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = textile(data, head_offset=1)
out_file.write(output)
@@ -58,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<notextile> <!--\n')
diff --git a/nikola/plugins/compile_txt2tags.plugin b/nikola/plugins/compile/txt2tags.plugin
index 2c65da1..55eb0a0 100644
--- a/nikola/plugins/compile_txt2tags.plugin
+++ b/nikola/plugins/compile/txt2tags.plugin
@@ -1,10 +1,10 @@
[Core]
Name = txt2tags
-Module = compile_txt2tags
+Module = txt2tags
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Txt2tags into HTML
diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile/txt2tags.py
index 001da6e..2f62f04 100644
--- a/nikola/plugins/compile_txt2tags.py
+++ b/nikola/plugins/compile/txt2tags.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -39,6 +41,7 @@ except ImportError:
txt2tags = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileTextile(PageCompiler):
@@ -46,14 +49,10 @@ class CompileTextile(PageCompiler):
name = "txt2tags"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if txt2tags is None:
- raise Exception('To build this site, you need to install the '
- '"txt2tags" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['txt2tags'], 'build this site (compile txt2tags)')
+ makedirs(os.path.dirname(dest))
cmd = ["-t", "html", "--no-headers", "--outfile", dest, source]
txt2tags(cmd)
@@ -61,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write("\n'''\n<!--\n")
diff --git a/nikola/plugins/compile_wiki.plugin b/nikola/plugins/compile/wiki.plugin
index 65cd942..eee14a8 100644
--- a/nikola/plugins/compile_wiki.plugin
+++ b/nikola/plugins/compile/wiki.plugin
@@ -1,10 +1,10 @@
[Core]
Name = wiki
-Module = compile_wiki
+Module = wiki
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile WikiMarkup into HTML
diff --git a/nikola/plugins/compile_wiki.py b/nikola/plugins/compile/wiki.py
index fb9e010..b2c4afc 100644
--- a/nikola/plugins/compile_wiki.py
+++ b/nikola/plugins/compile/wiki.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,7 +24,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on textile."""
+"""Implementation of compile_html based on CreoleWiki."""
import codecs
import os
@@ -35,21 +37,18 @@ except ImportError:
creole = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileTextile(PageCompiler):
- """Compile textile into HTML."""
+class CompileWiki(PageCompiler):
+ """Compile CreoleWiki into HTML."""
name = "wiki"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if creole is None:
- raise Exception('To build this site, you need to install the '
- '"creole" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['creole'], 'build this site (compile CreoleWiki)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
@@ -61,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
if onefile:
raise Exception('There are no comments in CreoleWiki markup, so '
'one-file format is not possible, use the -2 '
diff --git a/nikola/plugins/compile_ipynb/README.txt b/nikola/plugins/compile_ipynb/README.txt
deleted file mode 100644
index 2cfd45e..0000000
--- a/nikola/plugins/compile_ipynb/README.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-To make this work...
-
-1- First, you have to put this plugin in your_site/plugins/ folder.
-
-2- Then, you have to download the custom nbconvert from here: https://github.com/damianavila/compile_ipynb-for-Nikola.git
-and put it inside your_site/plugins/compile_ipynb/ folder
-
-3- Also, you have to use the site-ipython theme (or make a new one containing the ipython css, mathjax.js and the proper template).
-You can get it here: https://github.com/damianavila/site-ipython-theme-for-Nikola
-
-4- Finally, you have to put:
-
-post_pages = (
- ("posts/*.ipynb", "posts", "post.tmpl", True),
- ("stories/*.ipynb", "stories", "story.tmpl", False),
-)
-
-in your conf.py
-
-Then... to use it:
-
-$nikola new_page -f ipynb
-
-**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
-create one metadata file and the corresponding naive IPython notebook.
-
-$nikola build
-
-And deploy the output folder... to see it locally: $nikola serve
-
-If you have any doubts, just ask: @damianavila
-
-Cheers.
-
-Damián
diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py
deleted file mode 100644
index 3d41571..0000000
--- a/nikola/plugins/compile_rest/__init__.py
+++ /dev/null
@@ -1,138 +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 codecs
-import os
-
-try:
- import docutils.core
- import docutils.io
- from docutils.parsers.rst import directives
-
- from .listing import Listing, CodeBlock
- directives.register_directive('code-block', CodeBlock)
- directives.register_directive('sourcecode', CodeBlock)
- directives.register_directive('listing', Listing)
- from .youtube import Youtube
- directives.register_directive('youtube', Youtube)
- from .vimeo import Vimeo
- directives.register_directive('vimeo', Vimeo)
- 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)
- has_docutils = True
-except ImportError:
- has_docutils = False
-
-from nikola.plugin_categories import PageCompiler
-
-
-class CompileRest(PageCompiler):
- """Compile reSt into HTML."""
-
- name = "rest"
-
- def compile_html(self, source, dest):
- """Compile reSt into HTML."""
- if not has_docutils:
- raise Exception('To build this site, you need to install the '
- '"docutils" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- error_level = 100
- with codecs.open(dest, "w+", "utf8") as out_file:
- with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
- output, error_level, deps = rst2html(
- data, settings_overrides={
- 'initial_header_level': 2,
- 'record_dependencies': True,
- 'stylesheet_path': None,
- 'link_stylesheet': True,
- 'syntax_highlight': 'short',
- })
- out_file.write(output)
- deps_path = dest + '.dep'
- if deps.list:
- with codecs.open(deps_path, "wb+", "utf8") as deps_file:
- deps_file.write('\n'.join(deps.list))
- else:
- if os.path.isfile(deps_path):
- os.unlink(deps_path)
- if error_level < 3:
- return True
- else:
- return False
-
- def create_post(self, path, onefile=False, **kw):
- metadata = {}
- metadata.update(self.default_metadata)
- metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
- with codecs.open(path, "wb+", "utf8") as fd:
- if onefile:
- for k, v in metadata.items():
- fd.write('.. {0}: {1}\n'.format(k, v))
- fd.write("\nWrite your post here.")
-
-
-def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
- destination_path=None, reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext', writer=None,
- writer_name='html', settings=None, settings_spec=None,
- settings_overrides=None, config_section=None,
- enable_exit_status=None):
- """
- Set up & run a `Publisher`, and return a dictionary of document parts.
- Dictionary keys are the names of parts, and values are Unicode strings;
- encoding is up to the client. For programmatic use with string I/O.
-
- For encoded string input, be sure to set the 'input_encoding' setting to
- the desired encoding. Set it to 'unicode' for unencoded Unicode string
- input. Here's how::
-
- publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
-
- Parameters: see `publish_programmatically`.
- """
- output, pub = docutils.core.publish_programmatically(
- source=source, source_path=source_path, source_class=source_class,
- destination_class=docutils.io.StringOutput,
- destination=None, destination_path=destination_path,
- reader=reader, reader_name=reader_name,
- parser=parser, parser_name=parser_name,
- writer=writer, writer_name=writer_name,
- settings=settings, settings_spec=settings_spec,
- settings_overrides=settings_overrides,
- config_section=config_section,
- enable_exit_status=enable_exit_status)
- return pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
diff --git a/nikola/plugins/loghandler/smtp.plugin b/nikola/plugins/loghandler/smtp.plugin
new file mode 100644
index 0000000..e914b3d
--- /dev/null
+++ b/nikola/plugins/loghandler/smtp.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = smtp
+Module = smtp
+
+[Documentation]
+Author = Daniel Devine
+Version = 0.1
+Website = http://getnikola.com
+Description = Log over smtp (email).
diff --git a/nikola/plugins/loghandler/smtp.py b/nikola/plugins/loghandler/smtp.py
new file mode 100644
index 0000000..deb8f4e
--- /dev/null
+++ b/nikola/plugins/loghandler/smtp.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Daniel Devine and others.
+
+# 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 nikola.plugin_categories import SignalHandler
+from blinker import signal
+import logbook
+
+
+class SmtpHandler(SignalHandler):
+ name = 'smtp'
+
+ def attach_handler(self, sender):
+ """Add the handler to a list of handlers that are attached when get_logger() is called.."""
+ smtpconf = self.site.config.get('LOGGING_HANDLERS').get('smtp')
+ if smtpconf:
+ smtpconf['format_string'] = '''\
+Subject: {record.level_name}: {record.channel}
+
+{record.message}
+'''
+ self.site.loghandlers.append(logbook.MailHandler(
+ smtpconf.pop('from_addr'),
+ smtpconf.pop('recipients'),
+ **smtpconf
+ ))
+
+ def set_site(self, site):
+ self.site = site
+
+ ready = signal('sighandlers_loaded')
+ ready.connect(self.attach_handler)
diff --git a/nikola/plugins/loghandler/stderr.plugin b/nikola/plugins/loghandler/stderr.plugin
new file mode 100644
index 0000000..211d2b4
--- /dev/null
+++ b/nikola/plugins/loghandler/stderr.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = stderr
+Module = stderr
+
+[Documentation]
+Author = Daniel Devine
+Version = 0.1
+Website = http://getnikola.com
+Description = Log to stderr, the default logger.
diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py
new file mode 100644
index 0000000..71f1de5
--- /dev/null
+++ b/nikola/plugins/loghandler/stderr.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Daniel Devine and others.
+
+# 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 nikola.plugin_categories import SignalHandler
+from blinker import signal
+import logbook
+import os
+
+
+class StderrHandler(SignalHandler):
+ """Logs messages to stderr."""
+ name = 'stderr'
+
+ def attach_handler(self, sender):
+ """Attach the handler to the logger."""
+ conf = self.site.config.get('LOGGING_HANDLERS').get('stderr')
+ if conf or os.getenv('NIKOLA_DEBUG'):
+ self.site.loghandlers.append(logbook.StderrHandler(
+ level='DEBUG' if os.getenv('NIKOLA_DEBUG') else conf.get('loglevel', 'WARNING').upper(),
+ format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}'
+ ))
+
+ def set_site(self, site):
+ self.site = site
+
+ ready = signal('sighandlers_loaded')
+ ready.connect(self.attach_handler)
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/task/__init__.py
diff --git a/nikola/plugins/task_archive.plugin b/nikola/plugins/task/archive.plugin
index 23f93ed..448b115 100644
--- a/nikola/plugins/task_archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_archive
-Module = task_archive
+Module = archive
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's archive pages.
diff --git a/nikola/plugins/task_archive.py b/nikola/plugins/task/archive.py
index a67826f..3afbea1 100644
--- a/nikola/plugins/task_archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,10 +24,10 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import calendar
import os
-import sys
+# for tearDown with _reload we cannot use 'import from' to access LocaleBorg
+import nikola.utils
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -35,6 +37,10 @@ class Archive(Task):
name = "render_archive"
+ def set_site(self, site):
+ site.register_path_handler('archive', self.archive_path)
+ return super(Archive, self).set_site(site)
+
def gen_tasks(self):
kw = {
"messages": self.site.MESSAGES,
@@ -42,16 +48,28 @@ class Archive(Task):
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
+ "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'],
}
self.site.scan_posts()
+ yield self.group_task()
# TODO add next/prev links for years
+ if kw['create_monthly_archive'] and kw['create_single_archive']:
+ raise Exception('Cannot create monthly and single archives at the same time.')
for lang in kw["translations"]:
- for year, posts in self.site.posts_per_year.items():
+ archdata = self.site.posts_per_year
+ # A bit of a hack.
+ if kw['create_single_archive']:
+ archdata = {None: self.site.posts}
+
+ for year, posts in archdata.items():
output_name = os.path.join(
kw['output_folder'], self.site.path("archive", year, lang))
context = {}
context["lang"] = lang
- context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ if year:
+ context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ else:
+ context["title"] = kw["messages"][lang]["Archive"]
context["permalink"] = self.site.link("archive", year, lang)
if not kw["create_monthly_archive"]:
template_name = "list_post.tmpl"
@@ -62,8 +80,9 @@ class Archive(Task):
else: # Monthly archives, just list the months
months = set([m.split('/')[1] for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
months = sorted(list(months))
+ months.reverse()
template_name = "list.tmpl"
- context["items"] = [[get_month_name(int(month), lang), month] for month in months]
+ context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), month] for month in months]
post_list = []
task = self.site.generic_post_list_renderer(
lang,
@@ -95,7 +114,7 @@ class Archive(Task):
context["permalink"] = self.site.link("archive", year, lang)
context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
- year=year, month=get_month_name(int(month), lang))
+ year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -109,39 +128,40 @@ class Archive(Task):
task['basename'] = self.name
yield task
- # And global "all your years" page
- years = list(self.site.posts_per_year.keys())
- years.sort(reverse=True)
- template_name = "list.tmpl"
- kw['years'] = years
- for lang in kw["translations"]:
- context = {}
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", None,
- lang))
- context["title"] = kw["messages"][lang]["Archive"]
- context["items"] = [(year, self.site.link("archive", year, lang))
- for year in years]
- context["permalink"] = self.site.link("archive", None, lang)
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
+ if not kw['create_single_archive']:
+ # And an "all your years" page for yearly and monthly archives
+ years = list(self.site.posts_per_year.keys())
+ years.sort(reverse=True)
+ template_name = "list.tmpl"
+ kw['years'] = years
+ for lang in kw["translations"]:
+ context = {}
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", None,
+ lang))
+ context["title"] = kw["messages"][lang]["Archive"]
+ context["items"] = [(year, self.site.link("archive", year, lang))
+ for year in years]
+ context["permalink"] = self.site.link("archive", None, lang)
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = self.name
+ yield task
-def get_month_name(month_no, locale):
- if sys.version_info[0] == 3: # Python 3
- with calendar.different_locale((locale, "UTF-8")):
- s = calendar.month_name[month_no]
- else: # Python 2
- with calendar.TimeEncoding((locale, "UTF-8")):
- s = calendar.month_name[month_no]
- return s
+ def archive_path(self, name, lang):
+ if name:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['ARCHIVE_PATH'], name,
+ self.site.config['INDEX_FILE']] if _f]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['ARCHIVE_PATH'],
+ self.site.config['ARCHIVE_FILENAME']] if _f]
diff --git a/nikola/plugins/task/build_less.plugin b/nikola/plugins/task/build_less.plugin
new file mode 100644
index 0000000..27ca8cd
--- /dev/null
+++ b/nikola/plugins/task/build_less.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = build_less
+Module = build_less
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Build CSS out of LESS sources
+
diff --git a/nikola/plugins/task/build_less.py b/nikola/plugins/task/build_less.py
new file mode 100644
index 0000000..8889cbe
--- /dev/null
+++ b/nikola/plugins/task/build_less.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 codecs
+import glob
+import os
+import subprocess
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class BuildLess(Task):
+ """Generate CSS out of LESS sources."""
+
+ name = "build_less"
+ sources_folder = "less"
+ sources_ext = ".less"
+ compiler_name = "lessc"
+
+ def gen_tasks(self):
+ """Generate CSS out of LESS sources."""
+
+ kw = {
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'themes': self.site.THEMES,
+ }
+
+ # Find where in the theme chain we define the LESS targets
+ # There can be many *.less in the folder, but we only will build
+ # the ones listed in less/targets
+ targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES)
+ try:
+ with codecs.open(targets_path, "rb", "utf-8") as inf:
+ targets = [x.strip() for x in inf.readlines()]
+ except Exception:
+ targets = []
+
+ for theme_name in kw['themes']:
+ src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder)
+ for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)):
+ task['basename'] = 'prepare_less_sources'
+ yield task
+
+ # Build targets and write CSS files
+ base_path = utils.get_theme_path(self.site.THEMES[0])
+ dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css')
+ # Make everything depend on all sources, rough but enough
+ deps = glob.glob(os.path.join(
+ base_path,
+ self.sources_folder,
+ "*{0}".format(self.sources_ext)))
+
+ def compile_target(target, dst):
+ utils.makedirs(dst_dir)
+ src = os.path.join(kw['cache_folder'], self.sources_folder, target)
+ compiled = subprocess.check_output([self.compiler_name, src])
+ with open(dst, "wb+") as outf:
+ outf.write(compiled)
+
+ yield self.group_task()
+
+ for target in targets:
+ dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css"))
+ yield {
+ 'basename': self.name,
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': deps,
+ 'task_dep': ['prepare_less_sources'],
+ 'actions': ((compile_target, [target, dst]), ),
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True
+ }
diff --git a/nikola/plugins/task/build_sass.plugin b/nikola/plugins/task/build_sass.plugin
new file mode 100644
index 0000000..746c1df
--- /dev/null
+++ b/nikola/plugins/task/build_sass.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = build_sass
+Module = build_sass
+
+[Documentation]
+Author = Roberto Alsina, Chris “Kwpolska” Warrick
+Version = 0.1
+Website = http://getnikola.com
+Description = Build CSS out of Sass sources
diff --git a/nikola/plugins/task/build_sass.py b/nikola/plugins/task/build_sass.py
new file mode 100644
index 0000000..a5d22fb
--- /dev/null
+++ b/nikola/plugins/task/build_sass.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 codecs
+import glob
+import os
+import subprocess
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class BuildSass(Task):
+ """Generate CSS out of Sass sources."""
+
+ name = "build_sass"
+ sources_folder = "sass"
+ sources_ext = (".sass", ".scss")
+ compiler_name = "sass"
+
+ def gen_tasks(self):
+ """Generate CSS out of Sass sources."""
+ self.logger = utils.get_logger('build_sass', self.site.loghandlers)
+
+ kw = {
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'themes': self.site.THEMES,
+ }
+
+ # Find where in the theme chain we define the Sass targets
+ # There can be many *.sass/*.scss in the folder, but we only
+ # will build the ones listed in sass/targets
+ targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES)
+ try:
+ with codecs.open(targets_path, "rb", "utf-8") as inf:
+ targets = [x.strip() for x in inf.readlines()]
+ except Exception:
+ targets = []
+
+ for theme_name in kw['themes']:
+ src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder)
+ for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)):
+ task['basename'] = 'prepare_sass_sources'
+ yield task
+
+ # Build targets and write CSS files
+ base_path = utils.get_theme_path(self.site.THEMES[0])
+ dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css')
+ # Make everything depend on all sources, rough but enough
+ deps = glob.glob(os.path.join(
+ base_path,
+ self.sources_folder,
+ *("*{0}".format(ext) for ext in self.sources_ext)))
+
+ def compile_target(target, dst):
+ utils.makedirs(dst_dir)
+ src = os.path.join(kw['cache_folder'], self.sources_folder, target)
+ compiled = subprocess.check_output([self.compiler_name, src])
+ with open(dst, "wb+") as outf:
+ outf.write(compiled)
+
+ yield self.group_task()
+
+ # We can have file conflicts. This is a way to prevent them.
+ # I orignally wanted to use sets and their cannot-have-duplicates
+ # magic, but I decided not to do this so we can show the user
+ # what files were problematic.
+ # If we didn’t do this, there would be a cryptic message from doit
+ # instead.
+ seennames = {}
+ for target in targets:
+ base = os.path.splitext(target)[0]
+ dst = os.path.join(dst_dir, base + ".css")
+
+ if base in seennames:
+ self.logger.error(
+ 'Duplicate filenames for SASS compiled files: {0} and '
+ '{1} (both compile to {2})'.format(
+ seennames[base], target, base + ".css"))
+ else:
+ seennames.update({base: target})
+
+ yield {
+ 'basename': self.name,
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': deps,
+ 'task_dep': ['prepare_sass_sources'],
+ 'actions': ((compile_target, [target, dst]), ),
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True
+ }
diff --git a/nikola/plugins/task_create_bundles.plugin b/nikola/plugins/task/bundles.plugin
index 5d4f6d3..e0b0a4d 100644
--- a/nikola/plugins/task_create_bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -1,10 +1,10 @@
[Core]
Name = create_bundles
-Module = task_create_bundles
+Module = bundles
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Theme bundles using WebAssets
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task/bundles.py
index 84ac0ab..488f96f 100644
--- a/nikola/plugins/task_create_bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -38,7 +40,7 @@ from nikola import utils
class BuildBundles(LateTask):
"""Bundle assets using WebAssets."""
- name = "build_bundles"
+ name = "create_bundles"
def set_site(self, site):
super(BuildBundles, self).set_site(site)
@@ -64,44 +66,38 @@ class BuildBundles(LateTask):
inputs = [i for i in inputs if os.path.isfile(
os.path.join(out_dir, i))]
cache_dir = os.path.join(kw['cache_folder'], 'webassets')
- if not os.path.isdir(cache_dir):
- os.makedirs(cache_dir)
+ utils.makedirs(cache_dir)
env = webassets.Environment(out_dir, os.path.dirname(output),
cache=cache_dir)
- bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
- env.register(output, bundle)
- # This generates the file
- env[output].urls()
+ if inputs:
+ bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
+ env.register(output, bundle)
+ # This generates the file
+ env[output].urls()
+ else:
+ with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'):
+ pass # Create empty file
- flag = False
+ yield self.group_task()
if (webassets is not None and self.site.config['USE_BUNDLES'] is not
False):
for name, files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
- file_dep = [utils.get_asset_path(
- os.path.join(dname, fname), kw['themes'],
- kw['files_folders'])
- for fname in files
- ]
- file_dep = filter(None, file_dep) # removes missing files
+ file_dep = [os.path.join(kw['output_folder'], dname, fname)
+ for fname in files]
+ file_dep = filter(os.path.isfile, file_dep) # removes missing files
task = {
- 'file_dep': file_dep,
+ 'file_dep': list(file_dep),
+ 'task_dep': ['copy_assets'],
'basename': str(self.name),
'name': str(output_path),
'actions': [(build_bundle, (name, files))],
'targets': [output_path],
- 'uptodate': [utils.config_changed(kw)]
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True,
}
- flag = True
yield utils.apply_filters(task, kw['filters'])
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': self.name,
- 'uptodate': [True],
- 'name': 'None',
- 'actions': [],
- }
def get_theme_bundles(themes):
diff --git a/nikola/plugins/task_copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index b11133f..28b9e32 100644
--- a/nikola/plugins/task_copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -1,10 +1,10 @@
[Core]
Name = copy_assets
-Module = task_copy_assets
+Module = copy_assets
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy theme assets into output.
diff --git a/nikola/plugins/task_copy_assets.py b/nikola/plugins/task/copy_assets.py
index 06d17e7..f3d85df 100644
--- a/nikola/plugins/task_copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -47,10 +49,12 @@ class CopyAssets(Task):
"filters": self.site.config['FILTERS'],
"code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
}
- flag = True
has_code_css = False
tasks = {}
code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
+
+ yield self.group_task()
+
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
@@ -62,22 +66,14 @@ class CopyAssets(Task):
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw)]
task['basename'] = self.name
- flag = False
yield utils.apply_filters(task, kw['filters'])
- if flag:
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
if not has_code_css: # Generate it
def create_code_css():
from pygments.formatters import get_formatter_by_name
formatter = get_formatter_by_name('html', style=kw["code_color_scheme"])
+ utils.makedirs(os.path.dirname(code_css_path))
with codecs.open(code_css_path, 'wb+', 'utf8') as outf:
outf.write(formatter.get_style_defs('.code'))
outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}")
diff --git a/nikola/plugins/task_copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index 0bfc5be..45c9e0d 100644
--- a/nikola/plugins/task_copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -1,10 +1,10 @@
[Core]
Name = copy_files
-Module = task_copy_files
+Module = copy_files
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy static files into the output.
diff --git a/nikola/plugins/task_copy_files.py b/nikola/plugins/task/copy_files.py
index feaf147..88e89eb 100644
--- a/nikola/plugins/task_copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -42,18 +44,12 @@ class CopyFiles(Task):
'filters': self.site.config['FILTERS'],
}
- flag = False
+ yield self.group_task()
for src in kw['files_folders']:
dst = kw['output_folder']
filters = kw['filters']
real_dst = os.path.join(dst, kw['files_folders'][src])
for task in utils.copy_tree(src, real_dst, link_cutoff=dst):
- flag = True
task['basename'] = self.name
task['uptodate'] = [utils.config_changed(kw)]
yield utils.apply_filters(task, filters)
- if not flag:
- yield {
- 'basename': self.name,
- 'actions': (),
- }
diff --git a/nikola/plugins/task_render_galleries.plugin b/nikola/plugins/task/galleries.plugin
index e0a86c0..8352151 100644
--- a/nikola/plugins/task_render_galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_galleries
-Module = task_render_galleries
+Module = galleries
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create image galleries automatically.
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
new file mode 100644
index 0000000..cf670e0
--- /dev/null
+++ b/nikola/plugins/task/galleries.py
@@ -0,0 +1,553 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 codecs
+import datetime
+import glob
+import json
+import mimetypes
+import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
+
+Image = None
+try:
+ from PIL import Image, ExifTags # NOQA
+except ImportError:
+ try:
+ import Image as _Image
+ import ExifTags
+ Image = _Image
+ except ImportError:
+ pass
+import PyRSS2Gen as rss
+
+from nikola.plugin_categories import Task
+from nikola import utils
+from nikola.post import Post
+
+
+class Galleries(Task):
+ """Render image galleries."""
+
+ name = 'render_galleries'
+ dates = {}
+
+ def set_site(self, site):
+ site.register_path_handler('gallery', self.gallery_path)
+ site.register_path_handler('gallery_rss', self.gallery_rss_path)
+ return super(Galleries, self).set_site(site)
+
+ def gallery_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ self.site.config['INDEX_FILE']] if _f]
+
+ def gallery_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ 'rss.xml'] if _f]
+
+ def gen_tasks(self):
+ """Render image galleries."""
+
+ self.logger = utils.get_logger('render_galleries', self.site.loghandlers)
+ self.image_ext_list = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+ self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
+
+ self.kw = {
+ 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'default_lang': self.site.config['DEFAULT_LANG'],
+ 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_path': self.site.config['GALLERY_PATH'],
+ 'sort_by_date': self.site.config['GALLERY_SORT_BY_DATE'],
+ 'filters': self.site.config['FILTERS'],
+ 'translations': self.site.config['TRANSLATIONS'],
+ 'global_context': self.site.GLOBAL_CONTEXT,
+ "feed_length": self.site.config['FEED_LENGTH'],
+ }
+
+ yield self.group_task()
+
+ template_name = "gallery.tmpl"
+
+ # Find all galleries we need to process
+ self.find_galleries()
+
+ # Create all output folders
+ for task in self.create_galleries():
+ yield task
+
+ # For each gallery:
+ for gallery in self.gallery_list:
+
+ # Create subfolder list
+ folder_list = [x.split(os.sep)[-2] for x in
+ glob.glob(os.path.join(gallery, '*') + os.sep)]
+
+ # Parse index into a post (with translations)
+ post = self.parse_index(gallery)
+
+ # Create image list, filter exclusions
+ image_list = self.get_image_list(gallery)
+
+ # Sort as needed
+ # Sort by date
+ if self.kw['sort_by_date']:
+ image_list.sort(key=lambda a: self.image_date(a))
+ else: # Sort by name
+ image_list.sort()
+
+ # Create thumbnails and large images in destination
+ for image in image_list:
+ for task in self.create_target_images(image):
+ yield task
+
+ # Remove excluded images
+ for image in self.get_excluded_images(gallery):
+ for task in self.remove_excluded_image(image):
+ yield task
+
+ crumbs = utils.get_crumbs(gallery)
+
+ # Create index.html for each language
+ for lang in self.kw['translations']:
+ dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ dst = os.path.normpath(dst)
+
+ context = {}
+ context["lang"] = lang
+ if post:
+ context["title"] = post.title(lang)
+ else:
+ context["title"] = os.path.basename(gallery)
+ context["description"] = None
+
+ image_name_list = [os.path.basename(p) for p in image_list]
+
+ if self.kw['use_filename_as_title']:
+ img_titles = []
+ for fn in image_name_list:
+ name_without_ext = os.path.splitext(fn)[0]
+ img_titles.append(
+ 'id="{0}" alt="{1}" title="{2}"'.format(
+ name_without_ext,
+ name_without_ext,
+ utils.unslugify(name_without_ext)))
+ else:
+ img_titles = [''] * len(image_name_list)
+
+ thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list]
+ thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs]
+
+ ## TODO: in v7 remove images from context, use photo_array
+ context["images"] = list(zip(image_name_list, thumbs, img_titles))
+ context["folders"] = folder_list
+ context["crumbs"] = crumbs
+ context["permalink"] = self.site.link(
+ "gallery", os.path.basename(gallery), lang)
+ # FIXME: use kw
+ context["enable_comments"] = (
+ self.site.config["COMMENTS_IN_GALLERIES"])
+ context["thumbnail_size"] = self.kw["thumbnail_size"]
+
+ # FIXME: render post in a task
+ if post:
+ post.compile(lang)
+ context['text'] = post.text(lang)
+ else:
+ context['text'] = ''
+
+ file_dep = self.site.template_system.template_deps(
+ template_name) + image_list + thumbs
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': dst,
+ 'file_dep': file_dep,
+ 'targets': [dst],
+ 'actions': [
+ (self.render_gallery_index, (
+ template_name,
+ dst,
+ context,
+ image_list,
+ thumbs,
+ file_dep))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ 2: self.site.config["COMMENTS_IN_GALLERIES"],
+ 3: context,
+ })],
+ }, self.kw['filters'])
+
+ # RSS for the gallery
+ rss_dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery_rss",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ rss_dst = os.path.normpath(rss_dst)
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': rss_dst,
+ 'file_dep': file_dep,
+ 'targets': [rss_dst],
+ 'actions': [
+ (self.gallery_rss, (
+ image_list,
+ img_titles,
+ lang,
+ self.site.link(
+ "gallery_rss", os.path.basename(gallery), lang),
+ rss_dst,
+ context['title']
+ ))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ })],
+ }, self.kw['filters'])
+
+ def find_galleries(self):
+ """Find all galleries to be processed according to conf.py"""
+
+ self.gallery_list = []
+ for root, dirs, files in os.walk(self.kw['gallery_path']):
+ self.gallery_list.append(root)
+
+ def create_galleries(self):
+ """Given a list of galleries, create the output folders."""
+
+ # gallery_path is "gallery/foo/name"
+ for gallery_path in self.gallery_list:
+ gallery_name = os.path.relpath(gallery_path, self.kw['gallery_path'])
+ # have to use dirname because site.path returns .../index.html
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ output_gallery = os.path.normpath(output_gallery)
+ # Task to create gallery in output/
+ yield {
+ 'basename': self.name,
+ 'name': output_gallery,
+ 'actions': [(utils.makedirs, (output_gallery,))],
+ 'targets': [output_gallery],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }
+
+ def parse_index(self, gallery):
+ """Returns a Post object if there is an index.txt."""
+
+ index_path = os.path.join(gallery, "index.txt")
+ destination = os.path.join(
+ self.kw["output_folder"],
+ gallery)
+ if os.path.isfile(index_path):
+ post = Post(
+ index_path,
+ self.site.config,
+ destination,
+ False,
+ self.site.MESSAGES,
+ 'story.tmpl',
+ self.site.get_compiler(index_path).compile_html
+ )
+ else:
+ post = None
+ return post
+
+ def get_excluded_images(self, gallery_path):
+ exclude_path = os.path.join(gallery_path, "exclude.meta")
+
+ try:
+ f = open(exclude_path, 'r')
+ excluded_image_name_list = f.read().split()
+ except IOError:
+ excluded_image_name_list = []
+
+ excluded_image_list = ["{0}/{1}".format(gallery_path, i) for i in excluded_image_name_list]
+ return excluded_image_list
+
+ def get_image_list(self, gallery_path):
+
+ # Gather image_list contains "gallery/name/image_name.jpg"
+ image_list = []
+
+ for ext in self.image_ext_list:
+ image_list += glob.glob(gallery_path + '/*' + ext.lower()) +\
+ glob.glob(gallery_path + '/*' + ext.upper())
+
+ # Filter ignored images
+ excluded_image_list = self.get_excluded_images(gallery_path)
+ image_set = set(image_list) - set(excluded_image_list)
+ image_list = list(image_set)
+ return image_list
+
+ def create_target_images(self, img):
+ gallery_name = os.path.relpath(os.path.dirname(img), self.kw['gallery_path'])
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ # Do thumbnails and copy originals
+ # img is "galleries/name/image_name.jpg"
+ # img_name is "image_name.jpg"
+ # fname, ext are "image_name", ".jpg"
+ # thumb_path is
+ # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
+ img_name = os.path.basename(img)
+ fname, ext = os.path.splitext(img_name)
+ thumb_path = os.path.join(
+ output_gallery,
+ ".thumbnail".join([fname, ext]))
+ # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
+ orig_dest_path = os.path.join(output_gallery, img_name)
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': thumb_path,
+ 'file_dep': [img],
+ 'targets': [thumb_path],
+ 'actions': [
+ (self.resize_image,
+ (img, thumb_path, self.kw['thumbnail_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['thumbnail_size']
+ })],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': orig_dest_path,
+ 'file_dep': [img],
+ 'targets': [orig_dest_path],
+ 'actions': [
+ (self.resize_image,
+ (img, orig_dest_path, self.kw['max_image_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['max_image_size']
+ })],
+ }, self.kw['filters'])
+
+ def remove_excluded_image(self, img):
+ # Remove excluded images
+ # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path
+ # and we should remove both the large and thumbnail *destination* paths
+
+ img = os.path.relpath(img, self.kw['gallery_path'])
+ output_folder = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", os.path.dirname(img))))
+ img_path = os.path.join(output_folder, os.path.basename(img))
+ fname, ext = os.path.splitext(img_path)
+ thumb_path = fname + '.thumbnail' + ext
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': thumb_path,
+ 'actions': [
+ (utils.remove_file, (thumb_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': img_path,
+ 'actions': [
+ (utils.remove_file, (img_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ def render_gallery_index(
+ self,
+ template_name,
+ output_name,
+ context,
+ img_list,
+ thumbs,
+ file_dep):
+ """Build the gallery index."""
+
+ # The photo array needs to be created here, because
+ # it relies on thumbnails already being created on
+ # output
+
+ def url_from_path(p):
+ url = '/'.join(os.path.relpath(p, os.path.dirname(output_name) + os.sep).split(os.sep))
+ return url
+
+ photo_array = []
+ for img, thumb in zip(img_list, thumbs):
+ im = Image.open(thumb)
+ w, h = im.size
+ title = ''
+ if self.kw['use_filename_as_title']:
+ title = utils.unslugify(os.path.splitext(img)[0])
+ # Thumbs are files in output, we need URLs
+ photo_array.append({
+ 'url': url_from_path(img),
+ 'url_thumb': url_from_path(thumb),
+ 'title': title,
+ 'size': {
+ 'w': w,
+ 'h': h
+ },
+ })
+ context['photo_array_json'] = json.dumps(photo_array)
+ context['photo_array'] = photo_array
+
+ self.site.render_template(template_name, output_name, context)
+
+ def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title):
+ """Create a RSS showing the latest images in the gallery.
+
+ This doesn't use generic_rss_renderer because it
+ doesn't involve Post objects.
+ """
+
+ def make_url(url):
+ return urljoin(self.site.config['BASE_URL'], url)
+
+ items = []
+ for img, full_title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]:
+ img_size = os.stat(
+ os.path.join(
+ self.site.config['OUTPUT_FOLDER'], img)).st_size
+ args = {
+ 'title': full_title.split('"')[-2],
+ 'link': make_url(img),
+ 'guid': rss.Guid(img, False),
+ 'pubDate': self.image_date(img),
+ 'enclosure': rss.Enclosure(
+ make_url(img),
+ img_size,
+ mimetypes.guess_type(img)[0]
+ ),
+ }
+ items.append(rss.RSSItem(**args))
+ rss_obj = utils.ExtendedRSS2(
+ title=title,
+ link=make_url(permalink),
+ description='',
+ lastBuildDate=datetime.datetime.now(),
+ items=items,
+ generator='nikola',
+ language=lang
+ )
+ rss_obj.self_url = make_url(permalink)
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
+ dst_dir = os.path.dirname(output_path)
+ utils.makedirs(dst_dir)
+ with codecs.open(output_path, "wb+", "utf-8") as rss_file:
+ data = rss_obj.to_xml(encoding='utf-8')
+ if isinstance(data, utils.bytes_str):
+ data = data.decode('utf-8')
+ rss_file.write(data)
+
+ def resize_image(self, src, dst, max_size):
+ """Make a copy of the image in the requested size."""
+ if not Image:
+ utils.copy_file(src, dst)
+ return
+ im = Image.open(src)
+ w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+
+ # Panoramas get larger thumbnails because they look *awful*
+ if w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+
+ if decoded == 'Orientation':
+ if value == 3:
+ im = im.rotate(180)
+ elif value == 6:
+ im = im.rotate(270)
+ elif value == 8:
+ im = im.rotate(90)
+ break
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(dst)
+ except Exception:
+ self.logger.warn("Can't thumbnail {0}, using original image as thumbnail".format(src))
+ utils.copy_file(src, dst)
+ else: # Image is small
+ utils.copy_file(src, dst)
+
+ def image_date(self, src):
+ """Try to figure out the date of the image."""
+ if src not in self.dates:
+ try:
+ im = Image.open(src)
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+ if decoded == 'DateTimeOriginal':
+ try:
+ self.dates[src] = datetime.datetime.strptime(
+ value, r'%Y:%m:%d %H:%M:%S')
+ break
+ except ValueError: # Invalid EXIF date.
+ pass
+ if src not in self.dates:
+ self.dates[src] = datetime.datetime.fromtimestamp(
+ os.stat(src).st_mtime)
+ return self.dates[src]
diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin
new file mode 100644
index 0000000..b68ea6f
--- /dev/null
+++ b/nikola/plugins/task/gzip.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = gzip
+Module = gzip
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Create gzipped copies of files
+
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
new file mode 100644
index 0000000..738d52c
--- /dev/null
+++ b/nikola/plugins/task/gzip.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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.
+
+"""Create gzipped copies of files."""
+
+import gzip
+import os
+import shlex
+import subprocess
+
+from nikola.plugin_categories import TaskMultiplier
+
+
+class GzipFiles(TaskMultiplier):
+ """If appropiate, create tasks to create gzipped versions of files."""
+
+ name = "gzip"
+ is_default = True
+
+ def process(self, task, prefix):
+ if not self.site.config['GZIP_FILES']:
+ return []
+ if task.get('name') is None:
+ return []
+ gzip_task = {
+ 'file_dep': [],
+ 'targets': [],
+ 'actions': [],
+ 'basename': '{0}_gzip'.format(prefix),
+ 'name': task.get('name').split(":", 1)[-1] + '.gz',
+ 'clean': True,
+ }
+ targets = task.get('targets', [])
+ flag = False
+ for target in targets:
+ ext = os.path.splitext(target)[1]
+ if (ext.lower() in self.site.config['GZIP_EXTENSIONS'] and
+ target.startswith(self.site.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, self.site.config['GZIP_COMMAND'])))
+ if not flag:
+ return []
+ return [gzip_task]
+
+
+def create_gzipped_copy(in_path, out_path, command=None):
+ if command:
+ subprocess.check_call(shlex.split(command.format(filename=in_path)))
+ else:
+ with gzip.GzipFile(out_path, 'wb+') as outf:
+ with open(in_path, 'rb') as inf:
+ outf.write(inf.read())
diff --git a/nikola/plugins/task_indexes.plugin b/nikola/plugins/task/indexes.plugin
index 1536006..a18942c 100644
--- a/nikola/plugins/task_indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -1,10 +1,10 @@
[Core]
-Name = render_index
-Module = task_indexes
+Name = render_indexes
+Module = indexes
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's index pages.
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task/indexes.py
index aa5e648..0d20422 100644
--- a/nikola/plugins/task_indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,6 +26,7 @@
from __future__ import unicode_literals
import glob
+import itertools
import os
from nikola.plugin_categories import Task
@@ -35,8 +38,13 @@ class Indexes(Task):
name = "render_indexes"
+ def set_site(self, site):
+ site.register_path_handler('index', self.index_path)
+ return super(Indexes, self).set_site(site)
+
def gen_tasks(self):
self.site.scan_posts()
+ yield self.group_task()
kw = {
"translations": self.site.config['TRANSLATIONS'],
@@ -54,8 +62,6 @@ class Indexes(Task):
template_name = "index.tmpl"
posts = [x for x in self.site.timeline if x.use_in_feeds]
- if not posts:
- yield {'basename': 'render_indexes', 'actions': []}
for lang in kw["translations"]:
# Split in smaller lists
lists = []
@@ -63,31 +69,40 @@ class Indexes(Task):
filtered_posts = [x for x in posts if x.is_translation_available(lang)]
else:
filtered_posts = posts
+ lists.append(filtered_posts[:kw["index_display_post_count"]])
+ filtered_posts = filtered_posts[kw["index_display_post_count"]:]
while filtered_posts:
- lists.append(filtered_posts[:kw["index_display_post_count"]])
- filtered_posts = filtered_posts[kw["index_display_post_count"]:]
+ lists.append(filtered_posts[-kw["index_display_post_count"]:])
+ filtered_posts = filtered_posts[:-kw["index_display_post_count"]]
num_pages = len(lists)
for i, post_list in enumerate(lists):
context = {}
indexes_title = kw['indexes_title'] or kw['blog_title']
- if not i:
- context["title"] = indexes_title
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % i
else:
- if kw["indexes_pages"]:
- indexes_pages = kw["indexes_pages"] % i
- else:
- indexes_pages = " (" + \
- kw["messages"][lang]["old posts page %d"] % i + ")"
+ indexes_pages = " (" + \
+ kw["messages"][lang]["old posts page %d"] % i + ")"
+ if i > 0:
context["title"] = indexes_title + indexes_pages
+ else:
+ context["title"] = indexes_title
context["prevlink"] = None
context["nextlink"] = None
context['index_teasers'] = kw['index_teasers']
- if 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-{0}.html".format(i + 1)
+ if i == 0: # index.html page
+ context["prevlink"] = None
+ if num_pages > 1:
+ context["nextlink"] = "index-{0}.html".format(num_pages - 1)
+ else:
+ context["nextlink"] = None
+ else: # index-x.html pages
+ if i > 1:
+ context["nextlink"] = "index-{0}.html".format(i - 1)
+ if i < num_pages - 1:
+ context["prevlink"] = "index-{0}.html".format(i + 1)
+ elif i == num_pages - 1:
+ context["prevlink"] = "index.html"
context["permalink"] = self.site.link("index", i, lang)
output_name = os.path.join(
kw['output_folder'], self.site.path("index", i,
@@ -115,15 +130,15 @@ class Indexes(Task):
}
template_name = "list.tmpl"
for lang in kw["translations"]:
- for wildcard, dest, _, is_post in kw["post_pages"]:
- if is_post:
- continue
+ # Need to group by folder to avoid duplicated tasks (Issue #758)
+ for dirname, wildcards in itertools.groupby((w for w, d, x, i in kw["post_pages"] if not i), os.path.dirname):
context = {}
# vim/pyflakes thinks it's unused
# src_dir = os.path.dirname(wildcard)
- files = glob.glob(wildcard)
- post_list = [self.site.global_data[os.path.splitext(p)[0]] for
- p in files]
+ files = []
+ for wildcard in wildcards:
+ files += glob.glob(wildcard)
+ post_list = [self.site.global_data[p] for p in files]
output_name = os.path.join(kw["output_folder"],
self.site.path("post_path",
wildcard,
@@ -135,7 +150,18 @@ class Indexes(Task):
template_name,
kw['filters'],
context)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = self.name
+ yield task
+
+ def index_path(self, name, lang):
+ if name not in [None, 0]:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['INDEX_PATH'],
+ 'index-{0}.html'.format(name)] if _f]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['INDEX_PATH'],
+ self.site.config['INDEX_FILE']]
+ if _f]
diff --git a/nikola/plugins/task_render_listings.plugin b/nikola/plugins/task/listings.plugin
index 1f897b9..c93184d 100644
--- a/nikola/plugins/task_render_listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_listings
-Module = task_render_listings
+Module = listings
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Render code listings into output
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task/listings.py
index 0cadfd3..ab62e74 100644
--- a/nikola/plugins/task_render_listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -39,12 +41,17 @@ class Listings(Task):
name = "render_listings"
+ def set_site(self, site):
+ site.register_path_handler('listing', self.listing_path)
+ return super(Listings, self).set_site(site)
+
def gen_tasks(self):
"""Render pretty code listings."""
kw = {
"default_lang": self.site.config["DEFAULT_LANG"],
"listings_folder": self.site.config["LISTINGS_FOLDER"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
+ "index_file": self.site.config["INDEX_FILE"],
}
# Things to ignore in listings
@@ -60,7 +67,7 @@ class Listings(Task):
code = highlight(fd.read(), lexer,
HtmlFormatter(cssclass='code',
linenos="table", nowrap=False,
- lineanchors=utils.slugify(f),
+ lineanchors=utils.slugify(in_name),
anchorlinenos=True))
title = os.path.basename(in_name)
else:
@@ -80,14 +87,15 @@ class Listings(Task):
}
self.site.render_template('listing.tmpl', out_name,
context)
- flag = True
+
+ yield self.group_task()
+
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'
+ root, kw['index_file']
)
yield {
'basename': self.name,
@@ -98,7 +106,7 @@ class Listings(Task):
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])],
+ self.site.GLOBAL_CONTEXT)],
'clean': True,
}
for f in files:
@@ -119,11 +127,10 @@ class Listings(Task):
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])],
+ self.site.GLOBAL_CONTEXT)],
'clean': True,
}
- if flag:
- yield {
- 'basename': self.name,
- 'actions': [],
- }
+
+ def listing_path(self, name, lang):
+ return [_f for _f in [self.site.config['LISTINGS_FOLDER'], name +
+ '.html'] if _f]
diff --git a/nikola/plugins/task_localsearch.plugin b/nikola/plugins/task/localsearch.plugin
index 33eb78b..86accb6 100644
--- a/nikola/plugins/task_localsearch.plugin
+++ b/nikola/plugins/task/localsearch.plugin
@@ -1,10 +1,10 @@
[Core]
Name = local_search
-Module = task_localsearch
+Module = localsearch
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create data files for local search via Tipue
diff --git a/nikola/plugins/task_localsearch/MIT-LICENSE.txt b/nikola/plugins/task/localsearch/MIT-LICENSE.txt
index f131068..f131068 100644
--- a/nikola/plugins/task_localsearch/MIT-LICENSE.txt
+++ b/nikola/plugins/task/localsearch/MIT-LICENSE.txt
diff --git a/nikola/plugins/task_localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py
index db8610a..9162604 100644
--- a/nikola/plugins/task_localsearch/__init__.py
+++ b/nikola/plugins/task/localsearch/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,8 +29,10 @@ import codecs
import json
import os
+from doit.tools import result_dep
+
from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed, copy_tree
+from nikola.utils import config_changed, copy_tree, makedirs
# This is what we need to produce:
#var tipuesearch = {"pages": [
@@ -68,7 +72,7 @@ class Tipue(LateTask):
for lang in kw["translations"]:
for post in posts:
# Don't index drafts (Issue #387)
- if post.is_draft:
+ if post.is_draft or post.is_retired or post.publish_later:
continue
text = post.text(lang, strip_html=True)
text = text.replace('^', '')
@@ -80,10 +84,7 @@ class Tipue(LateTask):
data["loc"] = post.permalink(lang)
pages.append(data)
output = json.dumps({"pages": pages}, indent=2)
- try:
- os.makedirs(os.path.dirname(dst_path))
- except:
- pass
+ makedirs(os.path.dirname(dst_path))
with codecs.open(dst_path, "wb+", "utf8") as fd:
fd.write(output)
@@ -92,8 +93,11 @@ class Tipue(LateTask):
"name": dst_path,
"targets": [dst_path],
"actions": [(save_data, [])],
- 'uptodate': [config_changed(kw)]
+ 'uptodate': [config_changed(kw), result_dep('sitemap')]
}
+ # Note: The task should run everytime a new file is added or a
+ # file is changed. We cheat, and depend on the sitemap task,
+ # to run everytime a new file is added.
# Copy all the assets to the right places
asset_folder = os.path.join(os.path.dirname(__file__), "files")
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif
index 9c97738..9c97738 100644
--- a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif
+++ b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif
Binary files differ
diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/search.png b/nikola/plugins/task/localsearch/files/assets/css/img/search.png
new file mode 100755
index 0000000..9ab0f2c
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/css/img/search.png
Binary files differ
diff --git a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css
new file mode 100755
index 0000000..2230193
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css
@@ -0,0 +1,159 @@
+
+/*
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+#tipue_search_input
+{
+ font: 12px/1.7 'open sans', sans-serif;
+ color: #333;
+ padding: 7px;
+ width: 150px;
+ border: 1px solid #e2e2e2;
+ border-radius: 0;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ box-shadow: none;
+ outline: 0;
+ margin: 0;
+}
+#tipue_search_input:focus
+{
+ border: 1px solid #ccc;
+}
+#tipue_search_button
+{
+ width: 70px;
+ height: 36px;
+ border: 0;
+ border-radius: 1px;
+ background: #5193fb url('img/search.png') no-repeat center;
+ outline: none;
+}
+#tipue_search_button:hover
+{
+ background-color: #4589fb;
+}
+
+#tipue_search_content
+{
+ clear: left;
+ max-width: 650px;
+ padding: 25px 0 13px 0;
+ margin: 0;
+}
+#tipue_search_loading
+{
+ padding-top: 60px;
+ background: #fff url('img/loader.gif') no-repeat left;
+}
+
+#tipue_search_warning_head
+{
+ font: 300 16px/1.6 'open sans', sans-serif;
+ color: #333;
+}
+#tipue_search_warning
+{
+ font: 12px/1.6 'open sans', sans-serif;
+ color: #333;
+ margin: 7px 0;
+}
+#tipue_search_warning a
+{
+ color: #3f72d8;
+ text-decoration: none;
+}
+#tipue_search_warning a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+#tipue_search_results_count
+{
+ font: 13px/1.6 'open sans', sans-serif;
+ color: #333;
+}
+.tipue_search_content_title
+{
+ font: 300 23px/1.6 'open sans', sans-serif;
+ margin-top: 31px;
+}
+.tipue_search_content_title a
+{
+ color: #3f72d8;
+ text-decoration: none;
+}
+.tipue_search_content_title a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+.tipue_search_content_text
+{
+ font: 12px/1.7 'open sans', sans-serif;
+ color: #333;
+ padding: 13px 0;
+}
+.tipue_search_content_loc
+{
+ font: 300 13px/1.7 'open sans', sans-serif;
+ overflow: auto;
+}
+.tipue_search_content_loc a
+{
+ color: #555;
+ text-decoration: none;
+}
+.tipue_search_content_loc a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+#tipue_search_foot
+{
+ margin: 51px 0 21px 0;
+}
+#tipue_search_foot_boxes
+{
+ padding: 0;
+ margin: 0;
+ font: 12px/1 'open sans', sans-serif;
+}
+#tipue_search_foot_boxes li
+{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+#tipue_search_foot_boxes li a
+{
+ padding: 7px 13px 8px 13px;
+ background-color: #f1f1f1;
+ border: 1px solid #dcdcdc;
+ border-radius: 1px;
+ color: #333;
+ margin-right: 7px;
+ text-decoration: none;
+ text-align: center;
+}
+#tipue_search_foot_boxes li.current
+{
+ padding: 7px 13px 8px 13px;
+ background: #fff;
+ border: 1px solid #dcdcdc;
+ border-radius: 1px;
+ color: #333;
+ margin-right: 7px;
+ text-align: center;
+}
+#tipue_search_foot_boxes li a:hover
+{
+ border: 1px solid #ccc;
+ background-color: #f3f3f3;
+}
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js
index 5c766ea..a9982cd 100644
--- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js
@@ -1,10 +1,10 @@
/*
-Tipue Search 2.1
+Tipue Search 3.0.1
Copyright (c) 2013 Tipue
Tipue Search is released under the MIT License
http://www.tipue.com/search
-*/
+*/
(function($) {
@@ -12,7 +12,7 @@ http://www.tipue.com/search
$.fn.tipuesearch = function(options) {
var set = $.extend( {
-
+
'show' : 7,
'newWindow' : false,
'showURL' : true,
@@ -24,9 +24,9 @@ http://www.tipue.com/search
'liveDescription' : '*',
'liveContent' : '*',
'contentLocation' : 'tipuesearch/tipuesearch_content.json'
-
+
}, options);
-
+
return this.each(function() {
var tipuesearch_in = {
@@ -47,7 +47,7 @@ http://www.tipue.com/search
cont = cont.replace(/\s+/g, ' ');
var desc = $(set.liveDescription, html).text();
desc = desc.replace(/\s+/g, ' ');
-
+
var t_1 = html.toLowerCase().indexOf('<title>');
var t_2 = html.toLowerCase().indexOf('</title>', t_1 + 7);
if (t_1 != -1 && t_2 != -1)
@@ -63,13 +63,13 @@ http://www.tipue.com/search
"title": tit,
"text": desc,
"tags": cont,
- "loc": tipuesearch_pages[i]
- });
+ "loc": tipuesearch_pages[i]
+ });
}
);
}
}
-
+
if (set.mode == 'json')
{
$.getJSON(set.contentLocation,
@@ -80,15 +80,15 @@ http://www.tipue.com/search
);
}
- if (set.mode == 'static' || set.mode == 'static-images')
+ if (set.mode == 'static')
{
tipuesearch_in = $.extend({}, tipuesearch);
- }
-
+ }
+
var tipue_search_w = '';
if (set.newWindow)
{
- tipue_search_w = ' target="_blank"';
+ tipue_search_w = ' target="_blank"';
}
function getURLP(name)
@@ -99,8 +99,8 @@ http://www.tipue.com/search
{
$('#tipue_search_input').val(getURLP('q'));
getTipueSearch(0, true);
- }
-
+ }
+
$('#tipue_search_button').click(function()
{
getTipueSearch(0, true);
@@ -120,7 +120,7 @@ http://www.tipue.com/search
var results = '';
var show_replace = false;
var show_stop = false;
-
+
var d = $('#tipue_search_input').val().toLowerCase();
d = $.trim(d);
var d_w = d.split(' ');
@@ -133,7 +133,7 @@ http://www.tipue.com/search
if (d_w[i] == tipuesearch_stop_words[f])
{
a_w = false;
- show_stop = true;
+ show_stop = true;
}
}
if (a_w)
@@ -143,7 +143,7 @@ http://www.tipue.com/search
}
d = $.trim(d);
d_w = d.split(' ');
-
+
if (d.length >= set.minimumLength)
{
if (replace)
@@ -161,8 +161,8 @@ http://www.tipue.com/search
}
}
d_w = d.split(' ');
- }
-
+ }
+
var d_t = d;
for (var i = 0; i < d_w.length; i++)
{
@@ -193,10 +193,10 @@ http://www.tipue.com/search
{
score -= (150000 - i);
}
-
+
if (set.highlightTerms)
{
- if (set.highlightEveryTerm)
+ if (set.highlightEveryTerm)
{
var patr = new RegExp('(' + d_w[f] + ')', 'gi');
}
@@ -204,33 +204,26 @@ http://www.tipue.com/search
{
var patr = new RegExp('(' + d_w[f] + ')', 'i');
}
- s_t = s_t.replace(patr, "<em>$1</em>");
+ s_t = s_t.replace(patr, "<b>$1</b>");
}
-
if (tipuesearch_in.pages[i].tags.search(pat) != -1)
{
score -= (100000 - i);
- }
+ }
+
}
if (score < 1000000000)
{
- if (set.mode == 'static-images')
- {
- found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc + '^' + tipuesearch_in.pages[i].image;
- }
- else
- {
- found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc;
- }
+ found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc;
}
- }
-
+ }
+
if (c != 0)
{
if (show_replace == 1)
{
out += '<div id="tipue_search_warning_head">Showing results for ' + d + '</div>';
- out += '<div id="tipue_search_warning">Show results for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
+ out += '<div id="tipue_search_warning">Search for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
}
if (c == 1)
{
@@ -241,7 +234,7 @@ http://www.tipue.com/search
c_c = c.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
out += '<div id="tipue_search_results_count">' + c_c + ' results</div>';
}
-
+
found.sort();
var l_o = 0;
for (var i = 0; i < found.length; i++)
@@ -250,16 +243,7 @@ http://www.tipue.com/search
if (l_o >= start && l_o < set.show + start)
{
out += '<div class="tipue_search_content_title"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[1] + '</a></div>';
-
- if (set.mode == 'static-images')
- {
- if (fo[4])
- {
- out += '<div class="tipue_search_content_image_box"><img class="tipue_search_content_image" id="' + fo[1] + '^' + fo[3] + '" ';
- out += 'src=' + fo[4] + '></div><div style="clear: both;"></div>';
- }
- }
-
+
var t = fo[2];
var t_d = '';
var t_w = t.split(' ');
@@ -271,7 +255,7 @@ http://www.tipue.com/search
{
for (var f = 0; f < set.descriptiveWords; f++)
{
- t_d += t_w[f] + ' ';
+ t_d += t_w[f] + ' ';
}
}
t_d = $.trim(t_d);
@@ -280,33 +264,38 @@ http://www.tipue.com/search
t_d += ' ...';
}
out += '<div class="tipue_search_content_text">' + t_d + '</div>';
-
+
if (set.showURL)
{
- out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[3] + '</a></div>';
+ t_url = fo[3];
+ if (t_url.length > 45)
+ {
+ t_url = fo[3].substr(0, 45) + ' ...';
+ }
+ out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + t_url + '</a></div>';
}
}
- l_o++;
+ l_o++;
}
-
+
if (c > set.show)
{
var pages = Math.ceil(c / set.show);
var page = (start / set.show);
out += '<div id="tipue_search_foot"><ul id="tipue_search_foot_boxes">';
-
+
if (start > 0)
{
- out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">&#171; Previous</a></li>';
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">Prev</a></li>';
}
-
- if (page <= 4)
+
+ if (page <= 2)
{
var p_b = pages;
- if (pages > 5)
+ if (pages > 3)
{
- p_b = 5;
- }
+ p_b = 3;
+ }
for (var f = 0; f < p_b; f++)
{
if (f == page)
@@ -321,10 +310,10 @@ http://www.tipue.com/search
}
else
{
- var p_b = pages + 4;
+ var p_b = page + 3;
if (p_b > pages)
{
- p_b = pages;
+ p_b = pages;
}
for (var f = page; f < p_b; f++)
{
@@ -336,27 +325,27 @@ http://www.tipue.com/search
{
out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
}
- }
+ }
}
-
+
if (page + 1 != pages)
{
- out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next &#187;</a></li>';
- }
-
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next</a></li>';
+ }
+
out += '</ul></div>';
- }
+ }
}
else
{
- out += '<div id="tipue_search_warning_head">Nothing found</div>';
+ out += '<div id="tipue_search_warning_head">Nothing found</div>';
}
}
else
{
if (show_stop)
{
- out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>';
+ out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>';
}
else
{
@@ -371,56 +360,25 @@ http://www.tipue.com/search
}
}
}
-
+
$('#tipue_search_content').html(out);
$('#tipue_search_content').slideDown(200);
-
+
$('#tipue_search_replaced').click(function()
{
getTipueSearch(0, false);
});
-
- $('.tipue_search_content_image').click(function()
- {
- var src_i = $(this).attr('src');
- var id_v = $(this).attr('id');
- var id_a = id_v.split('^');
-
- var l_c_i = '<img src="' + src_i + '"><div id="tipue_lightbox_content_title">' + id_a[0] + '</div>';
- l_c_i += '<a href="' + id_a[1] + '"' + tipue_search_w + '><div id="tipue_lightbox_content_link"></div></a>';
- l_c_i += '<a href="' + src_i + '"' + tipue_search_w + '><div id="tipue_lightbox_content_expand"></div></a><div style="clear: both;"></div>';
-
- if ($('#tipue_lightbox').length > 0)
- {
- $('#tipue_lightbox_content').html(l_c_i);
- $('#tipue_lightbox').fadeIn();
- }
- else
- {
- var tipue_lightbox = '<div id="tipue_lightbox">' + '<div id="tipue_lightbox_content">' + l_c_i + '</div></div>';
- $('body').append(tipue_lightbox);
- $('#tipue_lightbox').fadeIn();
- }
- });
- $('#tipue_lightbox').live('click', function()
- {
- $('#tipue_lightbox').hide();
- });
-
+
$('.tipue_search_foot_box').click(function()
{
var id_v = $(this).attr('id');
var id_a = id_v.split('_');
-
+
getTipueSearch(parseInt(id_a[0]), id_a[1]);
- });
- }
-
+ });
+ }
+
});
};
-
-})(jQuery);
-
-
-
+})(jQuery);
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
index 8989c3c..8493ec1 100644
--- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
@@ -1,7 +1,7 @@
/*
-Tipue Search 2.0
-Copyright (c) 2012 Tipue
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
Tipue Search is released under the MIT License
http://www.tipue.com/search
*/
@@ -19,10 +19,3 @@ var tipuesearch_stem = {"words": [
{"word": "javascript", stem: "script"},
{"word": "javascript", stem: "js"}
]};
-
-/*
-Include the following variable listing the pages on your site if you're using Live mode
-*/
-
-var tipuesearch_pages = ["http://foo.com/", "http://foo.com/about/", "http://foo.com/blog/", "http://foo.com/tos/"];
-
diff --git a/nikola/plugins/task_localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html
index 789fbe5..789fbe5 100755
--- a/nikola/plugins/task_localsearch/files/tipue_search.html
+++ b/nikola/plugins/task/localsearch/files/tipue_search.html
diff --git a/nikola/plugins/task_mustache.plugin b/nikola/plugins/task/mustache.plugin
index 6103936..d6b487a 100644
--- a/nikola/plugins/task_mustache.plugin
+++ b/nikola/plugins/task/mustache.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_mustache
-Module = task_mustache
+Module = mustache
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's index pages in json.
diff --git a/nikola/plugins/task_mustache/__init__.py b/nikola/plugins/task/mustache/__init__.py
index 7364979..c392e3b 100644
--- a/nikola/plugins/task_mustache/__init__.py
+++ b/nikola/plugins/task/mustache/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,7 +31,7 @@ import json
import os
from nikola.plugin_categories import Task
-from nikola.utils import config_changed, copy_file, unicode_str
+from nikola.utils import config_changed, copy_file, unicode_str, makedirs
class Mustache(Task):
@@ -103,25 +105,11 @@ class Mustache(Task):
post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']),
})
- # Disqus comments
- data["disqus_html"] = ('<div id="disqus_thread"></div> <script '
- 'type="text/javascript">var disqus_'
- 'shortname="%s";var disqus_url="%s";'
- '(function(){var a=document.createElement'
- '("script");a.type="text/javascript";'
- 'a.async=true;a.src="http://"+disqus_'
- 'shortname+".disqus.com/embed.js";('
- 'document.getElementsByTagName("head")'
- '[0]||document.getElementsByTagName("body")'
- '[0]).appendChild(a)})(); </script>'
- '<noscript>Please enable JavaScript to view'
- ' the <a href="http://disqus.com/'
- '?ref_noscript">comments powered by DISQUS.'
- '</a></noscript><a href="http://disqus.com"'
- 'class="dsq-brlink">comments powered by <sp'
- 'an class="logo-disqus">DISQUS</span></a>' %
- (self.site.config['DISQUS_FORUM'],
- post.permalink(absolute=True)))
+ # Comments
+ context = dict(post=post, lang=self.site.current_lang())
+ context.update(self.site.GLOBAL_CONTEXT)
+ data["comment_html"] = self.site.template_system.render_template(
+ 'mustache-comment-form.tmpl', None, context).strip()
# Post translations
translations = []
@@ -135,10 +123,7 @@ class Mustache(Task):
".html", ".json")})
data["translations"] = translations
- try:
- os.makedirs(os.path.dirname(path))
- except:
- pass
+ makedirs(os.path.dirname(path))
with codecs.open(path, 'wb+', 'utf8') as fd:
fd.write(json.dumps(data))
diff --git a/nikola/plugins/task_mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html
index 7f2b34c..e9a0213 100644
--- a/nikola/plugins/task_mustache/mustache-template.html
+++ b/nikola/plugins/task/mustache/mustache-template.html
@@ -5,7 +5,7 @@
<hr>
<h2>{{title}}</h2>
Posted on: {{date}}</br>
- {{#tags?}} More posts about:
+ {{#tags?}} More posts about:
{{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}}
</br>
{{/tags?}}
@@ -14,13 +14,13 @@
{{{text}}}
<ul class="pager">
{{#prev}}
- <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li>
+ <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li>
{{/prev}}
{{#next}}
<li class="next"><a href="javascript:load_data('{{next}}')">{{message_Next post}}</a></li>
{{/next}}
</ul>
- {{{disqus_html}}}
+ {{{comment_html}}}
</div>
<div class="footerbox">
{{{CONTENT_FOOTER}}}
diff --git a/nikola/plugins/task_mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html
index 5dbebef..7ff6312 100644
--- a/nikola/plugins/task_mustache/mustache.html
+++ b/nikola/plugins/task/mustache/mustache.html
@@ -4,14 +4,12 @@
<link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
<link href="/assets/css/code.css" rel="stylesheet" type="text/css">
<link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
- <link href="/assets/css/slides.css" rel="stylesheet" type="text/css"/>
<link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
<link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script>
- <script src="https://raw.github.com/jonnyreeves/jquery-Mustache/master/src/jquery.mustache.js"></script>
- <script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script>
+ <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script>
+ <script src="//cdn.jsdelivr.net/jquery.mustache/0.2.7/jquery.mustache.js"></script>
+ <script src="//cdn.jsdelivr.net/mustache.js/0.7.2/mustache.js"></script>
<script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
- <script src="/assets/js/slides.min.jquery.js" type="text/javascript"></script>
<script type="text/javascript">
function load_data(dataurl) {
jQuery.getJSON(dataurl, function(data) {
@@ -29,7 +27,7 @@ $.Mustache.load('/mustache-template.html')
load_data('{{first_post_data}}');
};
})
-});
+});
</script>
</head>
<body style="padding-top: 0;">
diff --git a/nikola/plugins/task_render_pages.plugin b/nikola/plugins/task/pages.plugin
index e2a358c..67212d2 100644
--- a/nikola/plugins/task_render_pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_pages
-Module = task_render_pages
+Module = pages
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create pages in the output.
diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task/pages.py
index 1883d7b..eb5b49e 100644
--- a/nikola/plugins/task_render_pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -41,7 +43,7 @@ class RenderPages(Task):
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
- flag = False
+ yield self.group_task()
for lang in kw["translations"]:
for post in self.site.timeline:
if kw["hide_untranslated_posts"] and not post.is_translation_available(lang):
@@ -53,12 +55,4 @@ class RenderPages(Task):
2: kw})]
task['basename'] = self.name
task['task_dep'] = ['render_posts']
- flag = True
yield task
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
diff --git a/nikola/plugins/task_render_posts.plugin b/nikola/plugins/task/posts.plugin
index 0d19ea9..e1a42fd 100644
--- a/nikola/plugins/task_render_posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_posts
-Module = task_render_posts
+Module = posts
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create HTML fragments out of posts.
diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py
new file mode 100644
index 0000000..18d61b8
--- /dev/null
+++ b/nikola/plugins/task/posts.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 copy import copy
+import nikola.post
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class RenderPosts(Task):
+ """Build HTML fragments from metadata and text."""
+
+ name = "render_posts"
+
+ def gen_tasks(self):
+ """Build HTML fragments from metadata and text."""
+ self.site.scan_posts()
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "timeline": self.site.timeline,
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ }
+
+ nikola.post.READ_MORE_LINK = self.site.config['READ_MORE_LINK']
+ yield self.group_task()
+
+ for lang in kw["translations"]:
+ deps_dict = copy(kw)
+ deps_dict.pop('timeline')
+ for post in kw['timeline']:
+ dest = post.translated_base_path(lang)
+ task = {
+ 'basename': self.name,
+ 'name': dest,
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [dest],
+ 'actions': [(post.compile, (lang, ))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(deps_dict)],
+ }
+ yield task
diff --git a/nikola/plugins/task_redirect.plugin b/nikola/plugins/task/redirect.plugin
index 285720b..826f3d8 100644
--- a/nikola/plugins/task_redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -1,10 +1,10 @@
[Core]
Name = redirect
-Module = task_redirect
+Module = redirect
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create redirect pages.
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task/redirect.py
index 2503bb7..ade878a 100644
--- a/nikola/plugins/task_redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,7 +32,7 @@ from nikola import utils
class Redirect(Task):
- """Copy theme assets into output."""
+ """Generate redirections"""
name = "redirect"
@@ -42,17 +44,8 @@ class Redirect(Task):
'output_folder': self.site.config['OUTPUT_FOLDER'],
}
- if not kw['redirections']:
- # If there are no redirections, still needs to create a
- # dummy action so dependencies don't fail
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
- else:
+ yield self.group_task()
+ if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src)
yield {
@@ -66,10 +59,7 @@ class Redirect(Task):
def create_redirect(src, dst):
- try:
- os.makedirs(os.path.dirname(src))
- except:
- pass
+ utils.makedirs(os.path.dirname(src))
with codecs.open(src, "wb+", "utf8") as fd:
fd.write('<!DOCTYPE html><head><title>Redirecting...</title>'
'<meta http-equiv="refresh" content="0; '
diff --git a/nikola/plugins/task_render_rss.plugin b/nikola/plugins/task/rss.plugin
index 20caf15..7206a43 100644
--- a/nikola/plugins/task_render_rss.plugin
+++ b/nikola/plugins/task/rss.plugin
@@ -1,10 +1,10 @@
[Core]
-Name = render_rss
-Module = task_render_rss
+Name = generate_rss
+Module = rss
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generate RSS feeds.
diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task/rss.py
index 3000e47..bcca4da 100644
--- a/nikola/plugins/task_render_rss.py
+++ b/nikola/plugins/task/rss.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,16 +24,25 @@
# 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, print_function
import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
from nikola import utils
from nikola.plugin_categories import Task
-class RenderRSS(Task):
+class GenerateRSS(Task):
"""Generate RSS feeds."""
- name = "render_rss"
+ name = "generate_rss"
+
+ def set_site(self, site):
+ site.register_path_handler('rss', self.rss_path)
+ return super(GenerateRSS, self).set_site(site)
def gen_tasks(self):
"""Generate RSS feeds."""
@@ -44,8 +55,10 @@ class RenderRSS(Task):
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "feed_length": self.site.config['FEED_LENGTH'],
}
self.site.scan_posts()
+ yield self.group_task()
for lang in kw["translations"]:
output_name = os.path.join(kw['output_folder'],
self.site.path("rss", None, lang))
@@ -57,16 +70,22 @@ class RenderRSS(Task):
posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
for post in posts:
deps += post.deps(lang)
+
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/'))
yield {
- 'basename': 'render_rss',
+ 'basename': 'generate_rss',
'name': os.path.normpath(output_name),
'file_dep': deps,
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
(lang, kw["blog_title"], kw["site_url"],
kw["blog_description"], posts, output_name,
- kw["rss_teasers"]))],
+ kw["rss_teasers"], kw['feed_length'], feed_url))],
'task_dep': ['render_posts'],
'clean': True,
'uptodate': [utils.config_changed(kw)],
}
+
+ def rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['RSS_PATH'], 'rss.xml'] if _f]
diff --git a/nikola/plugins/task_sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index f6b01d7..2cd8195 100644
--- a/nikola/plugins/task_sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -1,10 +1,10 @@
[Core]
Name = sitemap
-Module = task_sitemap
+Module = sitemap
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generate google sitemap.
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py
new file mode 100644
index 0000000..f34bc0a
--- /dev/null
+++ b/nikola/plugins/task/sitemap/__init__.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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, absolute_import, unicode_literals
+import codecs
+import datetime
+import os
+try:
+ from urlparse import urljoin, urlparse
+except ImportError:
+ from urllib.parse import urljoin, urlparse # NOQA
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed
+
+
+header = """<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
+"""
+
+url_format = """ <url>
+ <loc>{0}</loc>
+ <lastmod>{1}</lastmod>
+ </url>
+"""
+
+get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
+
+
+def get_base_path(base):
+ """returns the path of a base URL if it contains one.
+
+ >>> get_base_path('http://some.site') == '/'
+ True
+ >>> get_base_path('http://some.site/') == '/'
+ True
+ >>> get_base_path('http://some.site/some/sub-path') == '/some/sub-path/'
+ True
+ >>> get_base_path('http://some.site/some/sub-path/') == '/some/sub-path/'
+ True
+ """
+ # first parse the base_url for some path
+ base_parsed = urlparse(base)
+
+ if not base_parsed.path:
+ sub_path = ''
+ else:
+ sub_path = base_parsed.path
+ if sub_path.endswith('/'):
+ return sub_path
+ else:
+ return sub_path + '/'
+
+
+class Sitemap(LateTask):
+ """Generate google sitemap."""
+
+ name = "sitemap"
+
+ def gen_tasks(self):
+ """Generate Google sitemap."""
+ kw = {
+ "base_url": self.site.config["BASE_URL"],
+ "site_url": self.site.config["SITE_URL"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ "strip_indexes": self.site.config["STRIP_INDEXES"],
+ "index_file": self.site.config["INDEX_FILE"],
+ "sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"],
+ "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm', '.xml'])
+ }
+ output_path = kw['output_folder']
+ sitemap_path = os.path.join(output_path, "sitemap.xml")
+ base_path = get_base_path(kw['base_url'])
+ locs = {}
+
+ output = kw['output_folder']
+ base_url = kw['base_url']
+ mapped_exts = kw['mapped_extensions']
+
+ def scan_locs():
+ for root, dirs, files in os.walk(output):
+ if not dirs and not files and not kw['sitemap_include_fileless_dirs']:
+ continue # Totally empty, not on sitemap
+ path = os.path.relpath(root, output)
+ # ignore the current directory.
+ path = (path.replace(os.sep, '/') + '/').replace('./', '')
+ lastmod = get_lastmod(root)
+ loc = urljoin(base_url, base_path + path)
+ if kw['index_file'] in files and kw['strip_indexes']: # ignore folders when not stripping urls
+ locs[loc] = url_format.format(loc, lastmod)
+ for fname in files:
+ if kw['strip_indexes'] and fname == kw['index_file']:
+ continue # We already mapped the folder
+ if os.path.splitext(fname)[-1] in mapped_exts:
+ real_path = os.path.join(root, fname)
+ path = os.path.relpath(real_path, output)
+ if path.endswith(kw['index_file']) and kw['strip_indexes']:
+ # ignore index files when stripping urls
+ continue
+ if path.endswith('.html') or path.endswith('.htm'):
+ if not u'<!doctype html' in codecs.open(real_path, 'r', 'utf8').read(1024).lower():
+ # ignores "html" files without doctype
+ # alexa-verify, google-site-verification, etc.
+ continue
+ if path.endswith('.xml'):
+ if not u'<rss' in codecs.open(real_path, 'r', 'utf8').read(512):
+ # ignores all XML files except those presumed to be RSS
+ continue
+ post = self.site.post_per_file.get(path)
+ if post and (post.is_draft or post.is_retired or post.publish_later):
+ continue
+ path = path.replace(os.sep, '/')
+ lastmod = get_lastmod(real_path)
+ loc = urljoin(base_url, base_path + path)
+ locs[loc] = url_format.format(loc, lastmod)
+
+ def write_sitemap():
+ # Have to rescan, because files may have been added between
+ # task dep scanning and task execution
+ scan_locs()
+ with codecs.open(sitemap_path, 'wb+', 'utf8') as outf:
+ outf.write(header)
+ for k in sorted(locs.keys()):
+ outf.write(locs[k])
+ outf.write("</urlset>")
+ # Other tasks can depend on this output, instead of having
+ # to scan locations.
+ return {'locations': list(locs.keys())}
+
+ scan_locs()
+ yield self.group_task()
+ task = {
+ "basename": "sitemap",
+ "name": sitemap_path,
+ "targets": [sitemap_path],
+ "actions": [(write_sitemap,)],
+ "uptodate": [config_changed({1: kw, 2: locs})],
+ "clean": True,
+ "task_dep": ["render_site"],
+ }
+ yield task
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/nikola/plugins/task_render_sources.plugin b/nikola/plugins/task/sources.plugin
index 5b59598..6224e48 100644
--- a/nikola/plugins/task_render_sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_sources
-Module = task_render_sources
+Module = sources
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy page sources into the output.
diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task/sources.py
index 392345c..672f354 100644
--- a/nikola/plugins/task_render_sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -50,35 +52,30 @@ class Sources(Task):
}
self.site.scan_posts()
- flag = False
- for lang in kw["translations"]:
- for post in self.site.timeline:
- if post.meta('password'):
- continue
- output_name = os.path.join(
- kw['output_folder'], post.destination_path(
- lang, post.source_ext()))
- source = post.source_path
- if source.endswith('.html'):
- continue
- if lang != kw["default_lang"]:
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
- if os.path.isfile(source):
- yield {
- 'basename': 'render_sources',
- 'name': os.path.normpath(output_name),
- 'file_dep': [source],
- 'targets': [output_name],
- 'actions': [(utils.copy_file, (source, output_name))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': 'render_sources',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
+ yield self.group_task()
+ if self.site.config['COPY_SOURCES']:
+ for lang in kw["translations"]:
+ for post in self.site.timeline:
+ if post.meta('password'):
+ continue
+ output_name = os.path.join(
+ kw['output_folder'], post.destination_path(
+ lang, post.source_ext()))
+ source = post.source_path
+ dest_ext = self.site.get_compiler(post.source_path).extension()
+ if dest_ext == post.source_ext():
+ continue
+ if lang != kw["default_lang"]:
+ source_lang = source + '.' + lang
+ if os.path.exists(source_lang):
+ source = source_lang
+ if os.path.isfile(source):
+ yield {
+ 'basename': 'render_sources',
+ 'name': os.path.normpath(output_name),
+ 'file_dep': [source],
+ 'targets': [output_name],
+ 'actions': [(utils.copy_file, (source, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
diff --git a/nikola/plugins/task_render_tags.plugin b/nikola/plugins/task/tags.plugin
index b826e87..f01e0f8 100644
--- a/nikola/plugins/task_render_tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_tags
-Module = task_render_tags
+Module = tags
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Render the tag pages and feeds.
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task/tags.py
index 58a7ff3..299dca4 100644
--- a/nikola/plugins/task_render_tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,16 +28,28 @@ from __future__ import unicode_literals
import codecs
import json
import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
from nikola.plugin_categories import Task
from nikola import utils
class RenderTags(Task):
- """Render the tag pages and feeds."""
+ """Render the tag/category pages and feeds."""
name = "render_tags"
+ def set_site(self, site):
+ site.register_path_handler('tag_index', self.tag_index_path)
+ site.register_path_handler('tag', self.tag_path)
+ site.register_path_handler('tag_rss', self.tag_rss_path)
+ site.register_path_handler('category', self.category_path)
+ site.register_path_handler('category_rss', self.category_rss_path)
+ return super(RenderTags, self).set_site(site)
+
def gen_tasks(self):
"""Render the tag pages and feeds."""
@@ -43,7 +57,6 @@ class RenderTags(Task):
"translations": self.site.config["TRANSLATIONS"],
"blog_title": self.site.config["BLOG_TITLE"],
"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'],
"filters": self.site.config['FILTERS'],
@@ -53,17 +66,21 @@ class RenderTags(Task):
"index_teasers": self.site.config['INDEX_TEASERS'],
"rss_teasers": self.site.config["RSS_TEASERS"],
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "feed_length": self.site.config['FEED_LENGTH'],
}
self.site.scan_posts()
+ yield self.group_task()
yield self.list_tags_page(kw)
- if not self.site.posts_per_tag:
- yield {'basename': str(self.name), 'actions': []}
+ if not self.site.posts_per_tag and not self.site.posts_per_category:
return
- for tag, posts in list(self.site.posts_per_tag.items()):
+ tag_list = list(self.site.posts_per_tag.items())
+ cat_list = list(self.site.posts_per_category.items())
+
+ def render_lists(tag, posts, is_category=True):
post_list = [self.site.global_data[post] for post in posts]
post_list.sort(key=lambda a: a.date)
post_list.reverse()
@@ -72,13 +89,23 @@ class RenderTags(Task):
filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
else:
filtered_posts = post_list
- rss_post_list = [p.post_name for p in filtered_posts]
- yield self.tag_rss(tag, lang, rss_post_list, kw)
+ rss_post_list = [p.source_path for p in filtered_posts]
+ yield self.tag_rss(tag, lang, rss_post_list, kw, is_category)
# Render HTML
if kw['tag_pages_are_indexes']:
- yield self.tag_page_as_index(tag, lang, filtered_posts, kw)
+ yield self.tag_page_as_index(tag, lang, filtered_posts, kw, is_category)
else:
- yield self.tag_page_as_list(tag, lang, filtered_posts, kw)
+ yield self.tag_page_as_list(tag, lang, filtered_posts, kw, is_category)
+
+ for tag, posts in tag_list:
+ for task in render_lists(tag, posts, False):
+ yield task
+
+ for tag, posts in cat_list:
+ if tag == '': # This is uncategorized posts
+ continue
+ for task in render_lists(tag, posts, True):
+ yield task
# Tag cloud json file
tag_cloud_data = {}
@@ -89,10 +116,7 @@ class RenderTags(Task):
'assets', 'js', 'tag_cloud_data.json')
def write_tag_data(data):
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ utils.makedirs(os.path.dirname(output_name))
with codecs.open(output_name, 'wb+', 'utf8') as fd:
fd.write(json.dumps(data))
@@ -107,21 +131,37 @@ class RenderTags(Task):
yield task
def list_tags_page(self, kw):
- """a global "all your tags" page for each language"""
+ """a global "all your tags/categories" page for each language"""
tags = list(self.site.posts_per_tag.keys())
+ categories = list(self.site.posts_per_category.keys())
# We want our tags to be sorted case insensitive
tags.sort(key=lambda a: a.lower())
+ categories.sort(key=lambda a: a.lower())
+ if categories != ['']:
+ has_categories = True
+ else:
+ has_categories = False
template_name = "tags.tmpl"
kw['tags'] = tags
+ kw['categories'] = categories
for lang in kw["translations"]:
output_name = os.path.join(
kw['output_folder'], self.site.path('tag_index', None, lang))
output_name = output_name
context = {}
- context["title"] = kw["messages"][lang]["Tags"]
+ if has_categories:
+ context["title"] = kw["messages"][lang]["Tags and Categories"]
+ else:
+ context["title"] = kw["messages"][lang]["Tags"]
context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
in tags]
+ if has_categories:
+ context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag
+ in categories]
+ else:
+ context["cat_items"] = None
context["permalink"] = self.site.link("tag_index", None, lang)
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
[],
@@ -132,15 +172,18 @@ class RenderTags(Task):
)
task_cfg = {1: task['uptodate'][0].config, 2: kw}
task['uptodate'] = [utils.config_changed(task_cfg)]
+ task['basename'] = str(self.name)
yield task
- def tag_page_as_index(self, tag, lang, post_list, kw):
+ def tag_page_as_index(self, tag, lang, post_list, kw, is_category):
"""render a sort of index page collection using only this
tag's posts."""
+ kind = "category" if is_category else "tag"
+
def page_name(tagname, i, lang):
"""Given tag, n, returns a page name."""
- name = self.site.path("tag", tag, lang)
+ name = self.site.path(kind, tag, lang)
if i:
name = name.replace('.html', '-{0}.html'.format(i))
return name
@@ -159,7 +202,7 @@ class RenderTags(Task):
rss_link = ("""<link rel="alternate" type="application/rss+xml" """
"""type="application/rss+xml" title="RSS for tag """
"""{0} ({1})" href="{2}">""".format(
- tag, lang, self.site.link("tag_rss", tag, lang)))
+ tag, lang, self.site.link(kind + "_rss", tag, lang)))
context['rss_link'] = rss_link
output_name = os.path.join(kw['output_folder'],
page_name(tag, i, lang))
@@ -177,8 +220,9 @@ class RenderTags(Task):
if i < num_pages - 1:
context["nextlink"] = os.path.basename(
page_name(tag, i + 1, lang))
- context["permalink"] = self.site.link("tag", tag, lang)
+ context["permalink"] = self.site.link(kind, tag, lang)
context["tag"] = tag
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -190,19 +234,23 @@ class RenderTags(Task):
task_cfg = {1: task['uptodate'][0].config, 2: kw}
task['uptodate'] = [utils.config_changed(task_cfg)]
task['basename'] = str(self.name)
+
yield task
- def tag_page_as_list(self, tag, lang, post_list, kw):
+ def tag_page_as_list(self, tag, lang, post_list, kw, is_category):
"""We render a single flat link list with this tag's posts"""
+ kind = "category" if is_category else "tag"
template_name = "tag.tmpl"
output_name = os.path.join(kw['output_folder'], self.site.path(
- "tag", tag, lang))
+ kind, tag, lang))
context = {}
context["lang"] = lang
context["title"] = kw["messages"][lang]["Posts about %s"] % tag
context["posts"] = post_list
- context["permalink"] = self.site.link("tag", tag, lang)
+ context["permalink"] = self.site.link(kind, tag, lang)
context["tag"] = tag
+ context["kind"] = kind
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -216,11 +264,14 @@ class RenderTags(Task):
task['basename'] = str(self.name)
yield task
- def tag_rss(self, tag, lang, posts, kw):
+ def tag_rss(self, tag, lang, posts, kw, is_category):
"""RSS for a single tag / language"""
+ kind = "category" if is_category else "tag"
#Render RSS
- output_name = os.path.join(kw['output_folder'],
- self.site.path("tag_rss", tag, lang))
+ output_name = os.path.normpath(
+ os.path.join(kw['output_folder'],
+ self.site.path(kind + "_rss", tag, lang)))
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/'))
deps = []
post_list = [self.site.global_data[post] for post in posts if
self.site.global_data[post].use_in_feeds]
@@ -235,9 +286,39 @@ class RenderTags(Task):
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
(lang, "{0} ({1})".format(kw["blog_title"], tag),
- kw["site_url"], kw["blog_description"], post_list,
- output_name, kw["rss_teasers"]))],
+ kw["site_url"], None, post_list,
+ output_name, kw["rss_teasers"], kw['feed_length'], feed_url))],
'clean': True,
'uptodate': [utils.config_changed(kw)],
'task_dep': ['render_posts'],
}
+
+ def slugify_name(self, name):
+ if self.site.config['SLUG_TAG_PATH']:
+ name = utils.slugify(name)
+ return name
+
+ def tag_index_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'],
+ self.site.config['INDEX_FILE']] if _f]
+
+ def tag_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_name(name) + ".html"] if
+ _f]
+
+ def tag_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_name(name) + ".xml"] if
+ _f]
+
+ def category_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".html"] if
+ _f]
+
+ def category_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".xml"] if
+ _f]
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
deleted file mode 100755
index 21bb7b0..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/link.png b/nikola/plugins/task_localsearch/files/assets/css/img/link.png
deleted file mode 100755
index d4e51c5..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/link.png
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
deleted file mode 100644
index 644bd17..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
deleted file mode 100755
index 96dadf0..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
+++ /dev/null
@@ -1,232 +0,0 @@
-
-/*
-Tipue Search 2.1
-Copyright (c) 2013 Tipue
-Tipue Search is released under the MIT License
-http://www.tipue.com/search
-*/
-
-
-em
-{
- font: inherit;
- font-weight: 400;
-}
-#tipue_search_input
-{
-}
-#tipue_search_input:focus
-{
- border-color: #c3c3c3;
- box-shadow: 0 0 3px rgba(0,0,0,.2);
-}
-#tipue_search_button
-{
- width: 60px;
- height: 33px;
- margin-top: 1px;
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- background: #f1f1f1 url('img/search.gif') no-repeat center;
- outline: none;
-}
-#tipue_search_button:hover
-{
- border: 1px solid #c3c3c3;
- -moz-box-shadow: 1px 1px 2px #e3e3e3;
- -webkit-box-shadow: 1px 1px 2px #e3e3e3;
- box-shadow: 1px 1px 2px #e3e3e3;
-}
-
-#tipue_search_content
-{
- clear: left;
- max-width: 650px;
- padding: 25px 0 13px 0;
- margin: 0;
-}
-#tipue_search_loading
-{
- padding-top: 60px;
- background: #fff url('img/loader.gif') no-repeat left;
-}
-
-#tipue_search_warning_head
-{
- font: 14px/1.5 'open sans', sans-serif;
- color: #333;
-}
-#tipue_search_warning
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
- margin: 13px 0;
-}
-#tipue_search_warning a
-{
- color: #36c;
- text-decoration: none;
-}
-#tipue_search_warning a:hover
-{
- color: #111;
-}
-
-#tipue_search_results_count
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
-}
-
-.tipue_search_content_title
-{
- font: 300 19px/1.5 'open sans', sans-serif;
- margin-top: 31px;
-}
-.tipue_search_content_title a
-{
- color: #36c;
- text-decoration: none;
-}
-.tipue_search_content_title a:hover
-{
- color: #333;
-}
-
-.tipue_search_content_image_box
-{
- float: left;
- border: 1px solid #f3f3f3;
- padding: 13px;
- margin: 21px 0 7px 0;
-}
-.tipue_search_content_image
-{
- max-width: 110px;
- height: auto;
- outline: none;
- cursor: pointer;
-}
-#tipue_lightbox
-{
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, .9);
-}
-#tipue_lightbox_content
-{
- margin: 37px auto;
- background-color: #fff;
- padding: 30px;
- border: 1px solid #ccc;
- width: 250px;
- text-align: center;
- box-shadow: 0 1px 2px #eee;
-}
-#tipue_lightbox img
-{
- max-width: 200px;
- height: auto;
-}
-#tipue_lightbox_content_title
-{
- font: 300 14px/1.7 lato, sans-serif;
- color: #111;
- padding: 17px 25px 0 25px;
- width: 200px;
-}
-#tipue_lightbox_content_link
-{
- float: left;
- width: 30px;
- height: 30px;
- margin-top: 17px;
- background: #fff url('img/link.png') no-repeat center;
-}
-#tipue_lightbox_content_expand
-{
- float: left;
- width: 30px;
- height: 30px;
- margin: 17px 0 0 3px;
- background: #fff url('img/expand.png') no-repeat center;
-}
-#tipue_lightbox_content_link:hover, #tipue_lightbox_content_expand:hover
-{
- background-color: #f3f3f3;
-}
-
-.tipue_search_content_text
-{
- font: 300 14px/1.7 lato, sans-serif;
- color: #111;
- padding: 13px 0;
-}
-.tipue_search_content_loc
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
-}
-.tipue_search_content_loc a
-{
- color: #555;
- text-decoration: none;
-}
-.tipue_search_content_loc a:hover
-{
- padding-bottom: 1px;
- border-bottom: 1px solid #ccc;
-}
-
-#tipue_search_foot
-{
- margin: 47px 0 31px 0;
-}
-#tipue_search_foot_boxes
-{
- padding: 0;
- margin: 0;
- font: 12px/1 'open sans', sans-serif;
-}
-#tipue_search_foot_boxes li
-{
- list-style: none;
- margin: 0;
- padding: 0;
- display: inline;
-}
-#tipue_search_foot_boxes li a
-{
- padding: 7px 10px 8px 10px;
- background-color: #f5f5f5;
- background: -webkit-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -moz-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -ms-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -o-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: linear-gradient(top, #f7f7f7, #f1f1f1);
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- color: #333;
- margin-right: 7px;
- text-decoration: none;
- text-align: center;
-}
-#tipue_search_foot_boxes li.current
-{
- padding: 7px 10px 8px 10px;
- background: #fff;
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- color: #333;
- margin-right: 7px;
- text-align: center;
-}
-#tipue_search_foot_boxes li a:hover
-{
- border: 1px solid #c3c3c3;
- box-shadow: 1px 1px 2px #e3e3e3;
-}
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
deleted file mode 100644
index d4e4a3a..0000000
--- a/nikola/plugins/task_render_galleries.py
+++ /dev/null
@@ -1,338 +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 codecs
-import datetime
-import glob
-import hashlib
-import os
-
-Image = None
-try:
- import Image as _Image
- import ExifTags
- Image = _Image
-except ImportError:
- try:
- from PIL import Image, ExifTags # NOQA
- except ImportError:
- pass
-
-
-from nikola.plugin_categories import Task
-from nikola import utils
-
-
-class Galleries(Task):
- """Copy theme assets into output."""
-
- name = str("render_galleries")
- dates = {}
-
- def gen_tasks(self):
- """Render image galleries."""
-
- kw = {
- 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
- 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
- 'output_folder': self.site.config['OUTPUT_FOLDER'],
- 'cache_folder': self.site.config['CACHE_FOLDER'],
- 'default_lang': self.site.config['DEFAULT_LANG'],
- 'blog_description': self.site.config['BLOG_DESCRIPTION'],
- 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
- 'gallery_path': self.site.config['GALLERY_PATH']
- }
-
- # FIXME: lots of work is done even when images don't change,
- # which should be moved into the task.
-
- template_name = "gallery.tmpl"
-
- gallery_list = []
- for root, dirs, files in os.walk(kw['gallery_path']):
- gallery_list.append(root)
- if not gallery_list:
- yield {
- 'basename': str('render_galleries'),
- 'actions': [],
- }
- return
-
- # gallery_path is "gallery/name"
- for gallery_path in gallery_list:
- # gallery_name is "name"
- splitted = gallery_path.split(os.sep)[1:]
- if not splitted:
- gallery_name = ''
- else:
- gallery_name = os.path.join(*splitted)
- # output_gallery is "output/GALLERY_PATH/name"
- output_gallery = os.path.dirname(os.path.join(
- kw["output_folder"], self.site.path("gallery", gallery_name,
- None)))
- output_name = os.path.join(output_gallery, "index.html")
- if not os.path.isdir(output_gallery):
- yield {
- 'basename': str('render_galleries'),
- 'name': output_gallery,
- 'actions': [(os.makedirs, (output_gallery,))],
- 'targets': [output_gallery],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- # image_list contains "gallery/name/image_name.jpg"
- image_list = glob.glob(gallery_path + "/*jpg") +\
- glob.glob(gallery_path + "/*JPG") +\
- glob.glob(gallery_path + "/*PNG") +\
- glob.glob(gallery_path + "/*png")
-
- # Filter ignore images
- try:
- def add_gallery_path(index):
- return "{0}/{1}".format(gallery_path, index)
-
- exclude_path = os.path.join(gallery_path, "exclude.meta")
- try:
- f = open(exclude_path, 'r')
- excluded_image_name_list = f.read().split()
- except IOError:
- excluded_image_name_list = []
-
- excluded_image_list = list(map(add_gallery_path,
- excluded_image_name_list))
- image_set = set(image_list) - set(excluded_image_list)
- image_list = list(image_set)
- except IOError:
- pass
-
- # List of sub-galleries
- folder_list = [x.split(os.sep)[-2] for x in
- glob.glob(os.path.join(gallery_path, '*') + os.sep)]
-
- crumbs = utils.get_crumbs(gallery_path)
-
- image_list = [x for x in image_list if "thumbnail" not in x]
- # 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)):
- # img is "galleries/name/image_name.jpg"
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- # thumb_path is
- # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
- thumb_path = os.path.join(output_gallery,
- ".thumbnail".join([fname, ext]))
- # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
- orig_dest_path = os.path.join(output_gallery, img_name)
- thumbs.append(os.path.basename(thumb_path))
- yield {
- 'basename': str('render_galleries'),
- 'name': thumb_path,
- 'file_dep': [img],
- 'targets': [thumb_path],
- 'actions': [
- (self.resize_image,
- (img, thumb_path, kw['thumbnail_size']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- yield {
- 'basename': str('render_galleries'),
- 'name': orig_dest_path,
- 'file_dep': [img],
- 'targets': [orig_dest_path],
- 'actions': [
- (self.resize_image,
- (img, orig_dest_path, kw['max_image_size']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- # Remove excluded images
- if excluded_image_name_list:
- for img, img_name in zip(excluded_image_list,
- excluded_image_name_list):
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- excluded_thumb_dest_path = os.path.join(
- output_gallery, ".thumbnail".join([fname, ext]))
- excluded_dest_path = os.path.join(output_gallery, img_name)
- yield {
- 'basename': str('render_galleries_clean'),
- 'name': excluded_thumb_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_thumb_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_thumb_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- yield {
- 'basename': str('render_galleries_clean'),
- 'name': excluded_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- context = {}
- context["lang"] = kw["default_lang"]
- context["title"] = os.path.basename(gallery_path)
- context["description"] = kw["blog_description"]
- if kw['use_filename_as_title']:
- img_titles = ['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))
- context["folders"] = folder_list
- context["crumbs"] = crumbs
- context["permalink"] = self.site.link(
- "gallery", gallery_name, None)
- context["enable_comments"] = (
- self.site.config["COMMENTS_IN_GALLERIES"])
-
- # Use galleries/name/index.txt to generate a blurb for
- # the gallery, if it exists
- index_path = os.path.join(gallery_path, "index.txt")
- cache_dir = os.path.join(kw["cache_folder"], 'galleries')
- if not os.path.isdir(cache_dir):
- os.makedirs(cache_dir)
- index_dst_path = os.path.join(
- cache_dir,
- str(hashlib.sha224(index_path.encode('utf-8')).hexdigest() +
- '.html'))
- if os.path.exists(index_path):
- compile_html = self.site.get_compiler(index_path)
- yield {
- 'basename': str('render_galleries'),
- 'name': index_dst_path,
- 'file_dep': [index_path],
- 'targets': [index_dst_path],
- 'actions': [(compile_html, [index_path, index_dst_path])],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- file_dep = self.site.template_system.template_deps(
- template_name) + image_list
-
- def render_gallery(output_name, context, index_dst_path):
- if os.path.exists(index_dst_path):
- with codecs.open(index_dst_path, "rb", "utf8") as fd:
- context['text'] = fd.read()
- file_dep.append(index_dst_path)
- else:
- context['text'] = ''
- self.site.render_template(template_name, output_name, context)
-
- yield {
- 'basename': str('render_galleries'),
- 'name': output_name,
- 'file_dep': file_dep,
- 'targets': [output_name],
- 'actions': [(render_gallery, (output_name, context,
- index_dst_path))],
- 'clean': True,
- 'uptodate': [utils.config_changed({
- 1: kw,
- 2: self.site.config['GLOBAL_CONTEXT'],
- 3: self.site.config["COMMENTS_IN_GALLERIES"],
- })],
- }
-
- def resize_image(self, src, dst, max_size):
- """Make a copy of the image in the requested size."""
- if not Image:
- utils.copy_file(src, dst)
- return
- im = Image.open(src)
- w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
-
- if decoded == 'Orientation':
- if value == 3:
- im = im.rotate(180)
- elif value == 6:
- im = im.rotate(270)
- elif value == 8:
- im = im.rotate(90)
-
- break
-
- try:
- im.thumbnail(size, Image.ANTIALIAS)
- except Exception:
- # TODO: inform the user, but do not fail
- pass
- else:
- im.save(dst)
-
- else:
- utils.copy_file(src, dst)
-
- def image_date(self, src):
- """Try to figure out the date of the image."""
- if src not in self.dates:
- try:
- im = Image.open(src)
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
- if decoded == 'DateTimeOriginal':
- try:
- self.dates[src] = datetime.datetime.strptime(
- value, r'%Y:%m:%d %H:%M:%S')
- break
- except ValueError: # Invalid EXIF date.
- pass
- if src not in self.dates:
- self.dates[src] = datetime.datetime.fromtimestamp(
- os.stat(src).st_mtime)
- return self.dates[src]
diff --git a/nikola/plugins/task_render_posts.py b/nikola/plugins/task_render_posts.py
deleted file mode 100644
index 4be68bf..0000000
--- a/nikola/plugins/task_render_posts.py
+++ /dev/null
@@ -1,140 +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 copy import copy
-import codecs
-import string
-
-from nikola.plugin_categories import Task
-from nikola import utils, rc4
-
-
-def wrap_encrypt(path, password):
- """Wrap a post with encryption."""
- with codecs.open(path, 'rb+', 'utf8') as inf:
- data = inf.read() + "<!--tail-->"
- data = CRYPT.substitute(data=rc4.rc4(password, data))
- with codecs.open(path, 'wb+', 'utf8') as outf:
- outf.write(data)
-
-
-class RenderPosts(Task):
- """Build HTML fragments from metadata and text."""
-
- name = "render_posts"
-
- def gen_tasks(self):
- """Build HTML fragments from metadata and text."""
- self.site.scan_posts()
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "timeline": self.site.timeline,
- "default_lang": self.site.config["DEFAULT_LANG"],
- "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
- }
-
- flag = False
- for lang in kw["translations"]:
- deps_dict = copy(kw)
- deps_dict.pop('timeline')
- for post in kw['timeline']:
- source = post.source_path
- dest = post.base_path
- if not post.is_translation_available(lang) and kw["hide_untranslated_posts"]:
- continue
- else:
- source = post.translated_source_path(lang)
- if lang != post.default_lang:
- dest = dest + '.' + lang
- flag = True
- task = {
- 'basename': self.name,
- 'name': dest,
- 'file_dep': post.fragment_deps(lang),
- 'targets': [dest],
- 'actions': [(self.site.get_compiler(post.source_path),
- [source, dest])],
- 'clean': True,
- 'uptodate': [utils.config_changed(deps_dict)],
- }
- if post.meta('password'):
- task['actions'].append((wrap_encrypt, (dest, post.meta('password'))))
- yield task
- if flag is False: # Return a dummy task
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
-
-CRYPT = string.Template("""\
-<script>
-function rc4(key, str) {
- var s = [], j = 0, x, res = '';
- for (var i = 0; i < 256; i++) {
- s[i] = i;
- }
- for (i = 0; i < 256; i++) {
- j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
- x = s[i];
- s[i] = s[j];
- s[j] = x;
- }
- i = 0;
- j = 0;
- for (var y = 0; y < str.length; y++) {
- i = (i + 1) % 256;
- j = (j + s[i]) % 256;
- x = s[i];
- s[i] = s[j];
- s[j] = x;
- res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
- }
- return res;
-}
-function decrypt() {
- key = $$("#key").val();
- crypt_div = $$("#encr")
- crypted = crypt_div.html();
- decrypted = rc4(key, window.atob(crypted));
- if (decrypted.substr(decrypted.length - 11) == "<!--tail-->"){
- crypt_div.html(decrypted);
- $$("#pwform").hide();
- crypt_div.show();
- } else { alert("Wrong password"); };
-}
-</script>
-
-<div id="encr" style="display: none;">${data}</div>
-<div id="pwform">
-<form onsubmit="javascript:decrypt(); return false;" class="form-inline">
-<fieldset>
-<legend>This post is password-protected.</legend>
-<input type="password" id="key" placeholder="Type password here">
-<button type="submit" class="btn">Show Content</button>
-</fieldset>
-</form>
-</div>""")
diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
deleted file mode 100644
index 044e0e3..0000000
--- a/nikola/plugins/task_sitemap/__init__.py
+++ /dev/null
@@ -1,105 +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 print_function, absolute_import, unicode_literals
-import codecs
-import datetime
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-
-from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed
-
-
-header = """<?xml version="1.0" encoding="UTF-8"?>
-<urlset
- xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
- http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
-"""
-
-url_format = """ <url>
- <loc>{0}</loc>
- <lastmod>{1}</lastmod>
- <priority>0.5000</priority>
- </url>
-"""
-
-get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
-
-
-class Sitemap(LateTask):
- """Generate google sitemap."""
-
- name = "sitemap"
-
- def gen_tasks(self):
- """Generate Google sitemap."""
- kw = {
- "base_url": self.site.config["BASE_URL"],
- "site_url": self.site.config["SITE_URL"],
- "output_folder": self.site.config["OUTPUT_FOLDER"],
- "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm'])
- }
- output_path = kw['output_folder']
- sitemap_path = os.path.join(output_path, "sitemap.xml")
-
- def sitemap():
- with codecs.open(sitemap_path, 'wb+', 'utf8') as outf:
- output = kw['output_folder']
- base_url = kw['base_url']
- mapped_exts = kw['mapped_extensions']
- outf.write(header)
- locs = {}
- for root, dirs, files in os.walk(output):
- path = os.path.relpath(root, output)
- path = path.replace(os.sep, '/') + '/'
- lastmod = get_lastmod(root)
- loc = urljoin(base_url, path)
- locs[loc] = url_format.format(loc, lastmod)
- for fname in files:
- if os.path.splitext(fname)[-1] in mapped_exts:
- real_path = os.path.join(root, fname)
- path = os.path.relpath(real_path, output)
- path = path.replace(os.sep, '/')
- lastmod = get_lastmod(real_path)
- loc = urljoin(base_url, path)
- locs[loc] = url_format.format(loc, lastmod)
-
- for k in sorted(locs.keys()):
- outf.write(locs[k])
- outf.write("</urlset>")
-
- yield {
- "basename": "sitemap",
- "name": sitemap_path,
- "targets": [sitemap_path],
- "actions": [(sitemap,)],
- "uptodate": [config_changed(kw)],
- "clean": True,
- }
diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/template/__init__.py
diff --git a/nikola/plugins/template_jinja.plugin b/nikola/plugins/template/jinja.plugin
index 01e6d8c..53b0fec 100644
--- a/nikola/plugins/template_jinja.plugin
+++ b/nikola/plugins/template/jinja.plugin
@@ -1,9 +1,9 @@
[Core]
Name = jinja
-Module = template_jinja
+Module = jinja
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Support for Jinja2 templates.
diff --git a/nikola/plugins/template_jinja.py b/nikola/plugins/template/jinja.py
index b6d762b..a40a476 100644
--- a/nikola/plugins/template_jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,12 +28,15 @@
import os
import json
+from collections import deque
try:
import jinja2
+ from jinja2 import meta
except ImportError:
jinja2 = None # NOQA
from nikola.plugin_categories import TemplateSystem
+from nikola.utils import makedirs, req_missing
class JinjaTemplates(TemplateSystem):
@@ -39,6 +44,7 @@ class JinjaTemplates(TemplateSystem):
name = "jinja"
lookup = None
+ dependency_cache = {}
def __init__(self):
""" initialize Jinja2 wrapper with extended set of filters"""
@@ -46,31 +52,51 @@ class JinjaTemplates(TemplateSystem):
return
self.lookup = jinja2.Environment()
self.lookup.filters['tojson'] = json.dumps
+ self.lookup.globals['enumerate'] = enumerate
def set_directories(self, directories, cache_folder):
"""Createa template lookup."""
if jinja2 is None:
- raise Exception('To use this theme you need to install the '
- '"Jinja2" package.')
+ req_missing(['jinja2'], 'use this theme')
self.lookup.loader = jinja2.FileSystemLoader(directories,
encoding='utf-8')
def render_template(self, template_name, output_name, context):
"""Render the template into output_name using context."""
if jinja2 is None:
- raise Exception('To use this theme you need to install the '
- '"Jinja2" package.')
+ req_missing(['jinja2'], 'use this theme')
template = self.lookup.get_template(template_name)
output = template.render(**context)
if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ makedirs(os.path.dirname(output_name))
with open(output_name, 'w+') as output:
output.write(output.encode('utf8'))
return output
+ def render_template_to_string(self, template, context):
+ """ Render template to a string using context. """
+
+ return jinja2.Template(template).render(**context)
+
def template_deps(self, template_name):
- # FIXME: unimplemented
- return []
+ # Cache the lists of dependencies for each template name.
+ if self.dependency_cache.get(template_name) is None:
+ # Use a breadth-first search to find all templates this one
+ # depends on.
+ queue = deque([template_name])
+ visited_templates = set([template_name])
+ deps = []
+ while len(queue) > 0:
+ curr = queue.popleft()
+ source, filename = self.lookup.loader.get_source(self.lookup,
+ curr)[:2]
+ deps.append(filename)
+ ast = self.lookup.parse(source)
+ dep_names = meta.find_referenced_templates(ast)
+ for dep_name in dep_names:
+ if (dep_name not in visited_templates
+ and dep_name is not None):
+ visited_templates.add(dep_name)
+ queue.append(dep_name)
+ self.dependency_cache[template_name] = deps
+ return self.dependency_cache[template_name]
diff --git a/nikola/plugins/template_mako.plugin b/nikola/plugins/template/mako.plugin
index 3fdc354..71f2c71 100644
--- a/nikola/plugins/template_mako.plugin
+++ b/nikola/plugins/template/mako.plugin
@@ -1,9 +1,9 @@
[Core]
Name = mako
-Module = template_mako
+Module = mako
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Support for Mako templates.
diff --git a/nikola/plugins/template_mako.py b/nikola/plugins/template/mako.py
index 6ec6698..ae51cac 100644
--- a/nikola/plugins/template_mako.py
+++ b/nikola/plugins/template/mako.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,14 +25,21 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Mako template handlers"""
-
+from __future__ import unicode_literals, print_function, absolute_import
import os
import shutil
+import sys
+import tempfile
from mako import util, lexer
from mako.lookup import TemplateLookup
+from mako.template import Template
+from markupsafe import Markup # It's ok, Mako requires it
from nikola.plugin_categories import TemplateSystem
+from nikola.utils import makedirs, get_logger, STDERR_HANDLER
+
+LOGGER = get_logger('mako', STDERR_HANDLER)
class MakoTemplates(TemplateSystem):
@@ -55,8 +64,16 @@ class MakoTemplates(TemplateSystem):
return deps
def set_directories(self, directories, cache_folder):
- """Createa template lookup."""
+ """Create a template lookup."""
cache_dir = os.path.join(cache_folder, '.mako.tmp')
+ # Workaround for a Mako bug, Issue #825
+ if sys.version_info[0] == 2:
+ try:
+ os.path.abspath(cache_dir).decode('ascii')
+ except UnicodeEncodeError:
+ cache_dir = tempfile.mkdtemp()
+ LOGGER.warning('Because of a Mako bug, setting cache_dir to {0}'.format(cache_dir))
+
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
self.lookup = TemplateLookup(
@@ -66,21 +83,23 @@ class MakoTemplates(TemplateSystem):
def render_template(self, template_name, output_name, context):
"""Render the template into output_name using context."""
-
+ context['striphtml'] = striphtml
template = self.lookup.get_template(template_name)
data = template.render_unicode(**context)
if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ makedirs(os.path.dirname(output_name))
with open(output_name, 'w+') as output:
output.write(data)
return data
+ def render_template_to_string(self, template, context):
+ """ Render template to a string using context. """
+
+ return Template(template).render(**context)
+
def template_deps(self, template_name):
"""Returns filenames which are dependencies for a template."""
- # We can cache here because depedencies should
+ # We can cache here because dependencies should
# not change between runs
if self.cache.get(template_name, None) is None:
template = self.lookup.get_template(template_name)
@@ -90,3 +109,7 @@ class MakoTemplates(TemplateSystem):
deps += self.template_deps(fname)
self.cache[template_name] = tuple(deps)
return list(self.cache[template_name])
+
+
+def striphtml(text):
+ return Markup(text).striptags()