summaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
commitca94afc07df55cb7fc6fe3b4f3011877b7881195 (patch)
treed81e1f275aa77545f33740723f307a83dde2e0b4 /nikola/plugins/task
parentf794eee787e9cde54e6b8f53e45d69c9ddc9936a (diff)
Imported Upstream version 6.2.1upstream/6.2.1
Diffstat (limited to 'nikola/plugins/task')
-rw-r--r--nikola/plugins/task/__init__.py0
-rw-r--r--nikola/plugins/task/archive.plugin10
-rw-r--r--nikola/plugins/task/archive.py167
-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.plugin10
-rw-r--r--nikola/plugins/task/bundles.py116
-rw-r--r--nikola/plugins/task/copy_assets.plugin10
-rw-r--r--nikola/plugins/task/copy_assets.py89
-rw-r--r--nikola/plugins/task/copy_files.plugin10
-rw-r--r--nikola/plugins/task/copy_files.py55
-rw-r--r--nikola/plugins/task/galleries.plugin10
-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.plugin10
-rw-r--r--nikola/plugins/task/indexes.py167
-rw-r--r--nikola/plugins/task/listings.plugin10
-rw-r--r--nikola/plugins/task/listings.py136
-rw-r--r--nikola/plugins/task/localsearch.plugin10
-rw-r--r--nikola/plugins/task/localsearch/MIT-LICENSE.txt20
-rw-r--r--nikola/plugins/task/localsearch/__init__.py106
-rw-r--r--nikola/plugins/task/localsearch/files/assets/css/img/loader.gifbin0 -> 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.js384
-rw-r--r--nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js21
-rwxr-xr-xnikola/plugins/task/localsearch/files/tipue_search.html31
-rw-r--r--nikola/plugins/task/mustache.plugin10
-rw-r--r--nikola/plugins/task/mustache/__init__.py182
-rw-r--r--nikola/plugins/task/mustache/mustache-template.html29
-rw-r--r--nikola/plugins/task/mustache/mustache.html34
-rw-r--r--nikola/plugins/task/pages.plugin10
-rw-r--r--nikola/plugins/task/pages.py58
-rw-r--r--nikola/plugins/task/posts.plugin10
-rw-r--r--nikola/plugins/task/posts.py66
-rw-r--r--nikola/plugins/task/redirect.plugin10
-rw-r--r--nikola/plugins/task/redirect.py66
-rw-r--r--nikola/plugins/task/rss.plugin10
-rw-r--r--nikola/plugins/task/rss.py91
-rw-r--r--nikola/plugins/task/sitemap.plugin10
-rw-r--r--nikola/plugins/task/sitemap/__init__.py172
-rw-r--r--nikola/plugins/task/sources.plugin10
-rw-r--r--nikola/plugins/task/sources.py81
-rw-r--r--nikola/plugins/task/tags.plugin10
-rw-r--r--nikola/plugins/task/tags.py324
48 files changed, 3590 insertions, 0 deletions
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
new file mode 100644
index 0000000..448b115
--- /dev/null
+++ b/nikola/plugins/task/archive.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_archive
+Module = archive
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..3afbea1
--- /dev/null
+++ b/nikola/plugins/task/archive.py
@@ -0,0 +1,167 @@
+# -*- 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.
+
+import os
+
+# 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
+
+
+class Archive(Task):
+ """Render the post archives."""
+
+ 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,
+ "translations": self.site.config['TRANSLATIONS'],
+ "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"]:
+ 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
+ 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"
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(key=lambda a: a.date)
+ post_list.reverse()
+ context["posts"] = post_list
+ 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"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), month] for month in months]
+ post_list = []
+ 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_monthly_archive"]:
+ continue # Just to avoid nesting the other loop in this if
+ template_name = "list_post.tmpl"
+ for yearmonth, posts in self.site.posts_per_month.items():
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", yearmonth,
+ lang))
+ year, month = yearmonth.split('/')
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(key=lambda a: a.date)
+ post_list.reverse()
+ context = {}
+ context["lang"] = lang
+ context["posts"] = post_list
+ context["permalink"] = self.site.link("archive", year, lang)
+
+ context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
+ year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
+ task = self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ 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 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/bundles.plugin b/nikola/plugins/task/bundles.plugin
new file mode 100644
index 0000000..e0b0a4d
--- /dev/null
+++ b/nikola/plugins/task/bundles.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = create_bundles
+Module = bundles
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Theme bundles using WebAssets
+
diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py
new file mode 100644
index 0000000..488f96f
--- /dev/null
+++ b/nikola/plugins/task/bundles.py
@@ -0,0 +1,116 @@
+# -*- 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 os
+
+try:
+ import webassets
+except ImportError:
+ webassets = None # NOQA
+
+from nikola.plugin_categories import LateTask
+from nikola import utils
+
+
+class BuildBundles(LateTask):
+ """Bundle assets using WebAssets."""
+
+ name = "create_bundles"
+
+ def set_site(self, site):
+ super(BuildBundles, self).set_site(site)
+ if webassets is None:
+ self.site.config['USE_BUNDLES'] = False
+
+ def gen_tasks(self):
+ """Bundle assets using WebAssets."""
+
+ kw = {
+ 'filters': self.site.config['FILTERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'theme_bundles': get_theme_bundles(self.site.THEMES),
+ 'themes': self.site.THEMES,
+ 'files_folders': self.site.config['FILES_FOLDERS'],
+ 'code_color_scheme': self.site.config['CODE_COLOR_SCHEME'],
+ }
+
+ def build_bundle(output, inputs):
+ out_dir = os.path.join(kw['output_folder'],
+ os.path.dirname(output))
+ inputs = [i for i in inputs if os.path.isfile(
+ os.path.join(out_dir, i))]
+ cache_dir = os.path.join(kw['cache_folder'], 'webassets')
+ utils.makedirs(cache_dir)
+ env = webassets.Environment(out_dir, os.path.dirname(output),
+ cache=cache_dir)
+ 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
+
+ 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 = [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': 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)],
+ 'clean': True,
+ }
+ yield utils.apply_filters(task, kw['filters'])
+
+
+def get_theme_bundles(themes):
+ """Given a theme chain, return the bundle definitions."""
+ bundles = {}
+ for theme_name in themes:
+ bundles_path = os.path.join(
+ utils.get_theme_path(theme_name), 'bundles')
+ if os.path.isfile(bundles_path):
+ with open(bundles_path) as fd:
+ for line in fd:
+ name, files = line.split('=')
+ files = [f.strip() for f in files.split(',')]
+ bundles[name.strip()] = files
+ break
+ return bundles
diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
new file mode 100644
index 0000000..28b9e32
--- /dev/null
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = copy_assets
+Module = copy_assets
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..f3d85df
--- /dev/null
+++ b/nikola/plugins/task/copy_assets.py
@@ -0,0 +1,89 @@
+# -*- 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.
+
+import codecs
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class CopyAssets(Task):
+ """Copy theme assets into output."""
+
+ name = "copy_assets"
+
+ def gen_tasks(self):
+ """Create tasks to copy the assets of the whole theme chain.
+
+ If a file is present on two themes, use the version
+ from the "youngest" theme.
+ """
+
+ kw = {
+ "themes": self.site.THEMES,
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
+ }
+ 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')
+ for task in utils.copy_tree(src, dst):
+ if task['name'] in tasks:
+ continue
+ if task['targets'][0] == code_css_path:
+ has_code_css = True
+ tasks[task['name']] = task
+ task['uptodate'] = [utils.config_changed(kw)]
+ task['basename'] = self.name
+ yield utils.apply_filters(task, kw['filters'])
+
+ 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;}")
+
+ task = {
+ 'basename': self.name,
+ 'name': code_css_path,
+ 'targets': [code_css_path],
+ 'uptodate': [utils.config_changed(kw)],
+ 'actions': [(create_code_css, [])],
+ 'clean': True,
+ }
+ yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin
new file mode 100644
index 0000000..45c9e0d
--- /dev/null
+++ b/nikola/plugins/task/copy_files.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = copy_files
+Module = copy_files
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..88e89eb
--- /dev/null
+++ b/nikola/plugins/task/copy_files.py
@@ -0,0 +1,55 @@
+# -*- 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.
+
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class CopyFiles(Task):
+ """Copy static files into the output folder."""
+
+ name = "copy_files"
+
+ def gen_tasks(self):
+ """Copy static files into the output folder."""
+
+ kw = {
+ 'files_folders': self.site.config['FILES_FOLDERS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'filters': self.site.config['FILTERS'],
+ }
+
+ 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):
+ task['basename'] = self.name
+ task['uptodate'] = [utils.config_changed(kw)]
+ yield utils.apply_filters(task, filters)
diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin
new file mode 100644
index 0000000..8352151
--- /dev/null
+++ b/nikola/plugins/task/galleries.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_galleries
+Module = galleries
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..a18942c
--- /dev/null
+++ b/nikola/plugins/task/indexes.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_indexes
+Module = indexes
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..0d20422
--- /dev/null
+++ b/nikola/plugins/task/indexes.py
@@ -0,0 +1,167 @@
+# -*- 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 glob
+import itertools
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed
+
+
+class Indexes(Task):
+ """Render the blog indexes."""
+
+ name = "render_indexes"
+
+ def 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'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "messages": self.site.MESSAGES,
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "indexes_title": self.site.config['INDEXES_TITLE'],
+ "indexes_pages": self.site.config['INDEXES_PAGES'],
+ "blog_title": self.site.config["BLOG_TITLE"],
+ }
+
+ template_name = "index.tmpl"
+ posts = [x for x in self.site.timeline if x.use_in_feeds]
+ for lang in kw["translations"]:
+ # Split in smaller lists
+ lists = []
+ if kw["hide_untranslated_posts"]:
+ 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"]]
+ num_pages = len(lists)
+ for i, post_list in enumerate(lists):
+ context = {}
+ indexes_title = kw['indexes_title'] or kw['blog_title']
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % i
+ else:
+ 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 == 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,
+ lang))
+ task = self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = 'render_indexes'
+ yield task
+
+ if not self.site.config["STORY_INDEX"]:
+ return
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "post_pages": self.site.config["post_pages"],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ }
+ template_name = "list.tmpl"
+ for lang in kw["translations"]:
+ # 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 = []
+ 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,
+ lang)).encode('utf8')
+ context["items"] = [(post.title(lang), post.permalink(lang))
+ for post in post_list]
+ task = self.site.generic_post_list_renderer(lang, post_list,
+ 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 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/listings.plugin b/nikola/plugins/task/listings.plugin
new file mode 100644
index 0000000..c93184d
--- /dev/null
+++ b/nikola/plugins/task/listings.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_listings
+Module = listings
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Render code listings into output
+
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
new file mode 100644
index 0000000..ab62e74
--- /dev/null
+++ b/nikola/plugins/task/listings.py
@@ -0,0 +1,136 @@
+# -*- 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 os
+
+from pygments import highlight
+from pygments.lexers import get_lexer_for_filename, TextLexer
+from pygments.formatters import HtmlFormatter
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Listings(Task):
+ """Render pretty listings."""
+
+ name = "render_listings"
+
+ def 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
+ ignored_extensions = (".pyc",)
+
+ def render_listing(in_name, out_name, folders=[], files=[]):
+ if in_name:
+ with open(in_name, 'r') as fd:
+ try:
+ lexer = get_lexer_for_filename(in_name)
+ except:
+ lexer = TextLexer()
+ code = highlight(fd.read(), lexer,
+ HtmlFormatter(cssclass='code',
+ linenos="table", nowrap=False,
+ lineanchors=utils.slugify(in_name),
+ anchorlinenos=True))
+ title = os.path.basename(in_name)
+ else:
+ code = ''
+ title = ''
+ crumbs = utils.get_crumbs(os.path.relpath(out_name,
+ kw['output_folder']),
+ is_file=True)
+ context = {
+ 'code': code,
+ 'title': title,
+ 'crumbs': crumbs,
+ 'lang': kw['default_lang'],
+ 'folders': folders,
+ 'files': files,
+ 'description': title,
+ }
+ self.site.render_template('listing.tmpl', out_name,
+ context)
+
+ yield self.group_task()
+
+ template_deps = self.site.template_system.template_deps('listing.tmpl')
+ for root, dirs, files in os.walk(kw['listings_folder']):
+ # Render all files
+ out_name = os.path.join(
+ kw['output_folder'],
+ root, kw['index_file']
+ )
+ yield {
+ 'basename': self.name,
+ 'name': out_name,
+ 'file_dep': template_deps,
+ 'targets': [out_name],
+ 'actions': [(render_listing, [None, out_name, dirs, files])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(
+ self.site.GLOBAL_CONTEXT)],
+ 'clean': True,
+ }
+ for f in files:
+ ext = os.path.splitext(f)[-1]
+ if ext in ignored_extensions:
+ continue
+ in_name = os.path.join(root, f)
+ out_name = os.path.join(
+ kw['output_folder'],
+ root,
+ f) + '.html'
+ yield {
+ 'basename': self.name,
+ 'name': out_name,
+ 'file_dep': template_deps + [in_name],
+ 'targets': [out_name],
+ 'actions': [(render_listing, [in_name, out_name])],
+ # This is necessary to reflect changes in blog title,
+ # sidebar links, etc.
+ 'uptodate': [utils.config_changed(
+ self.site.GLOBAL_CONTEXT)],
+ 'clean': True,
+ }
+
+ 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
new file mode 100644
index 0000000..86accb6
--- /dev/null
+++ b/nikola/plugins/task/localsearch.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = local_search
+Module = localsearch
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..f131068
--- /dev/null
+++ b/nikola/plugins/task/localsearch/MIT-LICENSE.txt
@@ -0,0 +1,20 @@
+Tipue Search Copyright (c) 2012 Tipue
+
+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/task/localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py
new file mode 100644
index 0000000..9162604
--- /dev/null
+++ b/nikola/plugins/task/localsearch/__init__.py
@@ -0,0 +1,106 @@
+# -*- 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 json
+import os
+
+from doit.tools import result_dep
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed, copy_tree, makedirs
+
+# This is what we need to produce:
+#var tipuesearch = {"pages": [
+ #{"title": "Tipue Search, a jQuery site search engine", "text": "Tipue
+ #Search is a site search engine jQuery plugin. It's free for both commercial and
+ #non-commercial use and released under the MIT License. Tipue Search includes
+ #features such as word stemming and word replacement.", "tags": "JavaScript",
+ #"loc": "http://www.tipue.com/search"},
+ #{"title": "Tipue Search demo", "text": "Tipue Search demo. Tipue Search is
+ #a site search engine jQuery plugin.", "tags": "JavaScript", "loc":
+ #"http://www.tipue.com/search/demo"},
+ #{"title": "About Tipue", "text": "Tipue is a small web development/design
+ #studio based in North London. We've been around for over a decade.", "tags": "",
+ #"loc": "http://www.tipue.com/about"}
+#]};
+
+
+class Tipue(LateTask):
+ """Render the blog posts as JSON data."""
+
+ name = "local_search"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ }
+
+ posts = self.site.timeline[:]
+ dst_path = os.path.join(kw["output_folder"], "assets", "js",
+ "tipuesearch_content.json")
+
+ def save_data():
+ pages = []
+ for lang in kw["translations"]:
+ for post in posts:
+ # Don't index drafts (Issue #387)
+ if post.is_draft or post.is_retired or post.publish_later:
+ continue
+ text = post.text(lang, strip_html=True)
+ text = text.replace('^', '')
+
+ data = {}
+ data["title"] = post.title(lang)
+ data["text"] = text
+ data["tags"] = ",".join(post.tags)
+ data["loc"] = post.permalink(lang)
+ pages.append(data)
+ output = json.dumps({"pages": pages}, indent=2)
+ makedirs(os.path.dirname(dst_path))
+ with codecs.open(dst_path, "wb+", "utf8") as fd:
+ fd.write(output)
+
+ yield {
+ "basename": str(self.name),
+ "name": dst_path,
+ "targets": [dst_path],
+ "actions": [(save_data, [])],
+ '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")
+ for task in copy_tree(asset_folder, kw["output_folder"]):
+ task["basename"] = str(self.name)
+ yield task
diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif
new file mode 100644
index 0000000..9c97738
--- /dev/null
+++ 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
new file mode 100644
index 0000000..a9982cd
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js
@@ -0,0 +1,384 @@
+
+/*
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+(function($) {
+
+ $.fn.tipuesearch = function(options) {
+
+ var set = $.extend( {
+
+ 'show' : 7,
+ 'newWindow' : false,
+ 'showURL' : true,
+ 'minimumLength' : 3,
+ 'descriptiveWords' : 25,
+ 'highlightTerms' : true,
+ 'highlightEveryTerm' : false,
+ 'mode' : 'static',
+ 'liveDescription' : '*',
+ 'liveContent' : '*',
+ 'contentLocation' : 'tipuesearch/tipuesearch_content.json'
+
+ }, options);
+
+ return this.each(function() {
+
+ var tipuesearch_in = {
+ pages: []
+ };
+ $.ajaxSetup({
+ async: false
+ });
+
+ if (set.mode == 'live')
+ {
+ for (var i = 0; i < tipuesearch_pages.length; i++)
+ {
+ $.get(tipuesearch_pages[i], '',
+ function (html)
+ {
+ var cont = $(set.liveContent, html).text();
+ 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)
+ {
+ var tit = html.slice(t_1 + 7, t_2);
+ }
+ else
+ {
+ var tit = 'No title';
+ }
+
+ tipuesearch_in.pages.push({
+ "title": tit,
+ "text": desc,
+ "tags": cont,
+ "loc": tipuesearch_pages[i]
+ });
+ }
+ );
+ }
+ }
+
+ if (set.mode == 'json')
+ {
+ $.getJSON(set.contentLocation,
+ function(json)
+ {
+ tipuesearch_in = $.extend({}, json);
+ }
+ );
+ }
+
+ if (set.mode == 'static')
+ {
+ tipuesearch_in = $.extend({}, tipuesearch);
+ }
+
+ var tipue_search_w = '';
+ if (set.newWindow)
+ {
+ tipue_search_w = ' target="_blank"';
+ }
+
+ function getURLP(name)
+ {
+ return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null;
+ }
+ if (getURLP('q'))
+ {
+ $('#tipue_search_input').val(getURLP('q'));
+ getTipueSearch(0, true);
+ }
+
+ $('#tipue_search_button').click(function()
+ {
+ getTipueSearch(0, true);
+ });
+ $(this).keyup(function(event)
+ {
+ if(event.keyCode == '13')
+ {
+ getTipueSearch(0, true);
+ }
+ });
+
+ function getTipueSearch(start, replace)
+ {
+ $('#tipue_search_content').hide();
+ var out = '';
+ 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(' ');
+ d = '';
+ for (var i = 0; i < d_w.length; i++)
+ {
+ var a_w = true;
+ for (var f = 0; f < tipuesearch_stop_words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stop_words[f])
+ {
+ a_w = false;
+ show_stop = true;
+ }
+ }
+ if (a_w)
+ {
+ d = d + ' ' + d_w[i];
+ }
+ }
+ d = $.trim(d);
+ d_w = d.split(' ');
+
+ if (d.length >= set.minimumLength)
+ {
+ if (replace)
+ {
+ var d_r = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_replace.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_replace.words[f].word)
+ {
+ d = d.replace(d_w[i], tipuesearch_replace.words[f].replace_with);
+ show_replace = true;
+ }
+ }
+ }
+ d_w = d.split(' ');
+ }
+
+ var d_t = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_stem.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stem.words[f].word)
+ {
+ d_t = d_t + ' ' + tipuesearch_stem.words[f].stem;
+ }
+ }
+ }
+ d_w = d_t.split(' ');
+
+ var c = 0;
+ found = new Array();
+ for (var i = 0; i < tipuesearch_in.pages.length; i++)
+ {
+ var score = 1000000000;
+ var s_t = tipuesearch_in.pages[i].text;
+ for (var f = 0; f < d_w.length; f++)
+ {
+ var pat = new RegExp(d_w[f], 'i');
+ if (tipuesearch_in.pages[i].title.search(pat) != -1)
+ {
+ score -= (200000 - i);
+ }
+ if (tipuesearch_in.pages[i].text.search(pat) != -1)
+ {
+ score -= (150000 - i);
+ }
+
+ if (set.highlightTerms)
+ {
+ if (set.highlightEveryTerm)
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'gi');
+ }
+ else
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'i');
+ }
+ s_t = s_t.replace(patr, "<b>$1</b>");
+ }
+ if (tipuesearch_in.pages[i].tags.search(pat) != -1)
+ {
+ score -= (100000 - i);
+ }
+
+ }
+ if (score < 1000000000)
+ {
+ 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">Search for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
+ }
+ if (c == 1)
+ {
+ out += '<div id="tipue_search_results_count">1 result</div>';
+ }
+ else
+ {
+ 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++)
+ {
+ var fo = found[i].split('^');
+ 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>';
+
+ var t = fo[2];
+ var t_d = '';
+ var t_w = t.split(' ');
+ if (t_w.length < set.descriptiveWords)
+ {
+ t_d = t;
+ }
+ else
+ {
+ for (var f = 0; f < set.descriptiveWords; f++)
+ {
+ t_d += t_w[f] + ' ';
+ }
+ }
+ t_d = $.trim(t_d);
+ if (t_d.charAt(t_d.length - 1) != '.')
+ {
+ t_d += ' ...';
+ }
+ out += '<div class="tipue_search_content_text">' + t_d + '</div>';
+
+ if (set.showURL)
+ {
+ 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++;
+ }
+
+ 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 + '">Prev</a></li>';
+ }
+
+ if (page <= 2)
+ {
+ var p_b = pages;
+ if (pages > 3)
+ {
+ p_b = 3;
+ }
+ for (var f = 0; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
+ }
+ }
+ }
+ else
+ {
+ var p_b = page + 3;
+ if (p_b > pages)
+ {
+ p_b = pages;
+ }
+ for (var f = page; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ 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</a></li>';
+ }
+
+ out += '</ul></div>';
+ }
+ }
+ else
+ {
+ 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>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning_head">Search too short</div>';
+ if (set.minimumLength == 1)
+ {
+ out += '<div id="tipue_search_warning">Should be one character or more</div>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning">Should be ' + set.minimumLength + ' characters or more</div>';
+ }
+ }
+ }
+
+ $('#tipue_search_content').html(out);
+ $('#tipue_search_content').slideDown(200);
+
+ $('#tipue_search_replaced').click(function()
+ {
+ getTipueSearch(0, false);
+ });
+
+ $('.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);
diff --git a/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
new file mode 100644
index 0000000..8493ec1
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
@@ -0,0 +1,21 @@
+
+/*
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+var tipuesearch_stop_words = ["and", "be", "by", "do", "for", "he", "how", "if", "is", "it", "my", "not", "of", "or", "the", "to", "up", "what", "when"];
+
+var tipuesearch_replace = {"words": [
+ {"word": "tipua", replace_with: "tipue"},
+ {"word": "javscript", replace_with: "javascript"}
+]};
+
+var tipuesearch_stem = {"words": [
+ {"word": "e-mail", stem: "email"},
+ {"word": "javascript", stem: "script"},
+ {"word": "javascript", stem: "js"}
+]};
diff --git a/nikola/plugins/task/localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html
new file mode 100755
index 0000000..789fbe5
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/tipue_search.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+<head>
+<title>Tipue Search</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+
+<link rel="stylesheet" type="text/css" href="assets/css/tipuesearch.css">
+<script type="text/javascript" src="assets/js/tipuesearch_set.js"></script>
+<script type="text/javascript" src="assets/js/tipuesearch.js"></script>
+
+</head>
+<body>
+
+<div style="float: left;"><input type="text" id="tipue_search_input"></div>
+<div style="float: left; margin-left: 13px;"><input type="button" id="tipue_search_button"></div>
+<div id="tipue_search_content"><div id="tipue_search_loading"></div></div>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#tipue_search_input').tipuesearch({
+ 'mode': 'json',
+ 'contentLocation': 'assets/js/tipuesearch_content.json'
+ });
+});
+</script>
+</body>
+</html>
diff --git a/nikola/plugins/task/mustache.plugin b/nikola/plugins/task/mustache.plugin
new file mode 100644
index 0000000..d6b487a
--- /dev/null
+++ b/nikola/plugins/task/mustache.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_mustache
+Module = mustache
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..c392e3b
--- /dev/null
+++ b/nikola/plugins/task/mustache/__init__.py
@@ -0,0 +1,182 @@
+# -*- 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 json
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed, copy_file, unicode_str, makedirs
+
+
+class Mustache(Task):
+ """Render the blog posts as JSON data."""
+
+ name = "render_mustache"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "messages": self.site.MESSAGES,
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "blog_title": self.site.config['BLOG_TITLE'],
+ "content_footer": self.site.config['CONTENT_FOOTER'],
+ }
+
+ # TODO: timeline is global, get rid of it
+ posts = [x for x in self.site.timeline if x.use_in_feeds]
+ if not posts:
+ yield {
+ 'basename': 'render_mustache',
+ 'actions': [],
+ }
+ return
+
+ def write_file(path, post, lang):
+
+ # Prev/Next links
+ prev_link = False
+ if post.prev_post:
+ prev_link = post.prev_post.permalink(lang).replace(".html",
+ ".json")
+ next_link = False
+ if post.next_post:
+ next_link = post.next_post.permalink(lang).replace(".html",
+ ".json")
+ data = {}
+
+ # Configuration
+ for k, v in self.site.config.items():
+ if isinstance(v, (str, unicode_str)): # NOQA
+ data[k] = v
+
+ # Tag data
+ tags = []
+ for tag in post.tags:
+ tags.append({'name': tag, 'link': self.site.link("tag", tag,
+ lang)})
+ data.update({
+ "tags": tags,
+ "tags?": True if tags else False,
+ })
+
+ # Template strings
+ for k, v in kw["messages"][lang].items():
+ data["message_" + k] = v
+
+ # Post data
+ data.update({
+ "title": post.title(lang),
+ "text": post.text(lang),
+ "prev": prev_link,
+ "next": next_link,
+ "date":
+ post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']),
+ })
+
+ # 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 = []
+ for langname in kw["translations"]:
+ if langname == lang:
+ continue
+ translations.append({'name':
+ kw["messages"][langname]["Read in English"],
+ 'link': "javascript:load_data('%s');"
+ % post.permalink(langname).replace(
+ ".html", ".json")})
+ data["translations"] = translations
+
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, 'wb+', 'utf8') as fd:
+ fd.write(json.dumps(data))
+
+ for lang in kw["translations"]:
+ for i, post in enumerate(posts):
+ out_path = post.destination_path(lang, ".json")
+ out_file = os.path.join(kw['output_folder'], out_path)
+ task = {
+ 'basename': 'render_mustache',
+ 'name': out_file,
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [out_file],
+ 'actions': [(write_file, (out_file, post, lang))],
+ 'task_dep': ['render_posts'],
+ 'uptodate': [config_changed({
+ 1: post.text(lang),
+ 2: post.prev_post,
+ 3: post.next_post,
+ 4: post.title(lang),
+ })]
+ }
+ yield task
+
+ if posts:
+ first_post_data = posts[0].permalink(
+ self.site.config["DEFAULT_LANG"]).replace(".html", ".json")
+
+ # Copy mustache template
+ src = os.path.join(os.path.dirname(__file__), 'mustache-template.html')
+ dst = os.path.join(kw['output_folder'], 'mustache-template.html')
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'actions': [(copy_file, (src, dst))],
+ }
+
+ # Copy mustache.html with the right starting file in it
+ src = os.path.join(os.path.dirname(__file__), 'mustache.html')
+ dst = os.path.join(kw['output_folder'], 'mustache.html')
+
+ def copy_mustache():
+ with codecs.open(src, 'rb', 'utf8') as in_file:
+ with codecs.open(dst, 'wb+', 'utf8') as out_file:
+ data = in_file.read().replace('{{first_post_data}}',
+ first_post_data)
+ out_file.write(data)
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'uptodate': [config_changed({1: first_post_data})],
+ 'actions': [(copy_mustache, [])],
+ }
diff --git a/nikola/plugins/task/mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html
new file mode 100644
index 0000000..e9a0213
--- /dev/null
+++ b/nikola/plugins/task/mustache/mustache-template.html
@@ -0,0 +1,29 @@
+<script id="view" type="text/html">
+<div class="container" id="container">
+ <div class="postbox">
+ <h1>{{BLOG_TITLE}}</h1>
+ <hr>
+ <h2>{{title}}</h2>
+ Posted on: {{date}}</br>
+ {{#tags?}} More posts about:
+ {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}}
+ </br>
+ {{/tags?}}
+ {{#translations}}<a href={{link}}>{{name}}</a>{{/translations}}&nbsp;</br>
+ <hr>
+ {{{text}}}
+ <ul class="pager">
+ {{#prev}}
+ <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>
+ {{{comment_html}}}
+ </div>
+ <div class="footerbox">
+ {{{CONTENT_FOOTER}}}
+ </div>
+</div>
+</script>
diff --git a/nikola/plugins/task/mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html
new file mode 100644
index 0000000..7ff6312
--- /dev/null
+++ b/nikola/plugins/task/mustache/mustache.html
@@ -0,0 +1,34 @@
+<head>
+ <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css">
+ <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/theme.css" rel="stylesheet" type="text/css"/>
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ <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 type="text/javascript">
+function load_data(dataurl) {
+ jQuery.getJSON(dataurl, function(data) {
+ $('body').mustache('view', data, { method: 'html' });
+ window.location.hash = '#' + dataurl;
+ })
+};
+$(document).ready(function() {
+$.Mustache.load('/mustache-template.html')
+ .done(function () {
+ if (window.location.hash != '') {
+ load_data(window.location.hash.slice(1));
+ }
+ else {
+ load_data('{{first_post_data}}');
+ };
+ })
+});
+</script>
+</head>
+<body style="padding-top: 0;">
+</body>
diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin
new file mode 100644
index 0000000..67212d2
--- /dev/null
+++ b/nikola/plugins/task/pages.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_pages
+Module = pages
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Create pages in the output.
+
diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py
new file mode 100644
index 0000000..eb5b49e
--- /dev/null
+++ b/nikola/plugins/task/pages.py
@@ -0,0 +1,58 @@
+# -*- 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
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed
+
+
+class RenderPages(Task):
+ """Render pages into output."""
+
+ name = "render_pages"
+
+ def gen_tasks(self):
+ """Build final pages from metadata and HTML fragments."""
+ kw = {
+ "post_pages": self.site.config["post_pages"],
+ "translations": self.site.config["TRANSLATIONS"],
+ "filters": self.site.config["FILTERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ }
+ self.site.scan_posts()
+ 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):
+ continue
+ for task in self.site.generic_page_renderer(lang, post,
+ kw["filters"]):
+ task['uptodate'] = [config_changed({
+ 1: task['uptodate'][0].config,
+ 2: kw})]
+ task['basename'] = self.name
+ task['task_dep'] = ['render_posts']
+ yield task
diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin
new file mode 100644
index 0000000..e1a42fd
--- /dev/null
+++ b/nikola/plugins/task/posts.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_posts
+Module = posts
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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
new file mode 100644
index 0000000..826f3d8
--- /dev/null
+++ b/nikola/plugins/task/redirect.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = redirect
+Module = redirect
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Create redirect pages.
+
diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py
new file mode 100644
index 0000000..ade878a
--- /dev/null
+++ b/nikola/plugins/task/redirect.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.
+
+import codecs
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Redirect(Task):
+ """Generate redirections"""
+
+ name = "redirect"
+
+ def gen_tasks(self):
+ """Generate redirections tasks."""
+
+ kw = {
+ 'redirections': self.site.config['REDIRECTIONS'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ }
+
+ yield self.group_task()
+ if kw['redirections']:
+ for src, dst in kw["redirections"]:
+ src_path = os.path.join(kw["output_folder"], src)
+ yield {
+ 'basename': self.name,
+ 'name': src_path,
+ 'targets': [src_path],
+ 'actions': [(create_redirect, (src_path, dst))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
+
+
+def create_redirect(src, dst):
+ 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; '
+ 'url={0}"></head>'.format(dst))
diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin
new file mode 100644
index 0000000..7206a43
--- /dev/null
+++ b/nikola/plugins/task/rss.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = generate_rss
+Module = rss
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Generate RSS feeds.
+
diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py
new file mode 100644
index 0000000..bcca4da
--- /dev/null
+++ b/nikola/plugins/task/rss.py
@@ -0,0 +1,91 @@
+# -*- 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 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 GenerateRSS(Task):
+ """Generate RSS feeds."""
+
+ 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."""
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "filters": self.site.config["FILTERS"],
+ "blog_title": self.site.config["BLOG_TITLE"],
+ "site_url": self.site.config["SITE_URL"],
+ "blog_description": self.site.config["BLOG_DESCRIPTION"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ "rss_teasers": self.site.config["RSS_TEASERS"],
+ "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))
+ deps = []
+ if kw["hide_untranslated_posts"]:
+ posts = [x for x in self.site.timeline if x.use_in_feeds
+ and x.is_translation_available(lang)][:10]
+ else:
+ 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': '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['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
new file mode 100644
index 0000000..2cd8195
--- /dev/null
+++ b/nikola/plugins/task/sitemap.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = sitemap
+Module = sitemap
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+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/sources.plugin b/nikola/plugins/task/sources.plugin
new file mode 100644
index 0000000..6224e48
--- /dev/null
+++ b/nikola/plugins/task/sources.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_sources
+Module = sources
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Copy page sources into the output.
+
diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py
new file mode 100644
index 0000000..672f354
--- /dev/null
+++ b/nikola/plugins/task/sources.py
@@ -0,0 +1,81 @@
+# -*- 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.
+
+import os
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class Sources(Task):
+ """Copy page sources into the output."""
+
+ name = "render_sources"
+
+ def gen_tasks(self):
+ """Publish the page sources into the output.
+
+ Required keyword arguments:
+
+ translations
+ default_lang
+ post_pages
+ output_folder
+ """
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "output_folder": self.site.config["OUTPUT_FOLDER"],
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ }
+
+ self.site.scan_posts()
+ 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/tags.plugin b/nikola/plugins/task/tags.plugin
new file mode 100644
index 0000000..f01e0f8
--- /dev/null
+++ b/nikola/plugins/task/tags.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_tags
+Module = tags
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Render the tag pages and feeds.
+
diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py
new file mode 100644
index 0000000..299dca4
--- /dev/null
+++ b/nikola/plugins/task/tags.py
@@ -0,0 +1,324 @@
+# -*- 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 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/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."""
+
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "blog_title": self.site.config["BLOG_TITLE"],
+ "site_url": self.site.config["SITE_URL"],
+ "messages": self.site.MESSAGES,
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "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 and not self.site.posts_per_category:
+ return
+
+ 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()
+ for lang in kw["translations"]:
+ if kw["hide_untranslated_posts"]:
+ filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
+ else:
+ filtered_posts = post_list
+ 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, is_category)
+ else:
+ 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 = {}
+ for tag, posts in self.site.posts_per_tag.items():
+ tag_cloud_data[tag] = [len(posts), self.site.link(
+ 'tag', tag, self.site.config['DEFAULT_LANG'])]
+ output_name = os.path.join(kw['output_folder'],
+ 'assets', 'js', 'tag_cloud_data.json')
+
+ def write_tag_data(data):
+ utils.makedirs(os.path.dirname(output_name))
+ with codecs.open(output_name, 'wb+', 'utf8') as fd:
+ fd.write(json.dumps(data))
+
+ task = {
+ 'basename': str(self.name),
+ 'name': str(output_name)
+ }
+ task['uptodate'] = [utils.config_changed(tag_cloud_data)]
+ task['targets'] = [output_name]
+ task['actions'] = [(write_tag_data, [tag_cloud_data])]
+ task['clean'] = True
+ yield task
+
+ def list_tags_page(self, kw):
+ """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 = {}
+ 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,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ 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, 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(kind, tag, lang)
+ if i:
+ name = name.replace('.html', '-{0}.html'.format(i))
+ return name
+
+ # FIXME: deduplicate this with render_indexes
+ template_name = "index.tmpl"
+ # Split in smaller lists
+ lists = []
+ while post_list:
+ lists.append(post_list[:kw["index_display_post_count"]])
+ post_list = post_list[kw["index_display_post_count"]:]
+ num_pages = len(lists)
+ for i, post_list in enumerate(lists):
+ context = {}
+ # On a tag page, the feeds include the tag's feeds
+ rss_link = ("""<link rel="alternate" type="application/rss+xml" """
+ """type="application/rss+xml" title="RSS for tag """
+ """{0} ({1})" href="{2}">""".format(
+ 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))
+ context["title"] = kw["messages"][lang][
+ "Posts about %s"] % tag
+ context["prevlink"] = None
+ context["nextlink"] = None
+ context['index_teasers'] = kw['index_teasers']
+ if i > 1:
+ context["prevlink"] = os.path.basename(
+ page_name(tag, i - 1, lang))
+ if i == 1:
+ context["prevlink"] = os.path.basename(
+ page_name(tag, 0, lang))
+ if i < num_pages - 1:
+ context["nextlink"] = os.path.basename(
+ page_name(tag, i + 1, lang))
+ context["permalink"] = self.site.link(kind, tag, lang)
+ context["tag"] = tag
+ context["description"] = None
+ task = self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ 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, 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(
+ 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(kind, tag, lang)
+ context["tag"] = tag
+ context["kind"] = kind
+ context["description"] = None
+ task = self.site.generic_post_list_renderer(
+ lang,
+ post_list,
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ 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_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.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]
+ post_list.sort(key=lambda a: a.date)
+ post_list.reverse()
+ for post in post_list:
+ deps += post.deps(lang)
+ return {
+ 'basename': str(self.name),
+ 'name': output_name,
+ 'file_dep': deps,
+ 'targets': [output_name],
+ 'actions': [(utils.generic_rss_renderer,
+ (lang, "{0} ({1})".format(kw["blog_title"], tag),
+ 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]